parent
f19d044181
commit
5b22b46912
|
@ -7,11 +7,18 @@ import me.ash.reader.ui.ext.showToastLong
|
|||
import java.lang.Thread.UncaughtExceptionHandler
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* The uncaught exception handler for the application.
|
||||
*/
|
||||
class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
|
||||
|
||||
init {
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch all uncaught exception and log it.
|
||||
*/
|
||||
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
||||
Looper.myLooper() ?: Looper.prepare()
|
||||
context.showToastLong(p1.message)
|
||||
|
|
|
@ -10,14 +10,18 @@ import androidx.profileinstaller.ProfileInstallerInitializer
|
|||
import coil.ImageLoader
|
||||
import coil.compose.LocalImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import me.ash.reader.data.preference.LanguagesPreference
|
||||
import me.ash.reader.data.preference.SettingsProvider
|
||||
import me.ash.reader.data.model.preference.LanguagesPreference
|
||||
import me.ash.reader.data.model.preference.SettingsProvider
|
||||
import me.ash.reader.ui.ext.languages
|
||||
import me.ash.reader.ui.page.common.HomeEntry
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* The Single-Activity Architecture.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
|
|
|
@ -9,8 +9,9 @@ import dagger.hilt.android.HiltAndroidApp
|
|||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.ash.reader.data.module.ApplicationScope
|
||||
import me.ash.reader.data.module.DispatcherDefault
|
||||
import me.ash.reader.data.module.IODispatcher
|
||||
import me.ash.reader.data.repository.*
|
||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||
import me.ash.reader.data.source.RYDatabase
|
||||
|
@ -21,16 +22,23 @@ import org.conscrypt.Conscrypt
|
|||
import java.security.Security
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* The Application class, where the Dagger components is generated.
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class RYApp : Application(), Configuration.Provider {
|
||||
|
||||
/**
|
||||
* From: [Feeder](https://gitlab.com/spacecowboy/Feeder).
|
||||
*
|
||||
* Install Conscrypt to handle TLSv1.3 pre Android10.
|
||||
*/
|
||||
init {
|
||||
// From: https://gitlab.com/spacecowboy/Feeder
|
||||
// Install Conscrypt to handle TLSv1.3 pre Android10
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var RYDatabase: RYDatabase
|
||||
lateinit var ryDatabase: RYDatabase
|
||||
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
@ -39,7 +47,7 @@ class RYApp : Application(), Configuration.Provider {
|
|||
lateinit var workManager: WorkManager
|
||||
|
||||
@Inject
|
||||
lateinit var RYNetworkDataSource: RYNetworkDataSource
|
||||
lateinit var ryNetworkDataSource: RYNetworkDataSource
|
||||
|
||||
@Inject
|
||||
lateinit var opmlLocalDataSource: OpmlLocalDataSource
|
||||
|
@ -73,8 +81,8 @@ class RYApp : Application(), Configuration.Provider {
|
|||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
@DispatcherDefault
|
||||
lateinit var dispatcherDefault: CoroutineDispatcher
|
||||
@IODispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
|
@ -82,27 +90,40 @@ class RYApp : Application(), Configuration.Provider {
|
|||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
/**
|
||||
* When the application startup.
|
||||
*
|
||||
* 1. Set the uncaught exception handler
|
||||
* 2. Initialize the default account if there is none
|
||||
* 3. Synchronize once
|
||||
* 4. Check for new version
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CrashHandler(this)
|
||||
dataStoreInit()
|
||||
applicationScope.launch(dispatcherDefault) {
|
||||
applicationScope.launch {
|
||||
accountInit()
|
||||
workerInit()
|
||||
if (notFdroid) {
|
||||
checkUpdate()
|
||||
}
|
||||
if (notFdroid) checkUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun dataStoreInit() {
|
||||
}
|
||||
/**
|
||||
* Override the [Configuration.Builder] to provide the [HiltWorkerFactory].
|
||||
*/
|
||||
override fun getWorkManagerConfiguration(): Configuration =
|
||||
Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.setMinimumLoggingLevel(android.util.Log.DEBUG)
|
||||
.build()
|
||||
|
||||
private suspend fun accountInit() {
|
||||
withContext(ioDispatcher) {
|
||||
if (accountRepository.isNoAccount()) {
|
||||
val account = accountRepository.addDefaultAccount()
|
||||
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!)
|
||||
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountType, account.type)
|
||||
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountType, account.type.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,17 +132,11 @@ class RYApp : Application(), Configuration.Provider {
|
|||
}
|
||||
|
||||
private suspend fun checkUpdate() {
|
||||
withContext(ioDispatcher) {
|
||||
applicationContext.getLatestApk().let {
|
||||
if (it.exists()) {
|
||||
it.del()
|
||||
if (it.exists()) it.del()
|
||||
}
|
||||
}
|
||||
ryRepository.checkUpdate(showToast = false)
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration(): Configuration =
|
||||
Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.setMinimumLoggingLevel(android.util.Log.DEBUG)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
package me.ash.reader.data.constant
|
||||
|
||||
/**
|
||||
* The tonal elevation tokens.
|
||||
*
|
||||
* @see androidx.compose.material3.tokens.ElevationTokens
|
||||
*/
|
||||
object ElevationTokens {
|
||||
|
||||
const val Level0 = 0
|
||||
const val Level1 = 1
|
||||
const val Level2 = 3
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package me.ash.reader.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import me.ash.reader.data.entity.Account
|
||||
import me.ash.reader.data.model.account.Account
|
||||
|
||||
@Dao
|
||||
interface AccountDao {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM account
|
||||
|
|
|
@ -3,13 +3,14 @@ package me.ash.reader.data.dao
|
|||
import androidx.paging.PagingSource
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.ash.reader.data.entity.Article
|
||||
import me.ash.reader.data.entity.ArticleWithFeed
|
||||
import me.ash.reader.data.model.ImportantCount
|
||||
import me.ash.reader.data.model.article.Article
|
||||
import me.ash.reader.data.model.article.ArticleWithFeed
|
||||
import me.ash.reader.data.model.feed.ImportantNum
|
||||
import java.util.*
|
||||
|
||||
@Dao
|
||||
interface ArticleDao {
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
|
@ -298,8 +299,8 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryImportantCountWhenIsUnread(
|
||||
accountId: Int,
|
||||
isUnread: Boolean
|
||||
): Flow<List<ImportantCount>>
|
||||
isUnread: Boolean,
|
||||
): Flow<List<ImportantNum>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
|
@ -315,8 +316,8 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryImportantCountWhenIsStarred(
|
||||
accountId: Int,
|
||||
isStarred: Boolean
|
||||
): Flow<List<ImportantCount>>
|
||||
isStarred: Boolean,
|
||||
): Flow<List<ImportantNum>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
|
@ -329,7 +330,7 @@ interface ArticleDao {
|
|||
GROUP BY a.feedId
|
||||
"""
|
||||
)
|
||||
fun queryImportantCountWhenIsAll(accountId: Int): Flow<List<ImportantCount>>
|
||||
fun queryImportantCountWhenIsAll(accountId: Int): Flow<List<ImportantNum>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
|
@ -352,7 +353,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedWhenIsStarred(
|
||||
accountId: Int,
|
||||
isStarred: Boolean
|
||||
isStarred: Boolean,
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
@Transaction
|
||||
|
@ -366,7 +367,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedWhenIsUnread(
|
||||
accountId: Int,
|
||||
isUnread: Boolean
|
||||
isUnread: Boolean,
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
@Transaction
|
||||
|
@ -444,7 +445,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedByFeedIdWhenIsAll(
|
||||
accountId: Int,
|
||||
feedId: String
|
||||
feedId: String,
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
@Transaction
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package me.ash.reader.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import me.ash.reader.data.entity.Feed
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
|
||||
@Dao
|
||||
interface FeedDao {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE feed SET groupId = :targetGroupId
|
||||
|
@ -15,7 +16,7 @@ interface FeedDao {
|
|||
suspend fun updateTargetGroupIdByGroupId(
|
||||
accountId: Int,
|
||||
groupId: String,
|
||||
targetGroupId: String
|
||||
targetGroupId: String,
|
||||
)
|
||||
|
||||
@Query(
|
||||
|
@ -28,7 +29,7 @@ interface FeedDao {
|
|||
suspend fun updateIsFullContentByGroupId(
|
||||
accountId: Int,
|
||||
groupId: String,
|
||||
isFullContent: Boolean
|
||||
isFullContent: Boolean,
|
||||
)
|
||||
|
||||
@Query(
|
||||
|
@ -41,7 +42,7 @@ interface FeedDao {
|
|||
suspend fun updateIsNotificationByGroupId(
|
||||
accountId: Int,
|
||||
groupId: String,
|
||||
isNotification: Boolean
|
||||
isNotification: Boolean,
|
||||
)
|
||||
|
||||
@Query(
|
||||
|
|
|
@ -2,11 +2,12 @@ package me.ash.reader.data.dao
|
|||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.ash.reader.data.entity.Group
|
||||
import me.ash.reader.data.entity.GroupWithFeed
|
||||
import me.ash.reader.data.model.group.Group
|
||||
import me.ash.reader.data.model.group.GroupWithFeed
|
||||
|
||||
@Dao
|
||||
interface GroupDao {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM `group`
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package me.ash.reader.data.model
|
||||
|
||||
data class ImportantCount(
|
||||
val important: Int,
|
||||
val feedId: String,
|
||||
val groupId: String,
|
||||
)
|
|
@ -1,2 +0,0 @@
|
|||
package me.ash.reader.data.model
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package me.ash.reader.data.model
|
||||
|
||||
class Version(identifiers: List<String>) {
|
||||
private var major: Int = 0
|
||||
private var minor: Int = 0
|
||||
private var point: Int = 0
|
||||
|
||||
init {
|
||||
major = identifiers.getOrNull(0)?.toIntOrNull() ?: 0
|
||||
minor = identifiers.getOrNull(1)?.toIntOrNull() ?: 0
|
||||
point = identifiers.getOrNull(2)?.toIntOrNull() ?: 0
|
||||
}
|
||||
|
||||
constructor() : this(listOf())
|
||||
constructor(string: String?) : this(string?.split(".") ?: listOf())
|
||||
|
||||
fun whetherNeedUpdate(current: Version, skip: Version): Boolean = this > current && this > skip
|
||||
|
||||
operator fun compareTo(target: Version): Int = when {
|
||||
major > target.major -> 1
|
||||
major < target.major -> -1
|
||||
minor > target.minor -> 1
|
||||
minor < target.minor -> -1
|
||||
point > target.point -> 1
|
||||
point < target.point -> -1
|
||||
else -> 0
|
||||
}
|
||||
|
||||
override fun toString() = "$major.$minor.$point"
|
||||
}
|
||||
|
||||
fun String?.toVersion(): Version = Version(this)
|
|
@ -1,10 +1,14 @@
|
|||
package me.ash.reader.data.entity
|
||||
package me.ash.reader.data.model.account
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* In the application, at least one account exists and different accounts
|
||||
* can have the same feeds and articles.
|
||||
*/
|
||||
@Entity(tableName = "account")
|
||||
data class Account(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -12,13 +16,7 @@ data class Account(
|
|||
@ColumnInfo
|
||||
var name: String,
|
||||
@ColumnInfo
|
||||
var type: Int,
|
||||
var type: AccountType,
|
||||
@ColumnInfo
|
||||
var updateAt: Date? = null,
|
||||
) {
|
||||
object Type {
|
||||
const val LOCAL = 1
|
||||
const val FEVER = 2
|
||||
const val GOOGLE_READER = 3
|
||||
}
|
||||
}
|
||||
)
|
|
@ -0,0 +1,45 @@
|
|||
package me.ash.reader.data.model.account
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
|
||||
/**
|
||||
* Each account will specify its local or third-party API type.
|
||||
*/
|
||||
class AccountType(val id: Int) {
|
||||
|
||||
/**
|
||||
* Make sure the constructed object is valid.
|
||||
*/
|
||||
init {
|
||||
if (id < 1 || id > 3) {
|
||||
throw IllegalArgumentException("Account type id is not valid.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of account currently supported.
|
||||
*/
|
||||
companion object {
|
||||
|
||||
val Local = AccountType(1)
|
||||
val Fever = AccountType(2)
|
||||
val GoogleReader = AccountType(3)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide [TypeConverter] of [AccountType] for [RoomDatabase].
|
||||
*/
|
||||
class AccountTypeConverters {
|
||||
|
||||
@TypeConverter
|
||||
fun toAccountType(id: Int): AccountType {
|
||||
return AccountType(id)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromAccountType(accountType: AccountType): Int {
|
||||
return accountType.id
|
||||
}
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
package me.ash.reader.data.entity
|
||||
package me.ash.reader.data.model.article
|
||||
|
||||
import androidx.room.*
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* TODO: Add class description
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "article",
|
||||
foreignKeys = [ForeignKey(
|
||||
|
@ -43,6 +47,7 @@ data class Article(
|
|||
@ColumnInfo(defaultValue = "false")
|
||||
var isReadLater: Boolean = false,
|
||||
) {
|
||||
|
||||
@Ignore
|
||||
var dateString: String? = null
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package me.ash.reader.data.model.article
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.insertSeparators
|
||||
import androidx.paging.map
|
||||
import me.ash.reader.data.repository.StringsRepository
|
||||
|
||||
/**
|
||||
* Provide paginated and inserted separator data types for article list view.
|
||||
*
|
||||
* @see me.ash.reader.ui.page.home.flow.ArticleList
|
||||
*/
|
||||
sealed class ArticleFlowItem {
|
||||
|
||||
/**
|
||||
* The [Article] item.
|
||||
*
|
||||
* @see me.ash.reader.ui.page.home.flow.ArticleItem
|
||||
*/
|
||||
class Article(val articleWithFeed: ArticleWithFeed) : ArticleFlowItem()
|
||||
|
||||
/**
|
||||
* The feed publication date separator between [Article] items.
|
||||
*
|
||||
* @see me.ash.reader.ui.page.home.flow.StickyHeader
|
||||
*/
|
||||
class Date(val date: String, val showSpacer: Boolean) : ArticleFlowItem()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping [ArticleWithFeed] list to [ArticleFlowItem] list.
|
||||
*/
|
||||
fun PagingData<ArticleWithFeed>.mapPagingFlowItem(stringsRepository: StringsRepository): PagingData<ArticleFlowItem> =
|
||||
map {
|
||||
ArticleFlowItem.Article(it.apply {
|
||||
article.dateString = stringsRepository.formatAsString(
|
||||
date = article.date,
|
||||
onlyHourMinute = true
|
||||
)
|
||||
})
|
||||
}.insertSeparators { before, after ->
|
||||
val beforeDate =
|
||||
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
|
||||
val afterDate =
|
||||
stringsRepository.formatAsString(after?.articleWithFeed?.article?.date)
|
||||
if (beforeDate != afterDate) {
|
||||
afterDate?.let { ArticleFlowItem.Date(it, beforeDate != null) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
package me.ash.reader.data.entity
|
||||
package me.ash.reader.data.model.article
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
|
||||
/**
|
||||
* An [article] contains a [feed].
|
||||
*/
|
||||
data class ArticleWithFeed(
|
||||
@Embedded
|
||||
var article: Article,
|
|
@ -1,7 +1,11 @@
|
|||
package me.ash.reader.data.entity
|
||||
package me.ash.reader.data.model.feed
|
||||
|
||||
import androidx.room.*
|
||||
import me.ash.reader.data.model.group.Group
|
||||
|
||||
/**
|
||||
* TODO: Add class description
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "feed",
|
||||
foreignKeys = [ForeignKey(
|
||||
|
@ -30,6 +34,7 @@ data class Feed(
|
|||
@ColumnInfo(defaultValue = "false")
|
||||
var isFullContent: Boolean = false,
|
||||
) {
|
||||
|
||||
@Ignore
|
||||
var important: Int? = 0
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
package me.ash.reader.data.entity
|
||||
package me.ash.reader.data.model.feed
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import me.ash.reader.data.model.article.Article
|
||||
|
||||
/**
|
||||
* A [feed] contains many [articles].
|
||||
*/
|
||||
data class FeedWithArticle(
|
||||
@Embedded
|
||||
var feed: Feed,
|
||||
@Relation(parentColumn = "id", entityColumn = "feedId")
|
||||
var articles: List<Article>
|
||||
var articles: List<Article>,
|
||||
)
|
|
@ -1,11 +1,15 @@
|
|||
package me.ash.reader.data.entity
|
||||
package me.ash.reader.data.model.feed
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import me.ash.reader.data.model.group.Group
|
||||
|
||||
/**
|
||||
* A [feed] contains a [group].
|
||||
*/
|
||||
data class FeedWithGroup(
|
||||
@Embedded
|
||||
var feed: Feed,
|
||||
@Relation(parentColumn = "groupId", entityColumn = "id")
|
||||
var group: Group
|
||||
var group: Group,
|
||||
)
|
|
@ -0,0 +1,15 @@
|
|||
package me.ash.reader.data.model.feed
|
||||
|
||||
/**
|
||||
* Counting the [important] number of articles in feeds and groups is generally
|
||||
* used in three situations.
|
||||
*
|
||||
* - Unread: Articles that have not been read yet
|
||||
* - Starred: Articles that have been marked as starred
|
||||
* - All: All articles
|
||||
*/
|
||||
data class ImportantNum(
|
||||
val important: Int,
|
||||
val feedId: String,
|
||||
val groupId: String,
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.model
|
||||
package me.ash.reader.data.model.general
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FiberManualRecord
|
||||
|
@ -13,17 +13,46 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.model.general.Filter.Companion.All
|
||||
import me.ash.reader.data.model.general.Filter.Companion.Starred
|
||||
import me.ash.reader.data.model.general.Filter.Companion.Unread
|
||||
|
||||
class Filter(
|
||||
/**
|
||||
* Indicates filter conditions.
|
||||
*
|
||||
* - [All]: all items
|
||||
* - [Unread]: unread items
|
||||
* - [Starred]: starred items
|
||||
*/
|
||||
class Filter private constructor(
|
||||
val index: Int,
|
||||
val iconOutline: ImageVector,
|
||||
val iconFilled: ImageVector,
|
||||
) {
|
||||
|
||||
fun isStarred(): Boolean = this == Starred
|
||||
fun isUnread(): Boolean = this == Unread
|
||||
fun isAll(): Boolean = this == All
|
||||
|
||||
@Stable
|
||||
@Composable
|
||||
fun toName(): String = when (this) {
|
||||
Unread -> stringResource(R.string.unread)
|
||||
Starred -> stringResource(R.string.starred)
|
||||
else -> stringResource(R.string.all)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Stable
|
||||
@Composable
|
||||
fun toDesc(important: Int): String = when (this) {
|
||||
Starred -> pluralStringResource(R.plurals.starred_desc, important, important)
|
||||
Unread -> pluralStringResource(R.plurals.unread_desc, important, important)
|
||||
else -> pluralStringResource(R.plurals.all_desc, important, important)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val Starred = Filter(
|
||||
index = 0,
|
||||
iconOutline = Icons.Rounded.StarOutline,
|
||||
|
@ -42,20 +71,3 @@ class Filter(
|
|||
val values = listOf(Starred, Unread, All)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
@Composable
|
||||
fun Filter.getName(): String = when (this) {
|
||||
Filter.Unread -> stringResource(R.string.unread)
|
||||
Filter.Starred -> stringResource(R.string.starred)
|
||||
else -> stringResource(R.string.all)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Stable
|
||||
@Composable
|
||||
fun Filter.getDesc(important: Int): String = when (this) {
|
||||
Filter.Starred -> pluralStringResource(R.plurals.starred_desc, important, important)
|
||||
Filter.Unread -> pluralStringResource(R.plurals.unread_desc, important, important)
|
||||
else -> pluralStringResource(R.plurals.all_desc, important, important)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package me.ash.reader.data.model.general
|
||||
|
||||
import me.ash.reader.data.model.general.MarkAsReadConditions.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Mark as read conditions.
|
||||
*
|
||||
* - [SevenDays]: Mark as read if more than 7 days old
|
||||
* - [ThreeDays]: Mark as read if more than 3 days old
|
||||
* - [OneDay]: Mark as read if more than 1 day old
|
||||
* - [All]: Mark all as read
|
||||
*/
|
||||
enum class MarkAsReadConditions {
|
||||
SevenDays,
|
||||
ThreeDays,
|
||||
OneDay,
|
||||
All,
|
||||
;
|
||||
|
||||
fun toDate(): Date? = when (this) {
|
||||
All -> null
|
||||
else -> Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.DAY_OF_MONTH, when (this@MarkAsReadConditions) {
|
||||
SevenDays -> -7
|
||||
ThreeDays -> -3
|
||||
OneDay -> -1
|
||||
else -> throw IllegalArgumentException("Unknown condition: $this")
|
||||
})
|
||||
}.time
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package me.ash.reader.data.model.general
|
||||
|
||||
/**
|
||||
* Application version number, consisting of three fields.
|
||||
*
|
||||
* - [major]: The major version number, such as 1
|
||||
* - [minor]: The major version number, such as 2
|
||||
* - [point]: The major version number, such as 3 (if converted to a string,
|
||||
* the value is: "1.2.3")
|
||||
*/
|
||||
class Version(numbers: List<String>) {
|
||||
|
||||
private var major: Int = 0
|
||||
private var minor: Int = 0
|
||||
private var point: Int = 0
|
||||
|
||||
init {
|
||||
major = numbers.getOrNull(0)?.toIntOrNull() ?: 0
|
||||
minor = numbers.getOrNull(1)?.toIntOrNull() ?: 0
|
||||
point = numbers.getOrNull(2)?.toIntOrNull() ?: 0
|
||||
}
|
||||
|
||||
constructor() : this(listOf())
|
||||
constructor(string: String?) : this(string?.split(".") ?: listOf())
|
||||
|
||||
override fun toString() = "$major.$minor.$point"
|
||||
|
||||
/**
|
||||
* Use [major], [minor], [point] for comparison.
|
||||
*
|
||||
* 1. [major] <=> [other.major]
|
||||
* 2. [minor] <=> [other.minor]
|
||||
* 3. [point] <=> [other.point]
|
||||
*/
|
||||
operator fun compareTo(other: Version): Int = when {
|
||||
major > other.major -> 1
|
||||
major < other.major -> -1
|
||||
minor > other.minor -> 1
|
||||
minor < other.minor -> -1
|
||||
point > other.point -> 1
|
||||
point < other.point -> -1
|
||||
else -> 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this version is larger [current] version and [skip] version.
|
||||
*/
|
||||
fun whetherNeedUpdate(current: Version, skip: Version): Boolean = this > current && this > skip
|
||||
}
|
||||
|
||||
fun String?.toVersion(): Version = Version(this)
|
|
@ -1,10 +1,13 @@
|
|||
package me.ash.reader.data.entity
|
||||
package me.ash.reader.data.model.group
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* TODO: Add class description
|
||||
*/
|
||||
@Entity(tableName = "group")
|
||||
data class Group(
|
||||
@PrimaryKey
|
||||
|
@ -14,6 +17,7 @@ data class Group(
|
|||
@ColumnInfo(index = true)
|
||||
var accountId: Int,
|
||||
) {
|
||||
|
||||
@Ignore
|
||||
var important: Int? = 0
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
package me.ash.reader.data.entity
|
||||
package me.ash.reader.data.model.group
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
|
||||
/**
|
||||
* A [group] contains many [feeds].
|
||||
*/
|
||||
data class GroupWithFeed(
|
||||
@Embedded
|
||||
var group: Group,
|
||||
@Relation(parentColumn = "id", entityColumn = "groupId")
|
||||
var feeds: MutableList<Feed>
|
||||
var feeds: MutableList<Feed>,
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class AmoledDarkThemePreference(val value: Boolean) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = OFF
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -9,6 +9,7 @@ import me.ash.reader.ui.ext.dataStore
|
|||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object CustomPrimaryColorPreference {
|
||||
|
||||
const val default = ""
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: String) {
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
|
@ -26,7 +26,7 @@ sealed class DarkThemePreference(val value: Int) : Preference() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
UseDeviceTheme -> context.getString(R.string.use_device_theme)
|
||||
ON -> context.getString(R.string.on)
|
||||
|
@ -42,6 +42,7 @@ sealed class DarkThemePreference(val value: Int) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = UseDeviceTheme
|
||||
val values = listOf(UseDeviceTheme, ON, OFF)
|
||||
|
||||
|
@ -63,6 +64,7 @@ operator fun DarkThemePreference.not(): DarkThemePreference =
|
|||
} else {
|
||||
DarkThemePreference.ON
|
||||
}
|
||||
|
||||
DarkThemePreference.ON -> DarkThemePreference.OFF
|
||||
DarkThemePreference.OFF -> DarkThemePreference.ON
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FeedsFilterBarFilledPreference(val value: Boolean) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = OFF
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -9,6 +9,7 @@ import me.ash.reader.ui.ext.dataStore
|
|||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object FeedsFilterBarPaddingPreference {
|
||||
|
||||
const val default = 60
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: Int) {
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -23,7 +23,7 @@ sealed class FeedsFilterBarStylePreference(val value: Int) : Preference() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Icon -> context.getString(R.string.icons)
|
||||
IconLabel -> context.getString(R.string.icons_and_labels)
|
||||
|
@ -31,6 +31,7 @@ sealed class FeedsFilterBarStylePreference(val value: Int) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Icon
|
||||
val values = listOf(Icon, IconLabel, IconLabelOnlySelected)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -26,7 +26,7 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
|
@ -37,6 +37,7 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Level0
|
||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FeedsGroupListExpandPreference(val value: Boolean) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = ON
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -26,7 +26,7 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
|
@ -37,6 +37,7 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Level0
|
||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -26,7 +26,7 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
|
@ -37,6 +37,7 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Level0
|
||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FlowArticleListDateStickyHeaderPreference(val value: Boolean) : Pre
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = ON
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FlowArticleListDescPreference(val value: Boolean) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = ON
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FlowArticleListFeedIconPreference(val value: Boolean) : Preference(
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = ON
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FlowArticleListFeedNamePreference(val value: Boolean) : Preference(
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = ON
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FlowArticleListImagePreference(val value: Boolean) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = ON
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FlowArticleListTimePreference(val value: Boolean) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = ON
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -26,7 +26,7 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
|
@ -37,6 +37,7 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Level0
|
||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,6 +22,7 @@ sealed class FlowFilterBarFilledPreference(val value: Boolean) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = OFF
|
||||
val values = listOf(ON, OFF)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -9,6 +9,7 @@ import me.ash.reader.ui.ext.dataStore
|
|||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object FlowFilterBarPaddingPreference {
|
||||
|
||||
const val default = 60
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: Int) {
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -23,7 +23,7 @@ sealed class FlowFilterBarStylePreference(val value: Int) : Preference() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Icon -> context.getString(R.string.icons)
|
||||
IconLabel -> context.getString(R.string.icons_and_labels)
|
||||
|
@ -31,6 +31,7 @@ sealed class FlowFilterBarStylePreference(val value: Int) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Icon
|
||||
val values = listOf(Icon, IconLabel, IconLabelOnlySelected)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -26,7 +26,7 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
|
@ -37,6 +37,7 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Level0
|
||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -26,7 +26,7 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
|
@ -37,6 +37,7 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Level0
|
||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -23,7 +23,7 @@ sealed class InitialFilterPreference(val value: Int) : Preference() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Starred -> context.getString(R.string.starred)
|
||||
Unread -> context.getString(R.string.unread)
|
||||
|
@ -31,6 +31,7 @@ sealed class InitialFilterPreference(val value: Int) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = All
|
||||
val values = listOf(Starred, Unread, All)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -22,13 +22,14 @@ sealed class InitialPagePreference(val value: Int) : Preference() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
FeedsPage -> context.getString(R.string.feeds_page)
|
||||
FlowPage -> context.getString(R.string.flow_page)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = FeedsPage
|
||||
val values = listOf(FeedsPage, FlowPage)
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.os.LocaleList
|
||||
import android.util.Log
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -19,6 +18,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
|||
object German : LanguagesPreference(3)
|
||||
object French : LanguagesPreference(4)
|
||||
object Czech : LanguagesPreference(5)
|
||||
object Italian : LanguagesPreference(6)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
|
@ -30,7 +30,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getDesc(context: Context): String =
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
UseDeviceLanguages -> context.getString(R.string.use_device_languages)
|
||||
English -> context.getString(R.string.english)
|
||||
|
@ -38,6 +38,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
|||
German -> context.getString(R.string.german)
|
||||
French -> context.getString(R.string.french)
|
||||
Czech -> context.getString(R.string.czech)
|
||||
Italian -> context.getString(R.string.italian)
|
||||
}
|
||||
|
||||
fun getLocale(): Locale =
|
||||
|
@ -48,13 +49,11 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
|||
German -> Locale("de", "DE")
|
||||
French -> Locale("fr", "FR")
|
||||
Czech -> Locale("cs", "CZ")
|
||||
Italian -> Locale("it", "IT")
|
||||
}
|
||||
|
||||
fun setLocale(context: Context) {
|
||||
val locale = getLocale()
|
||||
|
||||
Log.i("Rlog", "setLocale: $locale, ${LocaleList.getDefault().get(0)}")
|
||||
|
||||
val resources = context.resources
|
||||
val metrics = resources.displayMetrics
|
||||
val configuration = resources.configuration
|
||||
|
@ -73,8 +72,9 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = UseDeviceLanguages
|
||||
val values = listOf(UseDeviceLanguages, English, ChineseSimplified, German, French, Czech)
|
||||
val values = listOf(UseDeviceLanguages, English, ChineseSimplified, German, French, Czech, Italian)
|
||||
|
||||
fun fromPreferences(preferences: Preferences): LanguagesPreference =
|
||||
when (preferences[DataStoreKeys.Languages.key]) {
|
||||
|
@ -84,6 +84,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
|||
3 -> German
|
||||
4 -> French
|
||||
5 -> Czech
|
||||
6 -> Italian
|
||||
else -> default
|
||||
}
|
||||
|
||||
|
@ -95,6 +96,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
|||
3 -> German
|
||||
4 -> French
|
||||
5 -> Czech
|
||||
6 -> Italian
|
||||
else -> default
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
|
|||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object NewVersionDownloadUrlPreference {
|
||||
|
||||
const val default = ""
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: String) {
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
|
|||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object NewVersionLogPreference {
|
||||
|
||||
const val default = ""
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: String) {
|
|
@ -1,17 +1,18 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.model.Version
|
||||
import me.ash.reader.data.model.toVersion
|
||||
import me.ash.reader.data.model.general.Version
|
||||
import me.ash.reader.data.model.general.toVersion
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object NewVersionNumberPreference {
|
||||
|
||||
val default = Version()
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: String) {
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
|
|||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object NewVersionPublishDatePreference {
|
||||
|
||||
const val default = ""
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: String) {
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
|
|||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object NewVersionSizePreference {
|
||||
|
||||
const val default = ""
|
||||
|
||||
fun Int.formatSize(): String =
|
|
@ -1,10 +1,11 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
sealed class Preference {
|
||||
|
||||
abstract fun put(context: Context, scope: CoroutineScope)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -7,7 +7,7 @@ import androidx.compose.runtime.compositionLocalOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.ash.reader.data.model.Version
|
||||
import me.ash.reader.data.model.general.Version
|
||||
import me.ash.reader.ui.ext.collectAsStateValue
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.model.Version
|
||||
import me.ash.reader.data.model.toVersion
|
||||
import me.ash.reader.data.model.general.Version
|
||||
import me.ash.reader.data.model.general.toVersion
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object SkipVersionNumberPreference {
|
||||
|
||||
val default = Version()
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: String) {
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.data.preference
|
||||
package me.ash.reader.data.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
|
|||
import me.ash.reader.ui.ext.put
|
||||
|
||||
object ThemeIndexPreference {
|
||||
|
||||
const val default = 5
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: Int) {
|
|
@ -0,0 +1,13 @@
|
|||
package me.ash.reader.data.module
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Qualifier
|
||||
|
||||
/**
|
||||
* Provides [CoroutineScope] for the application.
|
||||
*
|
||||
* @see CoroutineScopeModule.provideCoroutineScope
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class ApplicationScope
|
|
@ -7,23 +7,31 @@ import dagger.hilt.components.SingletonComponent
|
|||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Provides global coroutine dispatcher.
|
||||
*
|
||||
* - [Dispatchers.Main]
|
||||
* - [Dispatchers.Main.immediate]
|
||||
* - [Dispatchers.IO]
|
||||
* - [Dispatchers.Default]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutineDispatcherModule {
|
||||
|
||||
@Provides
|
||||
@DispatcherDefault
|
||||
fun provideDispatcherDefault(): CoroutineDispatcher = Dispatchers.Default
|
||||
@DefaultDispatcher
|
||||
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@Provides
|
||||
@DispatcherIO
|
||||
fun provideDispatcherIO(): CoroutineDispatcher = Dispatchers.IO
|
||||
@IODispatcher
|
||||
fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@Provides
|
||||
@DispatcherMain
|
||||
fun provideDispatcherMain(): CoroutineDispatcher = Dispatchers.Main
|
||||
@MainDispatcher
|
||||
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
|
||||
@Provides
|
||||
@DispatcherMainImmediate
|
||||
fun provideDispatcherMainImmediate(): CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
@MainImmediateDispatcher
|
||||
fun provideMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
}
|
||||
|
|
|
@ -2,18 +2,30 @@ package me.ash.reader.data.module
|
|||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
/**
|
||||
* @see CoroutineDispatcherModule.provideDefaultDispatcher
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class DispatcherDefault
|
||||
annotation class DefaultDispatcher
|
||||
|
||||
/**
|
||||
* @see CoroutineDispatcherModule.provideIODispatcher
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class DispatcherIO
|
||||
annotation class IODispatcher
|
||||
|
||||
/**
|
||||
* @see CoroutineDispatcherModule.provideMainDispatcher
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class DispatcherMain
|
||||
annotation class MainDispatcher
|
||||
|
||||
/**
|
||||
* @see CoroutineDispatcherModule.provideMainImmediateDispatcher
|
||||
*/
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class DispatcherMainImmediate
|
||||
annotation class MainImmediateDispatcher
|
||||
|
|
|
@ -7,13 +7,12 @@ import dagger.hilt.components.SingletonComponent
|
|||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class ApplicationScope
|
||||
|
||||
/**
|
||||
* [CoroutineScope] for the application consisting of [SupervisorJob]
|
||||
* and [DefaultDispatcher] context.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutineScopeModule {
|
||||
|
@ -22,6 +21,6 @@ object CoroutineScopeModule {
|
|||
@Singleton
|
||||
@ApplicationScope
|
||||
fun provideCoroutineScope(
|
||||
@DispatcherDefault dispatcherDefault: CoroutineDispatcher
|
||||
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcherDefault)
|
||||
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher,
|
||||
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
|
||||
}
|
|
@ -13,29 +13,37 @@ import me.ash.reader.data.dao.GroupDao
|
|||
import me.ash.reader.data.source.RYDatabase
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides Data Access Objects for database.
|
||||
*
|
||||
* - [ArticleDao]
|
||||
* - [FeedDao]
|
||||
* - [GroupDao]
|
||||
* - [AccountDao]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideArticleDao(RYDatabase: RYDatabase): ArticleDao =
|
||||
RYDatabase.articleDao()
|
||||
fun provideArticleDao(ryDatabase: RYDatabase): ArticleDao =
|
||||
ryDatabase.articleDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeedDao(RYDatabase: RYDatabase): FeedDao =
|
||||
RYDatabase.feedDao()
|
||||
fun provideFeedDao(ryDatabase: RYDatabase): FeedDao =
|
||||
ryDatabase.feedDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGroupDao(RYDatabase: RYDatabase): GroupDao =
|
||||
RYDatabase.groupDao()
|
||||
fun provideGroupDao(ryDatabase: RYDatabase): GroupDao =
|
||||
ryDatabase.groupDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountDao(RYDatabase: RYDatabase): AccountDao =
|
||||
RYDatabase.accountDao()
|
||||
fun provideAccountDao(ryDatabase: RYDatabase): AccountDao =
|
||||
ryDatabase.accountDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
@ -18,6 +18,9 @@ import kotlinx.coroutines.Dispatchers
|
|||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides singleton [ImageLoader] for Coil.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ImageLoaderModule {
|
||||
|
@ -29,10 +32,14 @@ object ImageLoaderModule {
|
|||
okHttpClient: OkHttpClient,
|
||||
): ImageLoader {
|
||||
return ImageLoader.Builder(context)
|
||||
// Shared OKHttpClient instance.
|
||||
.okHttpClient(okHttpClient)
|
||||
.dispatcher(Dispatchers.Default) // This slightly improves scrolling performance
|
||||
// This slightly improves scrolling performance
|
||||
.dispatcher(Dispatchers.Default)
|
||||
.components {
|
||||
// Support SVG decoding
|
||||
add(SvgDecoder.Factory())
|
||||
// Support GIF decoding
|
||||
add(
|
||||
if (SDK_INT >= Build.VERSION_CODES.P) {
|
||||
ImageDecoderDecoder.Factory()
|
||||
|
@ -41,12 +48,14 @@ object ImageLoaderModule {
|
|||
}
|
||||
)
|
||||
}
|
||||
// Enable disk cache
|
||||
.diskCache(
|
||||
DiskCache.Builder()
|
||||
.directory(context.cacheDir.resolve("images"))
|
||||
.maxSizePercent(0.02)
|
||||
.build()
|
||||
)
|
||||
// Enable memory cache
|
||||
.memoryCache(
|
||||
MemoryCache.Builder(context)
|
||||
.maxSizePercent(0.25)
|
||||
|
|
|
@ -42,13 +42,17 @@ import javax.net.ssl.SSLContext
|
|||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
* Provides singleton [OkHttpClient] for the application.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object OkHttpClientModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
@ApplicationContext context: Context
|
||||
@ApplicationContext context: Context,
|
||||
): OkHttpClient = cachingHttpClient(
|
||||
cacheDirectory = context.cacheDir.resolve("http")
|
||||
).newBuilder()
|
||||
|
@ -61,7 +65,7 @@ fun cachingHttpClient(
|
|||
cacheSize: Long = 10L * 1024L * 1024L,
|
||||
trustAllCerts: Boolean = true,
|
||||
connectTimeoutSecs: Long = 30L,
|
||||
readTimeoutSecs: Long = 30L
|
||||
readTimeoutSecs: Long = 30L,
|
||||
): OkHttpClient {
|
||||
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
|
||||
|
||||
|
@ -107,6 +111,7 @@ fun OkHttpClient.Builder.trustAllCerts() {
|
|||
}
|
||||
|
||||
object UserAgentInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
return chain.proceed(
|
||||
chain.request()
|
||||
|
|
|
@ -4,11 +4,18 @@ import dagger.Module
|
|||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import me.ash.reader.data.source.RYNetworkDataSource
|
||||
import me.ash.reader.data.source.FeverApiDataSource
|
||||
import me.ash.reader.data.source.GoogleReaderApiDataSource
|
||||
import me.ash.reader.data.source.RYNetworkDataSource
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides network requests for Retrofit.
|
||||
*
|
||||
* - [RYNetworkDataSource]: For network requests within the application
|
||||
* - [FeverApiDataSource]: For network requests to the Fever API
|
||||
* - [GoogleReaderApiDataSource]: For network requests to the Google Reader API
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object RetrofitModule {
|
||||
|
|
|
@ -9,6 +9,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides singleton [WorkManager] for the application.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object WorkerModule {
|
||||
|
|
|
@ -15,7 +15,11 @@ import me.ash.reader.data.dao.AccountDao
|
|||
import me.ash.reader.data.dao.ArticleDao
|
||||
import me.ash.reader.data.dao.FeedDao
|
||||
import me.ash.reader.data.dao.GroupDao
|
||||
import me.ash.reader.data.entity.*
|
||||
import me.ash.reader.data.model.article.Article
|
||||
import me.ash.reader.data.model.article.ArticleWithFeed
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
import me.ash.reader.data.model.group.Group
|
||||
import me.ash.reader.data.model.group.GroupWithFeed
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import java.util.*
|
||||
|
||||
|
@ -29,6 +33,7 @@ abstract class AbstractRssRepository constructor(
|
|||
private val dispatcherIO: CoroutineDispatcher,
|
||||
private val dispatcherDefault: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
abstract suspend fun updateArticleInfo(article: Article)
|
||||
|
||||
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
|
||||
|
@ -53,19 +58,17 @@ abstract class AbstractRssRepository constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun pullGroups(): Flow<MutableList<Group>> {
|
||||
return groupDao.queryAllGroup(context.currentAccountId).flowOn(dispatcherIO)
|
||||
}
|
||||
fun pullGroups(): Flow<MutableList<Group>> =
|
||||
groupDao.queryAllGroup(context.currentAccountId).flowOn(dispatcherIO)
|
||||
|
||||
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
||||
return groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(dispatcherIO)
|
||||
}
|
||||
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> =
|
||||
groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(dispatcherIO)
|
||||
|
||||
fun pullArticles(
|
||||
groupId: String? = null,
|
||||
feedId: String? = null,
|
||||
isStarred: Boolean = false,
|
||||
isUnread: Boolean = false,
|
||||
groupId: String?,
|
||||
feedId: String?,
|
||||
isStarred: Boolean,
|
||||
isUnread: Boolean,
|
||||
): PagingSource<Int, ArticleWithFeed> {
|
||||
val accountId = context.currentAccountId
|
||||
Log.i(
|
||||
|
@ -74,32 +77,28 @@ abstract class AbstractRssRepository constructor(
|
|||
)
|
||||
return when {
|
||||
groupId != null -> when {
|
||||
isStarred -> articleDao
|
||||
.queryArticleWithFeedByGroupIdWhenIsStarred(accountId, groupId, isStarred)
|
||||
isUnread -> articleDao
|
||||
.queryArticleWithFeedByGroupIdWhenIsUnread(accountId, groupId, isUnread)
|
||||
isStarred -> articleDao.queryArticleWithFeedByGroupIdWhenIsStarred(accountId, groupId, true)
|
||||
isUnread -> articleDao.queryArticleWithFeedByGroupIdWhenIsUnread(accountId, groupId, true)
|
||||
else -> articleDao.queryArticleWithFeedByGroupIdWhenIsAll(accountId, groupId)
|
||||
}
|
||||
|
||||
feedId != null -> when {
|
||||
isStarred -> articleDao
|
||||
.queryArticleWithFeedByFeedIdWhenIsStarred(accountId, feedId, isStarred)
|
||||
isUnread -> articleDao
|
||||
.queryArticleWithFeedByFeedIdWhenIsUnread(accountId, feedId, isUnread)
|
||||
isStarred -> articleDao.queryArticleWithFeedByFeedIdWhenIsStarred(accountId, feedId, true)
|
||||
isUnread -> articleDao.queryArticleWithFeedByFeedIdWhenIsUnread(accountId, feedId, true)
|
||||
else -> articleDao.queryArticleWithFeedByFeedIdWhenIsAll(accountId, feedId)
|
||||
}
|
||||
|
||||
else -> when {
|
||||
isStarred -> articleDao
|
||||
.queryArticleWithFeedWhenIsStarred(accountId, isStarred)
|
||||
isUnread -> articleDao
|
||||
.queryArticleWithFeedWhenIsUnread(accountId, isUnread)
|
||||
isStarred -> articleDao.queryArticleWithFeedWhenIsStarred(accountId, true)
|
||||
isUnread -> articleDao.queryArticleWithFeedWhenIsUnread(accountId, true)
|
||||
else -> articleDao.queryArticleWithFeedWhenIsAll(accountId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pullImportant(
|
||||
isStarred: Boolean = false,
|
||||
isUnread: Boolean = false,
|
||||
isStarred: Boolean,
|
||||
isUnread: Boolean,
|
||||
): Flow<Map<String, Int>> {
|
||||
val accountId = context.currentAccountId
|
||||
Log.i(
|
||||
|
@ -107,42 +106,28 @@ abstract class AbstractRssRepository constructor(
|
|||
"pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
|
||||
)
|
||||
return when {
|
||||
isStarred -> articleDao
|
||||
.queryImportantCountWhenIsStarred(accountId, isStarred)
|
||||
isUnread -> articleDao
|
||||
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
||||
isStarred -> articleDao.queryImportantCountWhenIsStarred(accountId, true)
|
||||
isUnread -> articleDao.queryImportantCountWhenIsUnread(accountId, true)
|
||||
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
||||
}.mapLatest {
|
||||
mapOf(
|
||||
// Groups
|
||||
*(it.groupBy { it.groupId }.map {
|
||||
it.key to it.value.sumOf { it.important }
|
||||
}.toTypedArray()),
|
||||
*(it.groupBy { it.groupId }.map { it.key to it.value.sumOf { it.important } }.toTypedArray()),
|
||||
// Feeds
|
||||
*(it.map {
|
||||
it.feedId to it.important
|
||||
}.toTypedArray()),
|
||||
*(it.map { it.feedId to it.important }.toTypedArray()),
|
||||
// All summary
|
||||
"sum" to it.sumOf { it.important }
|
||||
)
|
||||
}.flowOn(dispatcherDefault)
|
||||
}
|
||||
|
||||
suspend fun findFeedById(id: String): Feed? {
|
||||
return feedDao.queryById(id)
|
||||
}
|
||||
suspend fun findFeedById(id: String): Feed? = feedDao.queryById(id)
|
||||
|
||||
suspend fun findGroupById(id: String): Group? {
|
||||
return groupDao.queryById(id)
|
||||
}
|
||||
suspend fun findGroupById(id: String): Group? = groupDao.queryById(id)
|
||||
|
||||
suspend fun findArticleById(id: String): ArticleWithFeed? {
|
||||
return articleDao.queryById(id)
|
||||
}
|
||||
suspend fun findArticleById(id: String): ArticleWithFeed? = articleDao.queryById(id)
|
||||
|
||||
suspend fun isFeedExist(url: String): Boolean {
|
||||
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
||||
}
|
||||
suspend fun isFeedExist(url: String): Boolean = feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
||||
|
||||
suspend fun updateGroup(group: Group) {
|
||||
groupDao.update(group)
|
||||
|
@ -184,10 +169,10 @@ abstract class AbstractRssRepository constructor(
|
|||
|
||||
fun searchArticles(
|
||||
content: String,
|
||||
groupId: String? = null,
|
||||
feedId: String? = null,
|
||||
isStarred: Boolean = false,
|
||||
isUnread: Boolean = false,
|
||||
groupId: String?,
|
||||
feedId: String?,
|
||||
isStarred: Boolean,
|
||||
isUnread: Boolean,
|
||||
): PagingSource<Int, ArticleWithFeed> {
|
||||
val accountId = context.currentAccountId
|
||||
Log.i(
|
||||
|
@ -196,22 +181,20 @@ abstract class AbstractRssRepository constructor(
|
|||
)
|
||||
return when {
|
||||
groupId != null -> when {
|
||||
isStarred -> articleDao
|
||||
.searchArticleByGroupIdWhenIsStarred(accountId, content, groupId, isStarred)
|
||||
isUnread -> articleDao
|
||||
.searchArticleByGroupIdWhenIsUnread(accountId, content, groupId, isUnread)
|
||||
isStarred -> articleDao.searchArticleByGroupIdWhenIsStarred(accountId, content, groupId, true)
|
||||
isUnread -> articleDao.searchArticleByGroupIdWhenIsUnread(accountId, content, groupId, true)
|
||||
else -> articleDao.searchArticleByGroupIdWhenAll(accountId, content, groupId)
|
||||
}
|
||||
|
||||
feedId != null -> when {
|
||||
isStarred -> articleDao
|
||||
.searchArticleByFeedIdWhenIsStarred(accountId, content, feedId, isStarred)
|
||||
isUnread -> articleDao
|
||||
.searchArticleByFeedIdWhenIsUnread(accountId, content, feedId, isUnread)
|
||||
isStarred -> articleDao.searchArticleByFeedIdWhenIsStarred(accountId, content, feedId, true)
|
||||
isUnread -> articleDao.searchArticleByFeedIdWhenIsUnread(accountId, content, feedId, true)
|
||||
else -> articleDao.searchArticleByFeedIdWhenAll(accountId, content, feedId)
|
||||
}
|
||||
|
||||
else -> when {
|
||||
isStarred -> articleDao.searchArticleWhenIsStarred(accountId, content, isStarred)
|
||||
isUnread -> articleDao.searchArticleWhenIsUnread(accountId, content, isUnread)
|
||||
isStarred -> articleDao.searchArticleWhenIsStarred(accountId, content, true)
|
||||
isUnread -> articleDao.searchArticleWhenIsUnread(accountId, content, true)
|
||||
else -> articleDao.searchArticleWhenAll(accountId, content)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import me.ash.reader.R
|
||||
import me.ash.reader.data.dao.AccountDao
|
||||
import me.ash.reader.data.dao.GroupDao
|
||||
import me.ash.reader.data.entity.Account
|
||||
import me.ash.reader.data.entity.Group
|
||||
import me.ash.reader.data.model.account.Account
|
||||
import me.ash.reader.data.model.account.AccountType
|
||||
import me.ash.reader.data.model.group.Group
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.ext.getDefaultGroupId
|
||||
import javax.inject.Inject
|
||||
|
@ -18,20 +19,16 @@ class AccountRepository @Inject constructor(
|
|||
private val groupDao: GroupDao,
|
||||
) {
|
||||
|
||||
suspend fun getCurrentAccount(): Account? {
|
||||
return accountDao.queryById(context.currentAccountId)
|
||||
}
|
||||
suspend fun getCurrentAccount(): Account? = accountDao.queryById(context.currentAccountId)
|
||||
|
||||
suspend fun isNoAccount(): Boolean {
|
||||
return accountDao.queryAll().isEmpty()
|
||||
}
|
||||
suspend fun isNoAccount(): Boolean = accountDao.queryAll().isEmpty()
|
||||
|
||||
suspend fun addDefaultAccount(): Account {
|
||||
val readYouString = context.getString(R.string.read_you)
|
||||
val defaultString = context.getString(R.string.defaults)
|
||||
return Account(
|
||||
name = readYouString,
|
||||
type = Account.Type.LOCAL,
|
||||
type = AccountType.Local,
|
||||
).apply {
|
||||
id = accountDao.insert(this).toInt()
|
||||
}.also {
|
||||
|
|
|
@ -14,12 +14,12 @@ import me.ash.reader.data.dao.AccountDao
|
|||
import me.ash.reader.data.dao.ArticleDao
|
||||
import me.ash.reader.data.dao.FeedDao
|
||||
import me.ash.reader.data.dao.GroupDao
|
||||
import me.ash.reader.data.entity.Article
|
||||
import me.ash.reader.data.entity.Feed
|
||||
import me.ash.reader.data.entity.FeedWithArticle
|
||||
import me.ash.reader.data.entity.Group
|
||||
import me.ash.reader.data.module.DispatcherDefault
|
||||
import me.ash.reader.data.module.DispatcherIO
|
||||
import me.ash.reader.data.model.article.Article
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
import me.ash.reader.data.model.feed.FeedWithArticle
|
||||
import me.ash.reader.data.model.group.Group
|
||||
import me.ash.reader.data.module.DefaultDispatcher
|
||||
import me.ash.reader.data.module.IODispatcher
|
||||
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.ext.spacerDollar
|
||||
|
@ -35,14 +35,14 @@ class LocalRssRepository @Inject constructor(
|
|||
private val notificationHelper: NotificationHelper,
|
||||
private val accountDao: AccountDao,
|
||||
private val groupDao: GroupDao,
|
||||
@DispatcherIO
|
||||
private val dispatcherIO: CoroutineDispatcher,
|
||||
@DispatcherDefault
|
||||
private val dispatcherDefault: CoroutineDispatcher,
|
||||
@IODispatcher
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
@DefaultDispatcher
|
||||
private val defaultDispatcher: CoroutineDispatcher,
|
||||
workManager: WorkManager,
|
||||
) : AbstractRssRepository(
|
||||
context, accountDao, articleDao, groupDao,
|
||||
feedDao, workManager, dispatcherIO, dispatcherDefault
|
||||
feedDao, workManager, ioDispatcher, defaultDispatcher
|
||||
) {
|
||||
|
||||
override suspend fun updateArticleInfo(article: Article) {
|
||||
|
@ -70,8 +70,8 @@ class LocalRssRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result {
|
||||
return supervisorScope {
|
||||
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result =
|
||||
supervisorScope {
|
||||
val preTime = System.currentTimeMillis()
|
||||
val accountId = context.currentAccountId
|
||||
feedDao.queryAll(accountId)
|
||||
|
@ -81,31 +81,23 @@ class LocalRssRepository @Inject constructor(
|
|||
it.map { feed -> async { syncFeed(feed) } }
|
||||
.awaitAll()
|
||||
.forEach {
|
||||
if (it.isNotify) {
|
||||
notificationHelper.notify(
|
||||
FeedWithArticle(
|
||||
it.feedWithArticle.feed,
|
||||
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
|
||||
)
|
||||
)
|
||||
if (it.feed.isNotification) {
|
||||
notificationHelper.notify(it.apply {
|
||||
articles = articleDao.insertListIfNotExist(it.articles)
|
||||
})
|
||||
} else {
|
||||
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
|
||||
articleDao.insertListIfNotExist(it.articles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
||||
accountDao.queryById(accountId)?.let { account ->
|
||||
accountDao.update(
|
||||
account.apply {
|
||||
updateAt = Date()
|
||||
}
|
||||
)
|
||||
accountDao.update(account.apply { updateAt = Date() })
|
||||
}
|
||||
coroutineWorker.setProgress(setIsSyncing(false))
|
||||
ListenableWorker.Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun markAsRead(
|
||||
groupId: String?,
|
||||
|
@ -124,6 +116,7 @@ class LocalRssRepository @Inject constructor(
|
|||
before = before ?: Date(Long.MAX_VALUE)
|
||||
)
|
||||
}
|
||||
|
||||
feedId != null -> {
|
||||
articleDao.markAllAsReadByFeedId(
|
||||
accountId = accountId,
|
||||
|
@ -132,41 +125,34 @@ class LocalRssRepository @Inject constructor(
|
|||
before = before ?: Date(Long.MAX_VALUE)
|
||||
)
|
||||
}
|
||||
|
||||
articleId != null -> {
|
||||
articleDao.markAsReadByArticleId(accountId, articleId, isUnread)
|
||||
}
|
||||
|
||||
else -> {
|
||||
articleDao.markAllAsRead(accountId, isUnread, before ?: Date(Long.MAX_VALUE))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ArticleNotify(
|
||||
val feedWithArticle: FeedWithArticle,
|
||||
val isNotify: Boolean,
|
||||
)
|
||||
|
||||
private suspend fun syncFeed(feed: Feed): ArticleNotify {
|
||||
private suspend fun syncFeed(feed: Feed): FeedWithArticle {
|
||||
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
||||
val articles: List<Article>?
|
||||
try {
|
||||
articles = rssHelper.queryRssXml(feed, latest?.link)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e("RLog", "queryRssXml[${feed.name}]: ${e.message}")
|
||||
return ArticleNotify(FeedWithArticle(feed, listOf()), false)
|
||||
}
|
||||
try {
|
||||
val articles = rssHelper.queryRssXml(feed, latest?.link)
|
||||
// try {
|
||||
// if (feed.icon == null && !articles.isNullOrEmpty()) {
|
||||
// rssHelper.queryRssIcon(feedDao, feed, articles.first().link)
|
||||
// }
|
||||
} catch (e: Exception) {
|
||||
Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
|
||||
return ArticleNotify(FeedWithArticle(feed, listOf()), false)
|
||||
}
|
||||
return ArticleNotify(
|
||||
feedWithArticle = FeedWithArticle(feed, articles),
|
||||
isNotify = articles.isNotEmpty() && feed.isNotification
|
||||
// } catch (e: Exception) {
|
||||
// Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
|
||||
// return FeedWithArticle(
|
||||
// feed = feed.apply { isNotification = false },
|
||||
// articles = listOf()
|
||||
// )
|
||||
// }
|
||||
return FeedWithArticle(
|
||||
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
|
||||
articles = articles
|
||||
)
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import androidx.core.app.NotificationManagerCompat
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import me.ash.reader.MainActivity
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.entity.FeedWithArticle
|
||||
import me.ash.reader.data.model.feed.FeedWithArticle
|
||||
import me.ash.reader.ui.page.common.ExtraName
|
||||
import me.ash.reader.ui.page.common.NotificationGroupName
|
||||
import java.util.*
|
||||
|
@ -19,6 +19,7 @@ class NotificationHelper @Inject constructor(
|
|||
@ApplicationContext
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
private val notificationManager: NotificationManagerCompat =
|
||||
NotificationManagerCompat.from(context).apply {
|
||||
createNotificationChannel(
|
||||
|
@ -30,9 +31,7 @@ class NotificationHelper @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun notify(
|
||||
feedWithArticle: FeedWithArticle,
|
||||
) {
|
||||
fun notify(feedWithArticle: FeedWithArticle) {
|
||||
notificationManager.createNotificationChannelGroup(
|
||||
NotificationChannelGroup(
|
||||
feedWithArticle.feed.id,
|
||||
|
|
|
@ -10,7 +10,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import me.ash.reader.data.dao.AccountDao
|
||||
import me.ash.reader.data.dao.FeedDao
|
||||
import me.ash.reader.data.dao.GroupDao
|
||||
import me.ash.reader.data.entity.Feed
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.ext.getDefaultGroupId
|
||||
|
@ -18,6 +18,9 @@ import java.io.InputStream
|
|||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Supports import and export from OPML files.
|
||||
*/
|
||||
class OpmlRepository @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
|
@ -27,6 +30,12 @@ class OpmlRepository @Inject constructor(
|
|||
private val rssRepository: RssRepository,
|
||||
private val opmlLocalDataSource: OpmlLocalDataSource,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Imports OPML file.
|
||||
*
|
||||
* @param [inputStream] input stream of OPML file
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
suspend fun saveToDatabase(inputStream: InputStream) {
|
||||
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
|
||||
|
@ -47,6 +56,9 @@ class OpmlRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports OPML file.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
suspend fun saveToString(): String {
|
||||
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
|
||||
|
@ -85,7 +97,5 @@ class OpmlRepository @Inject constructor(
|
|||
)!!
|
||||
}
|
||||
|
||||
private fun getDefaultGroupId(): String {
|
||||
return context.currentAccountId.getDefaultGroupId()
|
||||
}
|
||||
private fun getDefaultGroupId(): String = context.currentAccountId.getDefaultGroupId()
|
||||
}
|
|
@ -8,11 +8,11 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.model.toVersion
|
||||
import me.ash.reader.data.module.DispatcherIO
|
||||
import me.ash.reader.data.module.DispatcherMain
|
||||
import me.ash.reader.data.preference.*
|
||||
import me.ash.reader.data.preference.NewVersionSizePreference.formatSize
|
||||
import me.ash.reader.data.model.general.toVersion
|
||||
import me.ash.reader.data.model.preference.*
|
||||
import me.ash.reader.data.model.preference.NewVersionSizePreference.formatSize
|
||||
import me.ash.reader.data.module.IODispatcher
|
||||
import me.ash.reader.data.module.MainDispatcher
|
||||
import me.ash.reader.data.source.Download
|
||||
import me.ash.reader.data.source.RYNetworkDataSource
|
||||
import me.ash.reader.data.source.downloadToFileWithProgress
|
||||
|
@ -26,34 +26,35 @@ class RYRepository @Inject constructor(
|
|||
@ApplicationContext
|
||||
private val context: Context,
|
||||
private val RYNetworkDataSource: RYNetworkDataSource,
|
||||
@DispatcherIO
|
||||
private val dispatcherIO: CoroutineDispatcher,
|
||||
@DispatcherMain
|
||||
private val dispatcherMain: CoroutineDispatcher,
|
||||
@IODispatcher
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
@MainDispatcher
|
||||
private val mainDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) {
|
||||
|
||||
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(ioDispatcher) {
|
||||
try {
|
||||
val response =
|
||||
RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
||||
val response = RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
||||
when {
|
||||
response.code() == 403 -> {
|
||||
withContext(dispatcherMain) {
|
||||
withContext(mainDispatcher) {
|
||||
if (showToast) context.showToast(context.getString(R.string.rate_limit))
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
response.body() == null -> {
|
||||
withContext(dispatcherMain) {
|
||||
withContext(mainDispatcher) {
|
||||
if (showToast) context.showToast(context.getString(R.string.check_failure))
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
val skipVersion = context.skipVersionNumber.toVersion()
|
||||
val currentVersion = context.getCurrentVersion()
|
||||
val latest = response.body()!!
|
||||
val latestVersion = latest.tag_name.toVersion()
|
||||
// val latestVersion = "1.0.0".toVersion()
|
||||
val skipVersion = context.skipVersionNumber.toVersion()
|
||||
val currentVersion = context.getCurrentVersion()
|
||||
val latestLog = latest.body ?: ""
|
||||
val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
|
||||
val latestSize = latest.assets?.first()?.size ?: 0
|
||||
|
@ -74,7 +75,7 @@ class RYRepository @Inject constructor(
|
|||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e("RLog", "checkUpdate: ${e.message}")
|
||||
withContext(dispatcherMain) {
|
||||
withContext(mainDispatcher) {
|
||||
if (showToast) context.showToast(context.getString(R.string.check_failure))
|
||||
}
|
||||
null
|
||||
|
@ -82,7 +83,7 @@ class RYRepository @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun downloadFile(url: String): Flow<Download> =
|
||||
withContext(dispatcherIO) {
|
||||
withContext(ioDispatcher) {
|
||||
Log.i("RLog", "downloadFile start: $url")
|
||||
try {
|
||||
return@withContext RYNetworkDataSource.downloadFile(url)
|
||||
|
@ -90,7 +91,7 @@ class RYRepository @Inject constructor(
|
|||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e("RLog", "downloadFile: ${e.message}")
|
||||
withContext(dispatcherMain) {
|
||||
withContext(mainDispatcher) {
|
||||
context.showToast(context.getString(R.string.download_failure))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,13 +10,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.ash.reader.data.dao.FeedDao
|
||||
import me.ash.reader.data.entity.Article
|
||||
import me.ash.reader.data.entity.Feed
|
||||
import me.ash.reader.data.entity.FeedWithArticle
|
||||
import me.ash.reader.data.module.DispatcherIO
|
||||
import me.ash.reader.data.model.article.Article
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
import me.ash.reader.data.model.feed.FeedWithArticle
|
||||
import me.ash.reader.data.module.IODispatcher
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.ext.spacerDollar
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import net.dankito.readability4j.extended.Readability4JExtended
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
@ -25,16 +24,20 @@ import java.io.InputStream
|
|||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Some operations on RSS.
|
||||
*/
|
||||
class RssHelper @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
@DispatcherIO
|
||||
private val dispatcherIO: CoroutineDispatcher,
|
||||
@IODispatcher
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
) {
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
||||
return withContext(dispatcherIO) {
|
||||
return withContext(ioDispatcher) {
|
||||
val accountId = context.currentAccountId
|
||||
val syndFeed = SyndFeedInput().build(XmlReader(inputStream(okHttpClient, feedLink)))
|
||||
val feed = Feed(
|
||||
|
@ -49,16 +52,9 @@ class RssHelper @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun parseDescriptionContent(link: String, content: String): String {
|
||||
val readability4J: Readability4J = Readability4JExtended(link, content)
|
||||
val article = readability4J.parse()
|
||||
val element = article.articleContent
|
||||
return element.toString()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun parseFullContent(link: String, title: String): String {
|
||||
return withContext(dispatcherIO) {
|
||||
return withContext(ioDispatcher) {
|
||||
val response = response(okHttpClient, link)
|
||||
val content = response.body.string()
|
||||
val readability4J = Readability4JExtended(link, content)
|
||||
|
@ -75,13 +71,13 @@ class RssHelper @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun queryRssXml(
|
||||
feed: Feed,
|
||||
latestLink: String? = null,
|
||||
): List<Article> {
|
||||
latestLink: String?,
|
||||
): List<Article> =
|
||||
try {
|
||||
val accountId = context.currentAccountId
|
||||
return inputStream(okHttpClient, feed.url).use {
|
||||
inputStream(okHttpClient, feed.url).use {
|
||||
SyndFeedInput().apply { isPreserveWireFeed = true }
|
||||
.build(XmlReader(it))
|
||||
.entries
|
||||
|
@ -90,12 +86,16 @@ class RssHelper @Inject constructor(
|
|||
.map { article(feed, accountId, it) }
|
||||
.toList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e("RLog", "queryRssXml[${feed.name}]: ${e.message}")
|
||||
listOf()
|
||||
}
|
||||
|
||||
private fun article(
|
||||
feed: Feed,
|
||||
accountId: Int,
|
||||
syndEntry: SyndEntry
|
||||
syndEntry: SyndEntry,
|
||||
): Article {
|
||||
val desc = syndEntry.description?.value
|
||||
val content = syndEntry.contents
|
||||
|
@ -144,7 +144,7 @@ class RssHelper @Inject constructor(
|
|||
feed: Feed,
|
||||
articleLink: String,
|
||||
) {
|
||||
withContext(dispatcherIO) {
|
||||
withContext(ioDispatcher) {
|
||||
val domainRegex = Regex("(http|https)://(www.)?(\\w+(\\.)?)+")
|
||||
val request = response(okHttpClient, articleLink)
|
||||
val content = request.body.string()
|
||||
|
@ -183,11 +183,11 @@ class RssHelper @Inject constructor(
|
|||
|
||||
private suspend fun inputStream(
|
||||
client: OkHttpClient,
|
||||
url: String
|
||||
url: String,
|
||||
): InputStream = response(client, url).body.byteStream()
|
||||
|
||||
private suspend fun response(
|
||||
client: OkHttpClient,
|
||||
url: String
|
||||
url: String,
|
||||
) = client.newCall(Request.Builder().url(url).build()).executeAsync()
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package me.ash.reader.data.repository
|
|||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import me.ash.reader.data.entity.Account
|
||||
import me.ash.reader.data.model.account.AccountType
|
||||
import me.ash.reader.ui.ext.currentAccountType
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -13,8 +13,9 @@ class RssRepository @Inject constructor(
|
|||
// private val feverRssRepository: FeverRssRepository,
|
||||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||
) {
|
||||
|
||||
fun get() = when (context.currentAccountType) {
|
||||
Account.Type.LOCAL -> localRssRepository
|
||||
AccountType.Local.id -> localRssRepository
|
||||
// Account.Type.LOCAL -> feverRssRepository
|
||||
// Account.Type.FEVER -> feverRssRepository
|
||||
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
||||
|
|
|
@ -10,6 +10,7 @@ class StringsRepository @Inject constructor(
|
|||
@ApplicationContext
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
fun getString(resId: Int, vararg formatArgs: Any) = context.getString(resId, *formatArgs)
|
||||
|
||||
fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any) =
|
||||
|
@ -18,6 +19,6 @@ class StringsRepository @Inject constructor(
|
|||
fun formatAsString(
|
||||
date: Date?,
|
||||
onlyHourMinute: Boolean? = false,
|
||||
atHourMinute: Boolean? = false
|
||||
atHourMinute: Boolean? = false,
|
||||
) = date?.formatAsString(context, onlyHourMinute, atHourMinute)
|
||||
}
|
||||
|
|
|
@ -18,17 +18,17 @@ class SyncWorker @AssistedInject constructor(
|
|||
private val rssRepository: RssRepository,
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
override suspend fun doWork(): Result =
|
||||
withContext(Dispatchers.Default) {
|
||||
Log.i("RLog", "doWork: ")
|
||||
return withContext(Dispatchers.Default) {
|
||||
rssRepository.get().sync(this@SyncWorker)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val WORK_NAME = "article.sync"
|
||||
|
||||
val UUID: UUID
|
||||
val uuid: UUID
|
||||
|
||||
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
||||
15, TimeUnit.MINUTES
|
||||
|
@ -36,7 +36,7 @@ class SyncWorker @AssistedInject constructor(
|
|||
Constraints.Builder()
|
||||
.build()
|
||||
).addTag(WORK_NAME).build().also {
|
||||
UUID = it.id
|
||||
uuid = it.id
|
||||
}
|
||||
|
||||
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
|
||||
|
|
|
@ -11,6 +11,7 @@ import retrofit2.http.Part
|
|||
import retrofit2.http.Query
|
||||
|
||||
interface FeverApiDataSource {
|
||||
|
||||
@Multipart
|
||||
@POST("fever.php/?api=&feeds=")
|
||||
fun feeds(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed>
|
||||
|
@ -23,7 +24,7 @@ interface FeverApiDataSource {
|
|||
@POST("fever.php/?api=&items=")
|
||||
fun itemsBySince(
|
||||
@Query("since_id") since: Long,
|
||||
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()
|
||||
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody(),
|
||||
): Call<FeverApiDto.Items>
|
||||
|
||||
@Multipart
|
||||
|
@ -38,10 +39,11 @@ interface FeverApiDataSource {
|
|||
@POST("fever.php/?api=&items=")
|
||||
fun itemsByIds(
|
||||
@Query("with_ids") ids: String,
|
||||
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()
|
||||
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody(),
|
||||
): Call<FeverApiDto.Items>
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: FeverApiDataSource? = null
|
||||
|
||||
fun getInstance(): FeverApiDataSource {
|
||||
|
|
|
@ -7,6 +7,7 @@ import retrofit2.http.Headers
|
|||
import retrofit2.http.POST
|
||||
|
||||
interface GoogleReaderApiDataSource {
|
||||
|
||||
@POST("accounts/ClientLogin")
|
||||
fun login(Email: String, Passwd: String): Call<String>
|
||||
|
||||
|
@ -27,6 +28,7 @@ interface GoogleReaderApiDataSource {
|
|||
fun readingList(): Call<GoogleReaderApiDto.ReadingList>
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: GoogleReaderApiDataSource? = null
|
||||
|
||||
fun getInstance(): GoogleReaderApiDataSource {
|
||||
|
|
|
@ -5,10 +5,10 @@ import be.ceau.opml.OpmlParser
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.ash.reader.data.entity.Feed
|
||||
import me.ash.reader.data.entity.Group
|
||||
import me.ash.reader.data.entity.GroupWithFeed
|
||||
import me.ash.reader.data.module.DispatcherIO
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
import me.ash.reader.data.model.group.Group
|
||||
import me.ash.reader.data.model.group.GroupWithFeed
|
||||
import me.ash.reader.data.module.IODispatcher
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.ext.spacerDollar
|
||||
import java.io.InputStream
|
||||
|
@ -18,15 +18,16 @@ import javax.inject.Inject
|
|||
class OpmlLocalDataSource @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
@DispatcherIO
|
||||
private val dispatcherIO: CoroutineDispatcher,
|
||||
@IODispatcher
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun parseFileInputStream(
|
||||
inputStream: InputStream,
|
||||
defaultGroup: Group
|
||||
defaultGroup: Group,
|
||||
): List<GroupWithFeed> {
|
||||
return withContext(dispatcherIO) {
|
||||
return withContext(ioDispatcher) {
|
||||
val accountId = context.currentAccountId
|
||||
val opml = OpmlParser().parse(inputStream)
|
||||
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {
|
||||
|
|
|
@ -8,24 +8,27 @@ import me.ash.reader.data.dao.AccountDao
|
|||
import me.ash.reader.data.dao.ArticleDao
|
||||
import me.ash.reader.data.dao.FeedDao
|
||||
import me.ash.reader.data.dao.GroupDao
|
||||
import me.ash.reader.data.entity.Account
|
||||
import me.ash.reader.data.entity.Article
|
||||
import me.ash.reader.data.entity.Feed
|
||||
import me.ash.reader.data.entity.Group
|
||||
import me.ash.reader.data.model.account.Account
|
||||
import me.ash.reader.data.model.account.AccountTypeConverters
|
||||
import me.ash.reader.data.model.article.Article
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
import me.ash.reader.data.model.group.Group
|
||||
import java.util.*
|
||||
|
||||
@Database(
|
||||
entities = [Account::class, Feed::class, Article::class, Group::class],
|
||||
version = 2,
|
||||
version = 2
|
||||
)
|
||||
@TypeConverters(RYDatabase.Converters::class)
|
||||
@TypeConverters(RYDatabase.DateConverters::class, AccountTypeConverters::class)
|
||||
abstract class RYDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun accountDao(): AccountDao
|
||||
abstract fun feedDao(): FeedDao
|
||||
abstract fun articleDao(): ArticleDao
|
||||
abstract fun groupDao(): GroupDao
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: RYDatabase? = null
|
||||
|
||||
fun getInstance(context: Context): RYDatabase {
|
||||
|
@ -41,7 +44,7 @@ abstract class RYDatabase : RoomDatabase() {
|
|||
}
|
||||
}
|
||||
|
||||
class Converters {
|
||||
class DateConverters {
|
||||
|
||||
@TypeConverter
|
||||
fun toDate(dateLong: Long?): Date? {
|
||||
|
@ -61,6 +64,7 @@ val allMigrations = arrayOf(
|
|||
|
||||
@Suppress("ClassName")
|
||||
object MIGRATION_1_2 : Migration(1, 2) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""
|
||||
|
|
|
@ -15,6 +15,7 @@ import retrofit2.http.Url
|
|||
import java.io.File
|
||||
|
||||
interface RYNetworkDataSource {
|
||||
|
||||
@GET
|
||||
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
|
||||
|
||||
|
@ -23,6 +24,7 @@ interface RYNetworkDataSource {
|
|||
suspend fun downloadFile(@Url url: String): ResponseBody
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: RYNetworkDataSource? = null
|
||||
|
||||
fun getInstance(): RYNetworkDataSource {
|
||||
|
@ -69,8 +71,10 @@ fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
|
|||
when {
|
||||
progressBytes < totalBytes ->
|
||||
throw Exception("missing bytes")
|
||||
|
||||
progressBytes > totalBytes ->
|
||||
throw Exception("too many bytes")
|
||||
|
||||
else ->
|
||||
deleteFile = false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package me.ash.reader.ui.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.ui.component.base.TextFieldDialog
|
||||
|
||||
@Composable
|
||||
fun ChangeUrlDialog(
|
||||
visible: Boolean = false,
|
||||
value: String = "",
|
||||
onValueChange: (String) -> Unit = {},
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (String) -> Unit = {},
|
||||
) {
|
||||
TextFieldDialog(
|
||||
visible = visible,
|
||||
title = stringResource(R.string.change_url),
|
||||
icon = Icons.Outlined.Edit,
|
||||
value = value,
|
||||
placeholder = stringResource(R.string.feed_url_placeholder),
|
||||
onValueChange = onValueChange,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = onConfirm,
|
||||
)
|
||||
}
|
|
@ -14,7 +14,7 @@ import androidx.compose.ui.unit.dp
|
|||
@Composable
|
||||
fun FeedIcon(
|
||||
feedName: String,
|
||||
size: Dp = 20.dp
|
||||
size: Dp = 20.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -12,10 +12,9 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import me.ash.reader.data.model.Filter
|
||||
import me.ash.reader.data.model.getName
|
||||
import me.ash.reader.data.preference.FlowFilterBarStylePreference
|
||||
import me.ash.reader.data.preference.LocalThemeIndex
|
||||
import me.ash.reader.data.model.general.Filter
|
||||
import me.ash.reader.data.model.preference.FlowFilterBarStylePreference
|
||||
import me.ash.reader.data.model.preference.LocalThemeIndex
|
||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||
import me.ash.reader.ui.theme.palette.onDark
|
||||
|
||||
|
@ -54,7 +53,7 @@ fun FilterBar(
|
|||
} else {
|
||||
item.iconOutline
|
||||
},
|
||||
contentDescription = item.getName()
|
||||
contentDescription = item.toName()
|
||||
)
|
||||
},
|
||||
label = if (filterBarStyle == FlowFilterBarStylePreference.Icon.value) {
|
||||
|
@ -62,7 +61,7 @@ fun FilterBar(
|
|||
} else {
|
||||
{
|
||||
Text(
|
||||
text = item.getName(),
|
||||
text = item.toName(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
28
app/src/main/java/me/ash/reader/ui/component/RenameDialog.kt
Normal file
28
app/src/main/java/me/ash/reader/ui/component/RenameDialog.kt
Normal file
|
@ -0,0 +1,28 @@
|
|||
package me.ash.reader.ui.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.ui.component.base.TextFieldDialog
|
||||
|
||||
@Composable
|
||||
fun RenameDialog(
|
||||
visible: Boolean = false,
|
||||
value: String = "",
|
||||
onValueChange: (String) -> Unit = {},
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (String) -> Unit = {},
|
||||
) {
|
||||
TextFieldDialog(
|
||||
visible = visible,
|
||||
title = stringResource(R.string.rename),
|
||||
icon = Icons.Outlined.Edit,
|
||||
value = value,
|
||||
placeholder = stringResource(R.string.name),
|
||||
onValueChange = onValueChange,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = onConfirm
|
||||
)
|
||||
}
|
|
@ -29,7 +29,7 @@ fun AnimatedPopup(
|
|||
anchorBounds: IntRect,
|
||||
windowSize: IntSize,
|
||||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize
|
||||
popupContentSize: IntSize,
|
||||
): IntOffset {
|
||||
return IntOffset(
|
||||
x = with(density) { (absoluteX).roundToPx() },
|
||||
|
|
|
@ -36,7 +36,7 @@ fun AnimatedText(
|
|||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = text,
|
||||
|
|
|
@ -76,7 +76,7 @@ fun ClipboardTextField(
|
|||
private fun action(
|
||||
focusManager: FocusManager?,
|
||||
onConfirm: (String) -> Unit,
|
||||
value: String
|
||||
value: String,
|
||||
): KeyboardActionScope.() -> Unit = {
|
||||
focusManager?.clearFocus()
|
||||
onConfirm(value)
|
||||
|
|
|
@ -22,13 +22,14 @@ class CurlyCornerShape(
|
|||
bottomEnd = ZeroCornerSize,
|
||||
bottomStart = ZeroCornerSize
|
||||
) {
|
||||
|
||||
private fun sineCircleXYatAngle(
|
||||
d1: Double,
|
||||
d2: Double,
|
||||
d3: Double,
|
||||
d4: Double,
|
||||
d5: Double,
|
||||
i: Int
|
||||
i: Int,
|
||||
): List<Double> = (i.toDouble() * d5).run {
|
||||
listOf(
|
||||
(sin(this) * d4 + d3) * cos(d5) + d1,
|
||||
|
@ -42,7 +43,7 @@ class CurlyCornerShape(
|
|||
topEnd: Float,
|
||||
bottomEnd: Float,
|
||||
bottomStart: Float,
|
||||
layoutDirection: LayoutDirection
|
||||
layoutDirection: LayoutDirection,
|
||||
): Outline {
|
||||
val d = 2.0
|
||||
val r2: Double = size.width / d
|
||||
|
@ -73,7 +74,7 @@ class CurlyCornerShape(
|
|||
topStart: CornerSize,
|
||||
topEnd: CornerSize,
|
||||
bottomEnd: CornerSize,
|
||||
bottomStart: CornerSize
|
||||
bottomStart: CornerSize,
|
||||
) = RoundedCornerShape(
|
||||
topStart = topStart,
|
||||
topEnd = topEnd,
|
||||
|
|
|
@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import com.caverock.androidsvg.SVG
|
||||
import me.ash.reader.data.preference.LocalDarkTheme
|
||||
import me.ash.reader.data.model.preference.LocalDarkTheme
|
||||
import me.ash.reader.ui.svg.parseDynamicColor
|
||||
import me.ash.reader.ui.theme.palette.LocalTonalPalettes
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
|
|||
@Composable
|
||||
fun RYExtensibleVisibility(
|
||||
visible: Boolean,
|
||||
content: @Composable AnimatedVisibilityScope.() -> Unit
|
||||
content: @Composable AnimatedVisibilityScope.() -> Unit,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
|
|
|
@ -35,7 +35,7 @@ fun RYSwitch(
|
|||
modifier: Modifier = Modifier,
|
||||
activated: Boolean,
|
||||
enable: Boolean = true,
|
||||
onClick: (() -> Unit)? = null
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val tonalPalettes = LocalTonalPalettes.current
|
||||
|
||||
|
@ -74,7 +74,7 @@ fun SwitchHeadline(
|
|||
activated: Boolean,
|
||||
onClick: () -> Unit,
|
||||
title: String,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val tonalPalettes = LocalTonalPalettes.current
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const val INJECTION_TOKEN = "/android_asset_font/"
|
|||
fun WebView(
|
||||
modifier: Modifier = Modifier,
|
||||
content: String,
|
||||
onReceivedError: (error: WebResourceError?) -> Unit = {}
|
||||
onReceivedError: (error: WebResourceError?) -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
|
||||
|
@ -28,7 +28,7 @@ fun WebView(
|
|||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
url: String?
|
||||
url: String?,
|
||||
): WebResourceResponse? {
|
||||
if (url != null && url.contains(INJECTION_TOKEN)) {
|
||||
try {
|
||||
|
@ -63,7 +63,7 @@ fun WebView(
|
|||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
if (null == request?.url) return false
|
||||
val url = request.url.toString()
|
||||
|
@ -79,7 +79,7 @@ fun WebView(
|
|||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
error: WebResourceError?
|
||||
error: WebResourceError?,
|
||||
) {
|
||||
super.onReceivedError(view, request, error)
|
||||
onReceivedError(error)
|
||||
|
@ -88,7 +88,7 @@ fun WebView(
|
|||
override fun onReceivedSslError(
|
||||
view: WebView?,
|
||||
handler: SslErrorHandler?,
|
||||
error: SslError?
|
||||
error: SslError?,
|
||||
) {
|
||||
handler?.cancel()
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.compose.ui.text.AnnotatedString
|
|||
import androidx.compose.ui.text.SpanStyle
|
||||
|
||||
class AnnotatedParagraphStringBuilder {
|
||||
|
||||
// Private for a reason
|
||||
private val builder: AnnotatedString.Builder = AnnotatedString.Builder()
|
||||
|
||||
|
@ -60,7 +61,7 @@ class AnnotatedParagraphStringBuilder {
|
|||
builder.pushStringAnnotation(tag = tag, annotation = annotation)
|
||||
|
||||
fun pushComposableStyle(
|
||||
style: @Composable () -> SpanStyle
|
||||
style: @Composable () -> SpanStyle,
|
||||
): Int {
|
||||
composableStyles.add(
|
||||
ComposableStyleWithStartEnd(
|
||||
|
@ -72,7 +73,7 @@ class AnnotatedParagraphStringBuilder {
|
|||
}
|
||||
|
||||
fun popComposableStyle(
|
||||
index: Int
|
||||
index: Int,
|
||||
) {
|
||||
poppedComposableStyles.add(
|
||||
composableStyles.removeAt(index).copy(end = builder.length)
|
||||
|
@ -122,20 +123,25 @@ fun AnnotatedParagraphStringBuilder.ensureDoubleNewline() {
|
|||
lastTwoChars.isEmpty() -> {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
length == 1 && lastTwoChars.peekLatest()?.isWhitespace() == true -> {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
length == 2 &&
|
||||
lastTwoChars.peekLatest()?.isWhitespace() == true &&
|
||||
lastTwoChars.peekSecondLatest()?.isWhitespace() == true -> {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
lastTwoChars.peekLatest() == '\n' && lastTwoChars.peekSecondLatest() == '\n' -> {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
lastTwoChars.peekLatest() == '\n' -> {
|
||||
append('\n')
|
||||
}
|
||||
|
||||
else -> {
|
||||
append("\n\n")
|
||||
}
|
||||
|
@ -147,12 +153,15 @@ private fun AnnotatedParagraphStringBuilder.ensureSingleNewline() {
|
|||
lastTwoChars.isEmpty() -> {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
length == 1 && lastTwoChars.peekLatest()?.isWhitespace() == true -> {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
lastTwoChars.peekLatest() == '\n' -> {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
else -> {
|
||||
append('\n')
|
||||
}
|
||||
|
@ -187,5 +196,5 @@ private fun <T> List<T>.peekSecondLatest(): T? {
|
|||
data class ComposableStyleWithStartEnd(
|
||||
val style: @Composable () -> SpanStyle,
|
||||
val start: Int,
|
||||
val end: Int = -1
|
||||
val end: Int = -1,
|
||||
)
|
||||
|
|
|
@ -206,6 +206,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Element -> {
|
||||
val element = node
|
||||
when (element.tagName()) {
|
||||
|
@ -232,6 +233,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"br" -> append('\n')
|
||||
"h1" -> {
|
||||
withParagraph {
|
||||
|
@ -242,6 +244,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"h2" -> {
|
||||
withParagraph {
|
||||
withComposableStyle(
|
||||
|
@ -251,6 +254,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"h3" -> {
|
||||
withParagraph {
|
||||
withComposableStyle(
|
||||
|
@ -260,6 +264,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"h4" -> {
|
||||
withParagraph {
|
||||
withComposableStyle(
|
||||
|
@ -269,6 +274,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"h5" -> {
|
||||
withParagraph {
|
||||
withComposableStyle(
|
||||
|
@ -278,6 +284,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"h6" -> {
|
||||
withParagraph {
|
||||
withComposableStyle(
|
||||
|
@ -287,6 +294,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"strong", "b" -> {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
appendTextChildren(
|
||||
|
@ -298,6 +306,7 @@ private fun TextComposer.appendTextChildren(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
"i", "em", "cite", "dfn" -> {
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
|
||||
appendTextChildren(
|
||||
|
@ -309,6 +318,7 @@ private fun TextComposer.appendTextChildren(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
"tt" -> {
|
||||
withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) {
|
||||
appendTextChildren(
|
||||
|
@ -320,6 +330,7 @@ private fun TextComposer.appendTextChildren(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
"u" -> {
|
||||
withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) {
|
||||
appendTextChildren(
|
||||
|
@ -331,6 +342,7 @@ private fun TextComposer.appendTextChildren(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
"sup" -> {
|
||||
withStyle(SpanStyle(baselineShift = BaselineShift.Superscript)) {
|
||||
appendTextChildren(
|
||||
|
@ -342,6 +354,7 @@ private fun TextComposer.appendTextChildren(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
"sub" -> {
|
||||
withStyle(SpanStyle(baselineShift = BaselineShift.Subscript)) {
|
||||
appendTextChildren(
|
||||
|
@ -353,6 +366,7 @@ private fun TextComposer.appendTextChildren(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
"font" -> {
|
||||
val fontFamily: FontFamily? = element.attr("face")?.asFontFamily()
|
||||
withStyle(SpanStyle(fontFamily = fontFamily)) {
|
||||
|
@ -365,6 +379,7 @@ private fun TextComposer.appendTextChildren(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
"pre" -> {
|
||||
appendTextChildren(
|
||||
element.childNodes(),
|
||||
|
@ -375,6 +390,7 @@ private fun TextComposer.appendTextChildren(
|
|||
baseUrl = baseUrl,
|
||||
)
|
||||
}
|
||||
|
||||
"code" -> {
|
||||
if (element.parent()?.tagName() == "pre") {
|
||||
terminateCurrentText()
|
||||
|
@ -400,6 +416,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"blockquote" -> {
|
||||
withParagraph {
|
||||
withComposableStyle(
|
||||
|
@ -415,6 +432,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"a" -> {
|
||||
withComposableStyle(
|
||||
style = { linkTextStyle().toSpanStyle() }
|
||||
|
@ -430,6 +448,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"img" -> {
|
||||
val imageCandidates = getImageSource(baseUrl, element)
|
||||
if (imageCandidates.hasImage) {
|
||||
|
@ -502,6 +521,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"ul" -> {
|
||||
element.children()
|
||||
.filter { it.tagName() == "li" }
|
||||
|
@ -519,6 +539,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"ol" -> {
|
||||
element.children()
|
||||
.filter { it.tagName() == "li" }
|
||||
|
@ -536,6 +557,7 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"table" -> {
|
||||
appendTable {
|
||||
/*
|
||||
|
@ -581,6 +603,7 @@ private fun TextComposer.appendTextChildren(
|
|||
append("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
"iframe" -> {
|
||||
val video: Video? = getVideo(element.attr("abs:src"))
|
||||
|
||||
|
@ -629,9 +652,11 @@ private fun TextComposer.appendTextChildren(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
"video" -> {
|
||||
// not implemented yet. remember to disable selection
|
||||
}
|
||||
|
||||
else -> {
|
||||
appendTextChildren(
|
||||
nodes = element.childNodes(),
|
||||
|
@ -707,8 +732,9 @@ internal fun getImageSource(baseUrl: String, element: Element) = ImageCandidates
|
|||
internal class ImageCandidates(
|
||||
val baseUrl: String,
|
||||
val srcSet: String,
|
||||
val absSrc: String
|
||||
val absSrc: String,
|
||||
) {
|
||||
|
||||
val hasImage: Boolean = srcSet.isNotBlank() || absSrc.isNotBlank()
|
||||
|
||||
/**
|
||||
|
@ -728,9 +754,11 @@ internal class ImageCandidates(
|
|||
descriptor.endsWith("w", ignoreCase = true) -> {
|
||||
descriptor.substringBefore("w").toFloat() / maxSize.width.pxOrElse { 1 }
|
||||
}
|
||||
|
||||
descriptor.endsWith("x", ignoreCase = true) -> {
|
||||
descriptor.substringBefore("x").toFloat() / pixelDensity
|
||||
}
|
||||
|
||||
else -> {
|
||||
return@fold acc
|
||||
}
|
||||
|
|
|
@ -24,8 +24,9 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.text.SpanStyle
|
||||
|
||||
class TextComposer(
|
||||
val paragraphEmitter: (AnnotatedParagraphStringBuilder) -> Unit
|
||||
val paragraphEmitter: (AnnotatedParagraphStringBuilder) -> Unit,
|
||||
) {
|
||||
|
||||
val spanStack: MutableList<Span> = mutableListOf()
|
||||
|
||||
// The identity of this will change - do not reference it in blocks
|
||||
|
@ -48,6 +49,7 @@ class TextComposer(
|
|||
tag = span.tag,
|
||||
annotation = span.annotation
|
||||
)
|
||||
|
||||
is SpanWithComposableStyle -> builder.pushComposableStyle(span.spanStyle)
|
||||
}
|
||||
}
|
||||
|
@ -75,8 +77,8 @@ class TextComposer(
|
|||
link: String? = null,
|
||||
onLinkClick: (String) -> Unit,
|
||||
block: (
|
||||
onClick: (() -> Unit)?
|
||||
) -> R
|
||||
onClick: (() -> Unit)?,
|
||||
) -> R,
|
||||
): R {
|
||||
val url = link ?: findClosestLink()
|
||||
//builder.ensureDoubleNewline()
|
||||
|
@ -117,7 +119,7 @@ class TextComposer(
|
|||
}
|
||||
|
||||
inline fun <R : Any> TextComposer.withParagraph(
|
||||
crossinline block: TextComposer.() -> R
|
||||
crossinline block: TextComposer.() -> R,
|
||||
): R {
|
||||
ensureDoubleNewline()
|
||||
return block(this)
|
||||
|
@ -125,7 +127,7 @@ inline fun <R : Any> TextComposer.withParagraph(
|
|||
|
||||
inline fun <R : Any> TextComposer.withStyle(
|
||||
style: SpanStyle,
|
||||
crossinline block: TextComposer.() -> R
|
||||
crossinline block: TextComposer.() -> R,
|
||||
): R {
|
||||
spanStack.add(SpanWithStyle(style))
|
||||
val index = pushStyle(style)
|
||||
|
@ -139,7 +141,7 @@ inline fun <R : Any> TextComposer.withStyle(
|
|||
|
||||
inline fun <R : Any> TextComposer.withComposableStyle(
|
||||
noinline style: @Composable () -> SpanStyle,
|
||||
crossinline block: TextComposer.() -> R
|
||||
crossinline block: TextComposer.() -> R,
|
||||
): R {
|
||||
spanStack.add(SpanWithComposableStyle(style))
|
||||
val index = pushComposableStyle(style)
|
||||
|
@ -154,7 +156,7 @@ inline fun <R : Any> TextComposer.withComposableStyle(
|
|||
inline fun <R : Any> TextComposer.withAnnotation(
|
||||
tag: String,
|
||||
annotation: String,
|
||||
crossinline block: TextComposer.() -> R
|
||||
crossinline block: TextComposer.() -> R,
|
||||
): R {
|
||||
spanStack.add(SpanWithAnnotation(tag = tag, annotation = annotation))
|
||||
val index = pushStringAnnotation(tag = tag, annotation = annotation)
|
||||
|
@ -169,14 +171,14 @@ inline fun <R : Any> TextComposer.withAnnotation(
|
|||
sealed class Span
|
||||
|
||||
data class SpanWithStyle(
|
||||
val spanStyle: SpanStyle
|
||||
val spanStyle: SpanStyle,
|
||||
) : Span()
|
||||
|
||||
data class SpanWithAnnotation(
|
||||
val tag: String,
|
||||
val annotation: String
|
||||
val annotation: String,
|
||||
) : Span()
|
||||
|
||||
data class SpanWithComposableStyle(
|
||||
val spanStyle: @Composable () -> SpanStyle
|
||||
val spanStyle: @Composable () -> SpanStyle,
|
||||
) : Span()
|
||||
|
|
|
@ -44,8 +44,9 @@ data class Video(
|
|||
val src: String,
|
||||
val imageUrl: String,
|
||||
// Youtube needs a different link than embed links
|
||||
val link: String
|
||||
val link: String,
|
||||
) {
|
||||
|
||||
val width: Int
|
||||
get() = 480
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ import android.util.Log
|
|||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.model.Version
|
||||
import me.ash.reader.data.model.toVersion
|
||||
import me.ash.reader.data.model.general.Version
|
||||
import me.ash.reader.data.model.general.toVersion
|
||||
import java.io.File
|
||||
|
||||
fun Context.findActivity(): Activity? = when (this) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user