Format the code (#108)

* Format the code

* Add comments
This commit is contained in:
Ashinch 2022-06-16 15:36:05 +08:00 committed by GitHub
parent f19d044181
commit 5b22b46912
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
168 changed files with 1308 additions and 1012 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
package me.ash.reader.data.model
data class ImportantCount(
val important: Int,
val feedId: String,
val groupId: String,
)

View File

@ -1,2 +0,0 @@
package me.ash.reader.data.model

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 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,
)
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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