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