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 kotlin.system.exitProcess
/**
* The uncaught exception handler for the application.
*/
class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
/**
* Catch all uncaught exception and log it.
*/
override fun uncaughtException(p0: Thread, p1: Throwable) {
Looper.myLooper() ?: Looper.prepare()
context.showToastLong(p1.message)

View File

@ -10,14 +10,18 @@ import androidx.profileinstaller.ProfileInstallerInitializer
import coil.ImageLoader
import coil.compose.LocalImageLoader
import dagger.hilt.android.AndroidEntryPoint
import me.ash.reader.data.preference.LanguagesPreference
import me.ash.reader.data.preference.SettingsProvider
import me.ash.reader.data.model.preference.LanguagesPreference
import me.ash.reader.data.model.preference.SettingsProvider
import me.ash.reader.ui.ext.languages
import me.ash.reader.ui.page.common.HomeEntry
import javax.inject.Inject
/**
* The Single-Activity Architecture.
*/
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var imageLoader: ImageLoader

View File

@ -9,8 +9,9 @@ import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.IODispatcher
import me.ash.reader.data.repository.*
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.RYDatabase
@ -21,16 +22,23 @@ import org.conscrypt.Conscrypt
import java.security.Security
import javax.inject.Inject
/**
* The Application class, where the Dagger components is generated.
*/
@HiltAndroidApp
class RYApp : Application(), Configuration.Provider {
/**
* From: [Feeder](https://gitlab.com/spacecowboy/Feeder).
*
* Install Conscrypt to handle TLSv1.3 pre Android10.
*/
init {
// From: https://gitlab.com/spacecowboy/Feeder
// Install Conscrypt to handle TLSv1.3 pre Android10
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
@Inject
lateinit var RYDatabase: RYDatabase
lateinit var ryDatabase: RYDatabase
@Inject
lateinit var workerFactory: HiltWorkerFactory
@ -39,7 +47,7 @@ class RYApp : Application(), Configuration.Provider {
lateinit var workManager: WorkManager
@Inject
lateinit var RYNetworkDataSource: RYNetworkDataSource
lateinit var ryNetworkDataSource: RYNetworkDataSource
@Inject
lateinit var opmlLocalDataSource: OpmlLocalDataSource
@ -73,8 +81,8 @@ class RYApp : Application(), Configuration.Provider {
lateinit var applicationScope: CoroutineScope
@Inject
@DispatcherDefault
lateinit var dispatcherDefault: CoroutineDispatcher
@IODispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var okHttpClient: OkHttpClient
@ -82,27 +90,40 @@ class RYApp : Application(), Configuration.Provider {
@Inject
lateinit var imageLoader: ImageLoader
/**
* When the application startup.
*
* 1. Set the uncaught exception handler
* 2. Initialize the default account if there is none
* 3. Synchronize once
* 4. Check for new version
*/
override fun onCreate() {
super.onCreate()
CrashHandler(this)
dataStoreInit()
applicationScope.launch(dispatcherDefault) {
applicationScope.launch {
accountInit()
workerInit()
if (notFdroid) {
checkUpdate()
}
if (notFdroid) checkUpdate()
}
}
private fun dataStoreInit() {
}
/**
* Override the [Configuration.Builder] to provide the [HiltWorkerFactory].
*/
override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
private suspend fun accountInit() {
if (accountRepository.isNoAccount()) {
val account = accountRepository.addDefaultAccount()
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!)
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountType, account.type)
withContext(ioDispatcher) {
if (accountRepository.isNoAccount()) {
val account = accountRepository.addDefaultAccount()
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() {
applicationContext.getLatestApk().let {
if (it.exists()) {
it.del()
withContext(ioDispatcher) {
applicationContext.getLatestApk().let {
if (it.exists()) it.del()
}
}
ryRepository.checkUpdate(showToast = false)
}
override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
}

View File

@ -1,6 +1,12 @@
package me.ash.reader.data.constant
/**
* The tonal elevation tokens.
*
* @see androidx.compose.material3.tokens.ElevationTokens
*/
object ElevationTokens {
const val Level0 = 0
const val Level1 = 1
const val Level2 = 3

View File

@ -1,10 +1,11 @@
package me.ash.reader.data.dao
import androidx.room.*
import me.ash.reader.data.entity.Account
import me.ash.reader.data.model.account.Account
@Dao
interface AccountDao {
@Query(
"""
SELECT * FROM account

View File

@ -3,13 +3,14 @@ package me.ash.reader.data.dao
import androidx.paging.PagingSource
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.model.ImportantCount
import me.ash.reader.data.model.article.Article
import me.ash.reader.data.model.article.ArticleWithFeed
import me.ash.reader.data.model.feed.ImportantNum
import java.util.*
@Dao
interface ArticleDao {
@Transaction
@Query(
"""
@ -298,8 +299,8 @@ interface ArticleDao {
)
fun queryImportantCountWhenIsUnread(
accountId: Int,
isUnread: Boolean
): Flow<List<ImportantCount>>
isUnread: Boolean,
): Flow<List<ImportantNum>>
@Transaction
@Query(
@ -315,8 +316,8 @@ interface ArticleDao {
)
fun queryImportantCountWhenIsStarred(
accountId: Int,
isStarred: Boolean
): Flow<List<ImportantCount>>
isStarred: Boolean,
): Flow<List<ImportantNum>>
@Transaction
@Query(
@ -329,7 +330,7 @@ interface ArticleDao {
GROUP BY a.feedId
"""
)
fun queryImportantCountWhenIsAll(accountId: Int): Flow<List<ImportantCount>>
fun queryImportantCountWhenIsAll(accountId: Int): Flow<List<ImportantNum>>
@Transaction
@Query(
@ -352,7 +353,7 @@ interface ArticleDao {
)
fun queryArticleWithFeedWhenIsStarred(
accountId: Int,
isStarred: Boolean
isStarred: Boolean,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@ -366,7 +367,7 @@ interface ArticleDao {
)
fun queryArticleWithFeedWhenIsUnread(
accountId: Int,
isUnread: Boolean
isUnread: Boolean,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@ -444,7 +445,7 @@ interface ArticleDao {
)
fun queryArticleWithFeedByFeedIdWhenIsAll(
accountId: Int,
feedId: String
feedId: String,
): PagingSource<Int, ArticleWithFeed>
@Transaction

View File

@ -1,10 +1,11 @@
package me.ash.reader.data.dao
import androidx.room.*
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.model.feed.Feed
@Dao
interface FeedDao {
@Query(
"""
UPDATE feed SET groupId = :targetGroupId
@ -15,7 +16,7 @@ interface FeedDao {
suspend fun updateTargetGroupIdByGroupId(
accountId: Int,
groupId: String,
targetGroupId: String
targetGroupId: String,
)
@Query(
@ -28,7 +29,7 @@ interface FeedDao {
suspend fun updateIsFullContentByGroupId(
accountId: Int,
groupId: String,
isFullContent: Boolean
isFullContent: Boolean,
)
@Query(
@ -41,7 +42,7 @@ interface FeedDao {
suspend fun updateIsNotificationByGroupId(
accountId: Int,
groupId: String,
isNotification: Boolean
isNotification: Boolean,
)
@Query(

View File

@ -2,11 +2,12 @@ package me.ash.reader.data.dao
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import me.ash.reader.data.entity.Group
import me.ash.reader.data.entity.GroupWithFeed
import me.ash.reader.data.model.group.Group
import me.ash.reader.data.model.group.GroupWithFeed
@Dao
interface GroupDao {
@Query(
"""
SELECT * FROM `group`

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.Entity
import androidx.room.PrimaryKey
import java.util.*
/**
* In the application, at least one account exists and different accounts
* can have the same feeds and articles.
*/
@Entity(tableName = "account")
data class Account(
@PrimaryKey(autoGenerate = true)
@ -12,13 +16,7 @@ data class Account(
@ColumnInfo
var name: String,
@ColumnInfo
var type: Int,
var type: AccountType,
@ColumnInfo
var updateAt: Date? = null,
) {
object Type {
const val LOCAL = 1
const val FEVER = 2
const val GOOGLE_READER = 3
}
}
)

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 me.ash.reader.data.model.feed.Feed
import java.util.*
/**
* TODO: Add class description
*/
@Entity(
tableName = "article",
foreignKeys = [ForeignKey(
@ -43,6 +47,7 @@ data class Article(
@ColumnInfo(defaultValue = "false")
var isReadLater: Boolean = false,
) {
@Ignore
var dateString: String? = null
}

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.Relation
import me.ash.reader.data.model.feed.Feed
/**
* An [article] contains a [feed].
*/
data class ArticleWithFeed(
@Embedded
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 me.ash.reader.data.model.group.Group
/**
* TODO: Add class description
*/
@Entity(
tableName = "feed",
foreignKeys = [ForeignKey(
@ -30,6 +34,7 @@ data class Feed(
@ColumnInfo(defaultValue = "false")
var isFullContent: Boolean = false,
) {
@Ignore
var important: Int? = 0

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.Relation
import me.ash.reader.data.model.article.Article
/**
* A [feed] contains many [articles].
*/
data class FeedWithArticle(
@Embedded
var feed: Feed,
@Relation(parentColumn = "id", entityColumn = "feedId")
var articles: List<Article>
var articles: List<Article>,
)

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.Relation
import me.ash.reader.data.model.group.Group
/**
* A [feed] contains a [group].
*/
data class FeedWithGroup(
@Embedded
var feed: Feed,
@Relation(parentColumn = "groupId", entityColumn = "id")
var group: Group
var group: Group,
)

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.outlined.FiberManualRecord
@ -13,17 +13,46 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
import me.ash.reader.data.model.general.Filter.Companion.All
import me.ash.reader.data.model.general.Filter.Companion.Starred
import me.ash.reader.data.model.general.Filter.Companion.Unread
class Filter(
/**
* Indicates filter conditions.
*
* - [All]: all items
* - [Unread]: unread items
* - [Starred]: starred items
*/
class Filter private constructor(
val index: Int,
val iconOutline: ImageVector,
val iconFilled: ImageVector,
) {
fun isStarred(): Boolean = this == Starred
fun isUnread(): Boolean = this == Unread
fun isAll(): Boolean = this == All
@Stable
@Composable
fun toName(): String = when (this) {
Unread -> stringResource(R.string.unread)
Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all)
}
@OptIn(ExperimentalComposeUiApi::class)
@Stable
@Composable
fun toDesc(important: Int): String = when (this) {
Starred -> pluralStringResource(R.plurals.starred_desc, important, important)
Unread -> pluralStringResource(R.plurals.unread_desc, important, important)
else -> pluralStringResource(R.plurals.all_desc, important, important)
}
companion object {
val Starred = Filter(
index = 0,
iconOutline = Icons.Rounded.StarOutline,
@ -42,20 +71,3 @@ class Filter(
val values = listOf(Starred, Unread, All)
}
}
@Stable
@Composable
fun Filter.getName(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread)
Filter.Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all)
}
@OptIn(ExperimentalComposeUiApi::class)
@Stable
@Composable
fun Filter.getDesc(important: Int): String = when (this) {
Filter.Starred -> pluralStringResource(R.plurals.starred_desc, important, important)
Filter.Unread -> pluralStringResource(R.plurals.unread_desc, important, important)
else -> pluralStringResource(R.plurals.all_desc, important, important)
}

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.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
/**
* TODO: Add class description
*/
@Entity(tableName = "group")
data class Group(
@PrimaryKey
@ -14,6 +17,7 @@ data class Group(
@ColumnInfo(index = true)
var accountId: Int,
) {
@Ignore
var important: Int? = 0
}

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.Relation
import me.ash.reader.data.model.feed.Feed
/**
* A [group] contains many [feeds].
*/
data class GroupWithFeed(
@Embedded
var group: Group,
@Relation(parentColumn = "id", entityColumn = "groupId")
var feeds: MutableList<Feed>
var feeds: MutableList<Feed>,
)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class AmoledDarkThemePreference(val value: Boolean) : Preference() {
}
companion object {
val default = OFF
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -9,6 +9,7 @@ import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object CustomPrimaryColorPreference {
const val default = ""
fun put(context: Context, scope: CoroutineScope, value: String) {

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
@ -26,7 +26,7 @@ sealed class DarkThemePreference(val value: Int) : Preference() {
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
UseDeviceTheme -> context.getString(R.string.use_device_theme)
ON -> context.getString(R.string.on)
@ -42,6 +42,7 @@ sealed class DarkThemePreference(val value: Int) : Preference() {
}
companion object {
val default = UseDeviceTheme
val values = listOf(UseDeviceTheme, ON, OFF)
@ -63,6 +64,7 @@ operator fun DarkThemePreference.not(): DarkThemePreference =
} else {
DarkThemePreference.ON
}
DarkThemePreference.ON -> DarkThemePreference.OFF
DarkThemePreference.OFF -> DarkThemePreference.ON
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FeedsFilterBarFilledPreference(val value: Boolean) : Preference() {
}
companion object {
val default = OFF
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -9,6 +9,7 @@ import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object FeedsFilterBarPaddingPreference {
const val default = 60
fun put(context: Context, scope: CoroutineScope, value: Int) {

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -23,7 +23,7 @@ sealed class FeedsFilterBarStylePreference(val value: Int) : Preference() {
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Icon -> context.getString(R.string.icons)
IconLabel -> context.getString(R.string.icons_and_labels)
@ -31,6 +31,7 @@ sealed class FeedsFilterBarStylePreference(val value: Int) : Preference() {
}
companion object {
val default = Icon
val values = listOf(Icon, IconLabel, IconLabelOnlySelected)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -26,7 +26,7 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
@ -37,6 +37,7 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
}
companion object {
val default = Level0
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FeedsGroupListExpandPreference(val value: Boolean) : Preference() {
}
companion object {
val default = ON
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -26,7 +26,7 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
@ -37,6 +37,7 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
}
companion object {
val default = Level0
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -26,7 +26,7 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
@ -37,6 +37,7 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
}
companion object {
val default = Level0
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FlowArticleListDateStickyHeaderPreference(val value: Boolean) : Pre
}
companion object {
val default = ON
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FlowArticleListDescPreference(val value: Boolean) : Preference() {
}
companion object {
val default = ON
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FlowArticleListFeedIconPreference(val value: Boolean) : Preference(
}
companion object {
val default = ON
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FlowArticleListFeedNamePreference(val value: Boolean) : Preference(
}
companion object {
val default = ON
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FlowArticleListImagePreference(val value: Boolean) : Preference() {
}
companion object {
val default = ON
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FlowArticleListTimePreference(val value: Boolean) : Preference() {
}
companion object {
val default = ON
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -26,7 +26,7 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
@ -37,6 +37,7 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
}
companion object {
val default = Level0
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,6 +22,7 @@ sealed class FlowFilterBarFilledPreference(val value: Boolean) : Preference() {
}
companion object {
val default = OFF
val values = listOf(ON, OFF)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -9,6 +9,7 @@ import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object FlowFilterBarPaddingPreference {
const val default = 60
fun put(context: Context, scope: CoroutineScope, value: Int) {

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -23,7 +23,7 @@ sealed class FlowFilterBarStylePreference(val value: Int) : Preference() {
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Icon -> context.getString(R.string.icons)
IconLabel -> context.getString(R.string.icons_and_labels)
@ -31,6 +31,7 @@ sealed class FlowFilterBarStylePreference(val value: Int) : Preference() {
}
companion object {
val default = Icon
val values = listOf(Icon, IconLabel, IconLabelOnlySelected)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -26,7 +26,7 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
@ -37,6 +37,7 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
}
companion object {
val default = Level0
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -26,7 +26,7 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
@ -37,6 +37,7 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
}
companion object {
val default = Level0
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -23,7 +23,7 @@ sealed class InitialFilterPreference(val value: Int) : Preference() {
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
Starred -> context.getString(R.string.starred)
Unread -> context.getString(R.string.unread)
@ -31,6 +31,7 @@ sealed class InitialFilterPreference(val value: Int) : Preference() {
}
companion object {
val default = All
val values = listOf(Starred, Unread, All)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -22,13 +22,14 @@ sealed class InitialPagePreference(val value: Int) : Preference() {
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
FeedsPage -> context.getString(R.string.feeds_page)
FlowPage -> context.getString(R.string.flow_page)
}
companion object {
val default = FeedsPage
val values = listOf(FeedsPage, FlowPage)

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.os.LocaleList
import android.util.Log
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -19,6 +18,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
object German : LanguagesPreference(3)
object French : LanguagesPreference(4)
object Czech : LanguagesPreference(5)
object Italian : LanguagesPreference(6)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
@ -30,7 +30,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
}
}
fun getDesc(context: Context): String =
fun toDesc(context: Context): String =
when (this) {
UseDeviceLanguages -> context.getString(R.string.use_device_languages)
English -> context.getString(R.string.english)
@ -38,6 +38,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
German -> context.getString(R.string.german)
French -> context.getString(R.string.french)
Czech -> context.getString(R.string.czech)
Italian -> context.getString(R.string.italian)
}
fun getLocale(): Locale =
@ -48,13 +49,11 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
German -> Locale("de", "DE")
French -> Locale("fr", "FR")
Czech -> Locale("cs", "CZ")
Italian -> Locale("it", "IT")
}
fun setLocale(context: Context) {
val locale = getLocale()
Log.i("Rlog", "setLocale: $locale, ${LocaleList.getDefault().get(0)}")
val resources = context.resources
val metrics = resources.displayMetrics
val configuration = resources.configuration
@ -73,8 +72,9 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
}
companion object {
val default = UseDeviceLanguages
val values = listOf(UseDeviceLanguages, English, ChineseSimplified, German, French, Czech)
val values = listOf(UseDeviceLanguages, English, ChineseSimplified, German, French, Czech, Italian)
fun fromPreferences(preferences: Preferences): LanguagesPreference =
when (preferences[DataStoreKeys.Languages.key]) {
@ -84,6 +84,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
3 -> German
4 -> French
5 -> Czech
6 -> Italian
else -> default
}
@ -95,6 +96,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
3 -> German
4 -> French
5 -> Czech
6 -> Italian
else -> default
}
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object NewVersionDownloadUrlPreference {
const val default = ""
fun put(context: Context, scope: CoroutineScope, value: String) {

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object NewVersionLogPreference {
const val default = ""
fun put(context: Context, scope: CoroutineScope, value: String) {

View File

@ -1,17 +1,18 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.ash.reader.data.model.Version
import me.ash.reader.data.model.toVersion
import me.ash.reader.data.model.general.Version
import me.ash.reader.data.model.general.toVersion
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object NewVersionNumberPreference {
val default = Version()
fun put(context: Context, scope: CoroutineScope, value: String) {

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object NewVersionPublishDatePreference {
const val default = ""
fun put(context: Context, scope: CoroutineScope, value: String) {

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object NewVersionSizePreference {
const val default = ""
fun Int.formatSize(): String =

View File

@ -1,10 +1,11 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
sealed class Preference {
abstract fun put(context: Context, scope: CoroutineScope)
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.util.Log
import androidx.compose.runtime.Composable
@ -7,7 +7,7 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.flow.map
import me.ash.reader.data.model.Version
import me.ash.reader.data.model.general.Version
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.dataStore

View File

@ -1,17 +1,18 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.ash.reader.data.model.Version
import me.ash.reader.data.model.toVersion
import me.ash.reader.data.model.general.Version
import me.ash.reader.data.model.general.toVersion
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object SkipVersionNumberPreference {
val default = Version()
fun put(context: Context, scope: CoroutineScope, value: String) {

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.preference
package me.ash.reader.data.model.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@ -10,6 +10,7 @@ import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object ThemeIndexPreference {
const val default = 5
fun put(context: Context, scope: CoroutineScope, value: Int) {

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.Dispatchers
/**
* Provides global coroutine dispatcher.
*
* - [Dispatchers.Main]
* - [Dispatchers.Main.immediate]
* - [Dispatchers.IO]
* - [Dispatchers.Default]
*/
@Module
@InstallIn(SingletonComponent::class)
object CoroutineDispatcherModule {
@Provides
@DispatcherDefault
fun provideDispatcherDefault(): CoroutineDispatcher = Dispatchers.Default
@DefaultDispatcher
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@Provides
@DispatcherIO
fun provideDispatcherIO(): CoroutineDispatcher = Dispatchers.IO
@IODispatcher
fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@DispatcherMain
fun provideDispatcherMain(): CoroutineDispatcher = Dispatchers.Main
@MainDispatcher
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@Provides
@DispatcherMainImmediate
fun provideDispatcherMainImmediate(): CoroutineDispatcher = Dispatchers.Main.immediate
@MainImmediateDispatcher
fun provideMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}

View File

@ -2,18 +2,30 @@ package me.ash.reader.data.module
import javax.inject.Qualifier
/**
* @see CoroutineDispatcherModule.provideDefaultDispatcher
*/
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DispatcherDefault
annotation class DefaultDispatcher
/**
* @see CoroutineDispatcherModule.provideIODispatcher
*/
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DispatcherIO
annotation class IODispatcher
/**
* @see CoroutineDispatcherModule.provideMainDispatcher
*/
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DispatcherMain
annotation class MainDispatcher
/**
* @see CoroutineDispatcherModule.provideMainImmediateDispatcher
*/
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DispatcherMainImmediate
annotation class MainImmediateDispatcher

View File

@ -7,13 +7,12 @@ import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Qualifier
import javax.inject.Singleton
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope
/**
* [CoroutineScope] for the application consisting of [SupervisorJob]
* and [DefaultDispatcher] context.
*/
@Module
@InstallIn(SingletonComponent::class)
object CoroutineScopeModule {
@ -22,6 +21,6 @@ object CoroutineScopeModule {
@Singleton
@ApplicationScope
fun provideCoroutineScope(
@DispatcherDefault dispatcherDefault: CoroutineDispatcher
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcherDefault)
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher,
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
}

View File

@ -13,29 +13,37 @@ import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.source.RYDatabase
import javax.inject.Singleton
/**
* Provides Data Access Objects for database.
*
* - [ArticleDao]
* - [FeedDao]
* - [GroupDao]
* - [AccountDao]
*/
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideArticleDao(RYDatabase: RYDatabase): ArticleDao =
RYDatabase.articleDao()
fun provideArticleDao(ryDatabase: RYDatabase): ArticleDao =
ryDatabase.articleDao()
@Provides
@Singleton
fun provideFeedDao(RYDatabase: RYDatabase): FeedDao =
RYDatabase.feedDao()
fun provideFeedDao(ryDatabase: RYDatabase): FeedDao =
ryDatabase.feedDao()
@Provides
@Singleton
fun provideGroupDao(RYDatabase: RYDatabase): GroupDao =
RYDatabase.groupDao()
fun provideGroupDao(ryDatabase: RYDatabase): GroupDao =
ryDatabase.groupDao()
@Provides
@Singleton
fun provideAccountDao(RYDatabase: RYDatabase): AccountDao =
RYDatabase.accountDao()
fun provideAccountDao(ryDatabase: RYDatabase): AccountDao =
ryDatabase.accountDao()
@Provides
@Singleton

View File

@ -18,6 +18,9 @@ import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
import javax.inject.Singleton
/**
* Provides singleton [ImageLoader] for Coil.
*/
@Module
@InstallIn(SingletonComponent::class)
object ImageLoaderModule {
@ -29,10 +32,14 @@ object ImageLoaderModule {
okHttpClient: OkHttpClient,
): ImageLoader {
return ImageLoader.Builder(context)
// Shared OKHttpClient instance.
.okHttpClient(okHttpClient)
.dispatcher(Dispatchers.Default) // This slightly improves scrolling performance
.components{
// This slightly improves scrolling performance
.dispatcher(Dispatchers.Default)
.components {
// Support SVG decoding
add(SvgDecoder.Factory())
// Support GIF decoding
add(
if (SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoderDecoder.Factory()
@ -41,12 +48,14 @@ object ImageLoaderModule {
}
)
}
// Enable disk cache
.diskCache(
DiskCache.Builder()
.directory(context.cacheDir.resolve("images"))
.maxSizePercent(0.02)
.build()
)
// Enable memory cache
.memoryCache(
MemoryCache.Builder(context)
.maxSizePercent(0.25)

View File

@ -42,13 +42,17 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
/**
* Provides singleton [OkHttpClient] for the application.
*/
@Module
@InstallIn(SingletonComponent::class)
object OkHttpClientModule {
@Provides
@Singleton
fun provideOkHttpClient(
@ApplicationContext context: Context
@ApplicationContext context: Context,
): OkHttpClient = cachingHttpClient(
cacheDirectory = context.cacheDir.resolve("http")
).newBuilder()
@ -61,7 +65,7 @@ fun cachingHttpClient(
cacheSize: Long = 10L * 1024L * 1024L,
trustAllCerts: Boolean = true,
connectTimeoutSecs: Long = 30L,
readTimeoutSecs: Long = 30L
readTimeoutSecs: Long = 30L,
): OkHttpClient {
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
@ -107,6 +111,7 @@ fun OkHttpClient.Builder.trustAllCerts() {
}
object UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(
chain.request()

View File

@ -4,11 +4,18 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import me.ash.reader.data.source.RYNetworkDataSource
import me.ash.reader.data.source.FeverApiDataSource
import me.ash.reader.data.source.GoogleReaderApiDataSource
import me.ash.reader.data.source.RYNetworkDataSource
import javax.inject.Singleton
/**
* Provides network requests for Retrofit.
*
* - [RYNetworkDataSource]: For network requests within the application
* - [FeverApiDataSource]: For network requests to the Fever API
* - [GoogleReaderApiDataSource]: For network requests to the Google Reader API
*/
@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {

View File

@ -9,6 +9,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Provides singleton [WorkManager] for the application.
*/
@Module
@InstallIn(SingletonComponent::class)
object WorkerModule {

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.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.*
import me.ash.reader.data.model.article.Article
import me.ash.reader.data.model.article.ArticleWithFeed
import me.ash.reader.data.model.feed.Feed
import me.ash.reader.data.model.group.Group
import me.ash.reader.data.model.group.GroupWithFeed
import me.ash.reader.ui.ext.currentAccountId
import java.util.*
@ -29,6 +33,7 @@ abstract class AbstractRssRepository constructor(
private val dispatcherIO: CoroutineDispatcher,
private val dispatcherDefault: CoroutineDispatcher,
) {
abstract suspend fun updateArticleInfo(article: Article)
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
@ -53,19 +58,17 @@ abstract class AbstractRssRepository constructor(
)
}
fun pullGroups(): Flow<MutableList<Group>> {
return groupDao.queryAllGroup(context.currentAccountId).flowOn(dispatcherIO)
}
fun pullGroups(): Flow<MutableList<Group>> =
groupDao.queryAllGroup(context.currentAccountId).flowOn(dispatcherIO)
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
return groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(dispatcherIO)
}
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> =
groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(dispatcherIO)
fun pullArticles(
groupId: String? = null,
feedId: String? = null,
isStarred: Boolean = false,
isUnread: Boolean = false,
groupId: String?,
feedId: String?,
isStarred: Boolean,
isUnread: Boolean,
): PagingSource<Int, ArticleWithFeed> {
val accountId = context.currentAccountId
Log.i(
@ -74,32 +77,28 @@ abstract class AbstractRssRepository constructor(
)
return when {
groupId != null -> when {
isStarred -> articleDao
.queryArticleWithFeedByGroupIdWhenIsStarred(accountId, groupId, isStarred)
isUnread -> articleDao
.queryArticleWithFeedByGroupIdWhenIsUnread(accountId, groupId, isUnread)
isStarred -> articleDao.queryArticleWithFeedByGroupIdWhenIsStarred(accountId, groupId, true)
isUnread -> articleDao.queryArticleWithFeedByGroupIdWhenIsUnread(accountId, groupId, true)
else -> articleDao.queryArticleWithFeedByGroupIdWhenIsAll(accountId, groupId)
}
feedId != null -> when {
isStarred -> articleDao
.queryArticleWithFeedByFeedIdWhenIsStarred(accountId, feedId, isStarred)
isUnread -> articleDao
.queryArticleWithFeedByFeedIdWhenIsUnread(accountId, feedId, isUnread)
isStarred -> articleDao.queryArticleWithFeedByFeedIdWhenIsStarred(accountId, feedId, true)
isUnread -> articleDao.queryArticleWithFeedByFeedIdWhenIsUnread(accountId, feedId, true)
else -> articleDao.queryArticleWithFeedByFeedIdWhenIsAll(accountId, feedId)
}
else -> when {
isStarred -> articleDao
.queryArticleWithFeedWhenIsStarred(accountId, isStarred)
isUnread -> articleDao
.queryArticleWithFeedWhenIsUnread(accountId, isUnread)
isStarred -> articleDao.queryArticleWithFeedWhenIsStarred(accountId, true)
isUnread -> articleDao.queryArticleWithFeedWhenIsUnread(accountId, true)
else -> articleDao.queryArticleWithFeedWhenIsAll(accountId)
}
}
}
fun pullImportant(
isStarred: Boolean = false,
isUnread: Boolean = false,
isStarred: Boolean,
isUnread: Boolean,
): Flow<Map<String, Int>> {
val accountId = context.currentAccountId
Log.i(
@ -107,42 +106,28 @@ abstract class AbstractRssRepository constructor(
"pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
)
return when {
isStarred -> articleDao
.queryImportantCountWhenIsStarred(accountId, isStarred)
isUnread -> articleDao
.queryImportantCountWhenIsUnread(accountId, isUnread)
isStarred -> articleDao.queryImportantCountWhenIsStarred(accountId, true)
isUnread -> articleDao.queryImportantCountWhenIsUnread(accountId, true)
else -> articleDao.queryImportantCountWhenIsAll(accountId)
}.mapLatest {
mapOf(
// Groups
*(it.groupBy { it.groupId }.map {
it.key to it.value.sumOf { it.important }
}.toTypedArray()),
*(it.groupBy { it.groupId }.map { it.key to it.value.sumOf { it.important } }.toTypedArray()),
// Feeds
*(it.map {
it.feedId to it.important
}.toTypedArray()),
*(it.map { it.feedId to it.important }.toTypedArray()),
// All summary
"sum" to it.sumOf { it.important }
)
}.flowOn(dispatcherDefault)
}
suspend fun findFeedById(id: String): Feed? {
return feedDao.queryById(id)
}
suspend fun findFeedById(id: String): Feed? = feedDao.queryById(id)
suspend fun findGroupById(id: String): Group? {
return groupDao.queryById(id)
}
suspend fun findGroupById(id: String): Group? = groupDao.queryById(id)
suspend fun findArticleById(id: String): ArticleWithFeed? {
return articleDao.queryById(id)
}
suspend fun findArticleById(id: String): ArticleWithFeed? = articleDao.queryById(id)
suspend fun isFeedExist(url: String): Boolean {
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
}
suspend fun isFeedExist(url: String): Boolean = feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
suspend fun updateGroup(group: Group) {
groupDao.update(group)
@ -184,10 +169,10 @@ abstract class AbstractRssRepository constructor(
fun searchArticles(
content: String,
groupId: String? = null,
feedId: String? = null,
isStarred: Boolean = false,
isUnread: Boolean = false,
groupId: String?,
feedId: String?,
isStarred: Boolean,
isUnread: Boolean,
): PagingSource<Int, ArticleWithFeed> {
val accountId = context.currentAccountId
Log.i(
@ -196,22 +181,20 @@ abstract class AbstractRssRepository constructor(
)
return when {
groupId != null -> when {
isStarred -> articleDao
.searchArticleByGroupIdWhenIsStarred(accountId, content, groupId, isStarred)
isUnread -> articleDao
.searchArticleByGroupIdWhenIsUnread(accountId, content, groupId, isUnread)
isStarred -> articleDao.searchArticleByGroupIdWhenIsStarred(accountId, content, groupId, true)
isUnread -> articleDao.searchArticleByGroupIdWhenIsUnread(accountId, content, groupId, true)
else -> articleDao.searchArticleByGroupIdWhenAll(accountId, content, groupId)
}
feedId != null -> when {
isStarred -> articleDao
.searchArticleByFeedIdWhenIsStarred(accountId, content, feedId, isStarred)
isUnread -> articleDao
.searchArticleByFeedIdWhenIsUnread(accountId, content, feedId, isUnread)
isStarred -> articleDao.searchArticleByFeedIdWhenIsStarred(accountId, content, feedId, true)
isUnread -> articleDao.searchArticleByFeedIdWhenIsUnread(accountId, content, feedId, true)
else -> articleDao.searchArticleByFeedIdWhenAll(accountId, content, feedId)
}
else -> when {
isStarred -> articleDao.searchArticleWhenIsStarred(accountId, content, isStarred)
isUnread -> articleDao.searchArticleWhenIsUnread(accountId, content, isUnread)
isStarred -> articleDao.searchArticleWhenIsStarred(accountId, content, true)
isUnread -> articleDao.searchArticleWhenIsUnread(accountId, content, true)
else -> articleDao.searchArticleWhenAll(accountId, content)
}
}

View File

@ -5,8 +5,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.R
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.Group
import me.ash.reader.data.model.account.Account
import me.ash.reader.data.model.account.AccountType
import me.ash.reader.data.model.group.Group
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.getDefaultGroupId
import javax.inject.Inject
@ -18,20 +19,16 @@ class AccountRepository @Inject constructor(
private val groupDao: GroupDao,
) {
suspend fun getCurrentAccount(): Account? {
return accountDao.queryById(context.currentAccountId)
}
suspend fun getCurrentAccount(): Account? = accountDao.queryById(context.currentAccountId)
suspend fun isNoAccount(): Boolean {
return accountDao.queryAll().isEmpty()
}
suspend fun isNoAccount(): Boolean = accountDao.queryAll().isEmpty()
suspend fun addDefaultAccount(): Account {
val readYouString = context.getString(R.string.read_you)
val defaultString = context.getString(R.string.defaults)
return Account(
name = readYouString,
type = Account.Type.LOCAL,
type = AccountType.Local,
).apply {
id = accountDao.insert(this).toInt()
}.also {

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.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.FeedWithArticle
import me.ash.reader.data.entity.Group
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.model.article.Article
import me.ash.reader.data.model.feed.Feed
import me.ash.reader.data.model.feed.FeedWithArticle
import me.ash.reader.data.model.group.Group
import me.ash.reader.data.module.DefaultDispatcher
import me.ash.reader.data.module.IODispatcher
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
@ -35,14 +35,14 @@ class LocalRssRepository @Inject constructor(
private val notificationHelper: NotificationHelper,
private val accountDao: AccountDao,
private val groupDao: GroupDao,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
@DispatcherDefault
private val dispatcherDefault: CoroutineDispatcher,
@IODispatcher
private val ioDispatcher: CoroutineDispatcher,
@DefaultDispatcher
private val defaultDispatcher: CoroutineDispatcher,
workManager: WorkManager,
) : AbstractRssRepository(
context, accountDao, articleDao, groupDao,
feedDao, workManager, dispatcherIO, dispatcherDefault
feedDao, workManager, ioDispatcher, defaultDispatcher
) {
override suspend fun updateArticleInfo(article: Article) {
@ -70,8 +70,8 @@ class LocalRssRepository @Inject constructor(
}
}
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result {
return supervisorScope {
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result =
supervisorScope {
val preTime = System.currentTimeMillis()
val accountId = context.currentAccountId
feedDao.queryAll(accountId)
@ -81,31 +81,23 @@ class LocalRssRepository @Inject constructor(
it.map { feed -> async { syncFeed(feed) } }
.awaitAll()
.forEach {
if (it.isNotify) {
notificationHelper.notify(
FeedWithArticle(
it.feedWithArticle.feed,
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
)
)
if (it.feed.isNotification) {
notificationHelper.notify(it.apply {
articles = articleDao.insertListIfNotExist(it.articles)
})
} else {
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
articleDao.insertListIfNotExist(it.articles)
}
}
}
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
accountDao.queryById(accountId)?.let { account ->
accountDao.update(
account.apply {
updateAt = Date()
}
)
accountDao.update(account.apply { updateAt = Date() })
}
coroutineWorker.setProgress(setIsSyncing(false))
ListenableWorker.Result.success()
}
}
override suspend fun markAsRead(
groupId: String?,
@ -124,6 +116,7 @@ class LocalRssRepository @Inject constructor(
before = before ?: Date(Long.MAX_VALUE)
)
}
feedId != null -> {
articleDao.markAllAsReadByFeedId(
accountId = accountId,
@ -132,41 +125,34 @@ class LocalRssRepository @Inject constructor(
before = before ?: Date(Long.MAX_VALUE)
)
}
articleId != null -> {
articleDao.markAsReadByArticleId(accountId, articleId, isUnread)
}
else -> {
articleDao.markAllAsRead(accountId, isUnread, before ?: Date(Long.MAX_VALUE))
}
}
}
data class ArticleNotify(
val feedWithArticle: FeedWithArticle,
val isNotify: Boolean,
)
private suspend fun syncFeed(feed: Feed): ArticleNotify {
private suspend fun syncFeed(feed: Feed): FeedWithArticle {
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
val articles: List<Article>?
try {
articles = rssHelper.queryRssXml(feed, latest?.link)
} catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "queryRssXml[${feed.name}]: ${e.message}")
return ArticleNotify(FeedWithArticle(feed, listOf()), false)
}
try {
val articles = rssHelper.queryRssXml(feed, latest?.link)
// try {
// if (feed.icon == null && !articles.isNullOrEmpty()) {
// rssHelper.queryRssIcon(feedDao, feed, articles.first().link)
// }
} catch (e: Exception) {
Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
return ArticleNotify(FeedWithArticle(feed, listOf()), false)
}
return ArticleNotify(
feedWithArticle = FeedWithArticle(feed, articles),
isNotify = articles.isNotEmpty() && feed.isNotification
// } catch (e: Exception) {
// Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
// return FeedWithArticle(
// feed = feed.apply { isNotification = false },
// articles = listOf()
// )
// }
return FeedWithArticle(
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
articles = articles
)
}
}

View File

@ -9,7 +9,7 @@ import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.MainActivity
import me.ash.reader.R
import me.ash.reader.data.entity.FeedWithArticle
import me.ash.reader.data.model.feed.FeedWithArticle
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.*
@ -19,6 +19,7 @@ class NotificationHelper @Inject constructor(
@ApplicationContext
private val context: Context,
) {
private val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(context).apply {
createNotificationChannel(
@ -30,9 +31,7 @@ class NotificationHelper @Inject constructor(
)
}
fun notify(
feedWithArticle: FeedWithArticle,
) {
fun notify(feedWithArticle: FeedWithArticle) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(
feedWithArticle.feed.id,

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.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.model.feed.Feed
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.getDefaultGroupId
@ -18,6 +18,9 @@ import java.io.InputStream
import java.util.*
import javax.inject.Inject
/**
* Supports import and export from OPML files.
*/
class OpmlRepository @Inject constructor(
@ApplicationContext
private val context: Context,
@ -27,6 +30,12 @@ class OpmlRepository @Inject constructor(
private val rssRepository: RssRepository,
private val opmlLocalDataSource: OpmlLocalDataSource,
) {
/**
* Imports OPML file.
*
* @param [inputStream] input stream of OPML file
*/
@Throws(Exception::class)
suspend fun saveToDatabase(inputStream: InputStream) {
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
@ -47,6 +56,9 @@ class OpmlRepository @Inject constructor(
}
}
/**
* Exports OPML file.
*/
@Throws(Exception::class)
suspend fun saveToString(): String {
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
@ -85,7 +97,5 @@ class OpmlRepository @Inject constructor(
)!!
}
private fun getDefaultGroupId(): String {
return context.currentAccountId.getDefaultGroupId()
}
private fun getDefaultGroupId(): String = context.currentAccountId.getDefaultGroupId()
}

View File

@ -8,11 +8,11 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.withContext
import me.ash.reader.R
import me.ash.reader.data.model.toVersion
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.module.DispatcherMain
import me.ash.reader.data.preference.*
import me.ash.reader.data.preference.NewVersionSizePreference.formatSize
import me.ash.reader.data.model.general.toVersion
import me.ash.reader.data.model.preference.*
import me.ash.reader.data.model.preference.NewVersionSizePreference.formatSize
import me.ash.reader.data.module.IODispatcher
import me.ash.reader.data.module.MainDispatcher
import me.ash.reader.data.source.Download
import me.ash.reader.data.source.RYNetworkDataSource
import me.ash.reader.data.source.downloadToFileWithProgress
@ -26,34 +26,35 @@ class RYRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val RYNetworkDataSource: RYNetworkDataSource,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
@DispatcherMain
private val dispatcherMain: CoroutineDispatcher,
@IODispatcher
private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher
private val mainDispatcher: CoroutineDispatcher,
) {
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) {
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(ioDispatcher) {
try {
val response =
RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
val response = RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
when {
response.code() == 403 -> {
withContext(dispatcherMain) {
withContext(mainDispatcher) {
if (showToast) context.showToast(context.getString(R.string.rate_limit))
}
return@withContext null
}
response.body() == null -> {
withContext(dispatcherMain) {
withContext(mainDispatcher) {
if (showToast) context.showToast(context.getString(R.string.check_failure))
}
return@withContext null
}
}
val skipVersion = context.skipVersionNumber.toVersion()
val currentVersion = context.getCurrentVersion()
val latest = response.body()!!
val latestVersion = latest.tag_name.toVersion()
// val latestVersion = "1.0.0".toVersion()
val skipVersion = context.skipVersionNumber.toVersion()
val currentVersion = context.getCurrentVersion()
val latestLog = latest.body ?: ""
val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
val latestSize = latest.assets?.first()?.size ?: 0
@ -74,7 +75,7 @@ class RYRepository @Inject constructor(
} catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "checkUpdate: ${e.message}")
withContext(dispatcherMain) {
withContext(mainDispatcher) {
if (showToast) context.showToast(context.getString(R.string.check_failure))
}
null
@ -82,7 +83,7 @@ class RYRepository @Inject constructor(
}
suspend fun downloadFile(url: String): Flow<Download> =
withContext(dispatcherIO) {
withContext(ioDispatcher) {
Log.i("RLog", "downloadFile start: $url")
try {
return@withContext RYNetworkDataSource.downloadFile(url)
@ -90,7 +91,7 @@ class RYRepository @Inject constructor(
} catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "downloadFile: ${e.message}")
withContext(dispatcherMain) {
withContext(mainDispatcher) {
context.showToast(context.getString(R.string.download_failure))
}
}

View File

@ -10,13 +10,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.FeedWithArticle
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.model.article.Article
import me.ash.reader.data.model.feed.Feed
import me.ash.reader.data.model.feed.FeedWithArticle
import me.ash.reader.data.module.IODispatcher
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
import net.dankito.readability4j.Readability4J
import net.dankito.readability4j.extended.Readability4JExtended
import okhttp3.OkHttpClient
import okhttp3.Request
@ -25,16 +24,20 @@ import java.io.InputStream
import java.util.*
import javax.inject.Inject
/**
* Some operations on RSS.
*/
class RssHelper @Inject constructor(
@ApplicationContext
private val context: Context,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
@IODispatcher
private val ioDispatcher: CoroutineDispatcher,
private val okHttpClient: OkHttpClient,
) {
@Throws(Exception::class)
suspend fun searchFeed(feedLink: String): FeedWithArticle {
return withContext(dispatcherIO) {
return withContext(ioDispatcher) {
val accountId = context.currentAccountId
val syndFeed = SyndFeedInput().build(XmlReader(inputStream(okHttpClient, feedLink)))
val feed = Feed(
@ -49,16 +52,9 @@ class RssHelper @Inject constructor(
}
}
fun parseDescriptionContent(link: String, content: String): String {
val readability4J: Readability4J = Readability4JExtended(link, content)
val article = readability4J.parse()
val element = article.articleContent
return element.toString()
}
@Throws(Exception::class)
suspend fun parseFullContent(link: String, title: String): String {
return withContext(dispatcherIO) {
return withContext(ioDispatcher) {
val response = response(okHttpClient, link)
val content = response.body.string()
val readability4J = Readability4JExtended(link, content)
@ -75,27 +71,31 @@ class RssHelper @Inject constructor(
}
}
@Throws(Exception::class)
suspend fun queryRssXml(
feed: Feed,
latestLink: String? = null,
): List<Article> {
val accountId = context.currentAccountId
return inputStream(okHttpClient, feed.url).use {
SyndFeedInput().apply { isPreserveWireFeed = true }
.build(XmlReader(it))
.entries
.asSequence()
.takeWhile { latestLink == null || latestLink != it.link }
.map { article(feed, accountId, it) }
.toList()
latestLink: String?,
): List<Article> =
try {
val accountId = context.currentAccountId
inputStream(okHttpClient, feed.url).use {
SyndFeedInput().apply { isPreserveWireFeed = true }
.build(XmlReader(it))
.entries
.asSequence()
.takeWhile { latestLink == null || latestLink != it.link }
.map { article(feed, accountId, it) }
.toList()
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "queryRssXml[${feed.name}]: ${e.message}")
listOf()
}
}
private fun article(
feed: Feed,
accountId: Int,
syndEntry: SyndEntry
syndEntry: SyndEntry,
): Article {
val desc = syndEntry.description?.value
val content = syndEntry.contents
@ -144,7 +144,7 @@ class RssHelper @Inject constructor(
feed: Feed,
articleLink: String,
) {
withContext(dispatcherIO) {
withContext(ioDispatcher) {
val domainRegex = Regex("(http|https)://(www.)?(\\w+(\\.)?)+")
val request = response(okHttpClient, articleLink)
val content = request.body.string()
@ -183,11 +183,11 @@ class RssHelper @Inject constructor(
private suspend fun inputStream(
client: OkHttpClient,
url: String
url: String,
): InputStream = response(client, url).body.byteStream()
private suspend fun response(
client: OkHttpClient,
url: String
url: String,
) = client.newCall(Request.Builder().url(url).build()).executeAsync()
}

View File

@ -2,7 +2,7 @@ package me.ash.reader.data.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.data.entity.Account
import me.ash.reader.data.model.account.AccountType
import me.ash.reader.ui.ext.currentAccountType
import javax.inject.Inject
@ -13,8 +13,9 @@ class RssRepository @Inject constructor(
// private val feverRssRepository: FeverRssRepository,
// private val googleReaderRssRepository: GoogleReaderRssRepository,
) {
fun get() = when (context.currentAccountType) {
Account.Type.LOCAL -> localRssRepository
AccountType.Local.id -> localRssRepository
// Account.Type.LOCAL -> feverRssRepository
// Account.Type.FEVER -> feverRssRepository
// Account.Type.GOOGLE_READER -> googleReaderRssRepository

View File

@ -10,6 +10,7 @@ class StringsRepository @Inject constructor(
@ApplicationContext
private val context: Context,
) {
fun getString(resId: Int, vararg formatArgs: Any) = context.getString(resId, *formatArgs)
fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any) =
@ -18,6 +19,6 @@ class StringsRepository @Inject constructor(
fun formatAsString(
date: Date?,
onlyHourMinute: Boolean? = false,
atHourMinute: Boolean? = false
atHourMinute: Boolean? = false,
) = date?.formatAsString(context, onlyHourMinute, atHourMinute)
}

View File

@ -18,17 +18,17 @@ class SyncWorker @AssistedInject constructor(
private val rssRepository: RssRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ")
return withContext(Dispatchers.Default) {
override suspend fun doWork(): Result =
withContext(Dispatchers.Default) {
Log.i("RLog", "doWork: ")
rssRepository.get().sync(this@SyncWorker)
}
}
companion object {
const val WORK_NAME = "article.sync"
val UUID: UUID
val uuid: UUID
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
@ -36,7 +36,7 @@ class SyncWorker @AssistedInject constructor(
Constraints.Builder()
.build()
).addTag(WORK_NAME).build().also {
UUID = it.id
uuid = it.id
}
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)

View File

@ -11,6 +11,7 @@ import retrofit2.http.Part
import retrofit2.http.Query
interface FeverApiDataSource {
@Multipart
@POST("fever.php/?api=&feeds=")
fun feeds(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed>
@ -23,7 +24,7 @@ interface FeverApiDataSource {
@POST("fever.php/?api=&items=")
fun itemsBySince(
@Query("since_id") since: Long,
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody(),
): Call<FeverApiDto.Items>
@Multipart
@ -38,10 +39,11 @@ interface FeverApiDataSource {
@POST("fever.php/?api=&items=")
fun itemsByIds(
@Query("with_ids") ids: String,
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody(),
): Call<FeverApiDto.Items>
companion object {
private var instance: FeverApiDataSource? = null
fun getInstance(): FeverApiDataSource {

View File

@ -7,6 +7,7 @@ import retrofit2.http.Headers
import retrofit2.http.POST
interface GoogleReaderApiDataSource {
@POST("accounts/ClientLogin")
fun login(Email: String, Passwd: String): Call<String>
@ -27,6 +28,7 @@ interface GoogleReaderApiDataSource {
fun readingList(): Call<GoogleReaderApiDto.ReadingList>
companion object {
private var instance: GoogleReaderApiDataSource? = null
fun getInstance(): GoogleReaderApiDataSource {

View File

@ -5,10 +5,10 @@ import be.ceau.opml.OpmlParser
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.entity.GroupWithFeed
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.model.feed.Feed
import me.ash.reader.data.model.group.Group
import me.ash.reader.data.model.group.GroupWithFeed
import me.ash.reader.data.module.IODispatcher
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
import java.io.InputStream
@ -18,15 +18,16 @@ import javax.inject.Inject
class OpmlLocalDataSource @Inject constructor(
@ApplicationContext
private val context: Context,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
@IODispatcher
private val ioDispatcher: CoroutineDispatcher,
) {
@Throws(Exception::class)
suspend fun parseFileInputStream(
inputStream: InputStream,
defaultGroup: Group
defaultGroup: Group,
): List<GroupWithFeed> {
return withContext(dispatcherIO) {
return withContext(ioDispatcher) {
val accountId = context.currentAccountId
val opml = OpmlParser().parse(inputStream)
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {

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.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.model.account.Account
import me.ash.reader.data.model.account.AccountTypeConverters
import me.ash.reader.data.model.article.Article
import me.ash.reader.data.model.feed.Feed
import me.ash.reader.data.model.group.Group
import java.util.*
@Database(
entities = [Account::class, Feed::class, Article::class, Group::class],
version = 2,
version = 2
)
@TypeConverters(RYDatabase.Converters::class)
@TypeConverters(RYDatabase.DateConverters::class, AccountTypeConverters::class)
abstract class RYDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao
abstract fun feedDao(): FeedDao
abstract fun articleDao(): ArticleDao
abstract fun groupDao(): GroupDao
companion object {
private var instance: RYDatabase? = null
fun getInstance(context: Context): RYDatabase {
@ -41,7 +44,7 @@ abstract class RYDatabase : RoomDatabase() {
}
}
class Converters {
class DateConverters {
@TypeConverter
fun toDate(dateLong: Long?): Date? {
@ -61,6 +64,7 @@ val allMigrations = arrayOf(
@Suppress("ClassName")
object MIGRATION_1_2 : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""

View File

@ -15,6 +15,7 @@ import retrofit2.http.Url
import java.io.File
interface RYNetworkDataSource {
@GET
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
@ -23,6 +24,7 @@ interface RYNetworkDataSource {
suspend fun downloadFile(@Url url: String): ResponseBody
companion object {
private var instance: RYNetworkDataSource? = null
fun getInstance(): RYNetworkDataSource {
@ -69,8 +71,10 @@ fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
when {
progressBytes < totalBytes ->
throw Exception("missing bytes")
progressBytes > totalBytes ->
throw Exception("too many bytes")
else ->
deleteFile = false
}

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
fun FeedIcon(
feedName: String,
size: Dp = 20.dp
size: Dp = 20.dp,
) {
Row(
modifier = Modifier

View File

@ -12,10 +12,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import me.ash.reader.data.model.Filter
import me.ash.reader.data.model.getName
import me.ash.reader.data.preference.FlowFilterBarStylePreference
import me.ash.reader.data.preference.LocalThemeIndex
import me.ash.reader.data.model.general.Filter
import me.ash.reader.data.model.preference.FlowFilterBarStylePreference
import me.ash.reader.data.model.preference.LocalThemeIndex
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.theme.palette.onDark
@ -54,7 +53,7 @@ fun FilterBar(
} else {
item.iconOutline
},
contentDescription = item.getName()
contentDescription = item.toName()
)
},
label = if (filterBarStyle == FlowFilterBarStylePreference.Icon.value) {
@ -62,7 +61,7 @@ fun FilterBar(
} else {
{
Text(
text = item.getName(),
text = item.toName(),
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

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,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
popupContentSize: IntSize,
): IntOffset {
return IntOffset(
x = with(density) { (absoluteX).roundToPx() },

View File

@ -36,7 +36,7 @@ fun AnimatedText(
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
style: TextStyle = LocalTextStyle.current,
) {
AnimatedContent(
targetState = text,

View File

@ -76,7 +76,7 @@ fun ClipboardTextField(
private fun action(
focusManager: FocusManager?,
onConfirm: (String) -> Unit,
value: String
value: String,
): KeyboardActionScope.() -> Unit = {
focusManager?.clearFocus()
onConfirm(value)

View File

@ -22,13 +22,14 @@ class CurlyCornerShape(
bottomEnd = ZeroCornerSize,
bottomStart = ZeroCornerSize
) {
private fun sineCircleXYatAngle(
d1: Double,
d2: Double,
d3: Double,
d4: Double,
d5: Double,
i: Int
i: Int,
): List<Double> = (i.toDouble() * d5).run {
listOf(
(sin(this) * d4 + d3) * cos(d5) + d1,
@ -42,7 +43,7 @@ class CurlyCornerShape(
topEnd: Float,
bottomEnd: Float,
bottomStart: Float,
layoutDirection: LayoutDirection
layoutDirection: LayoutDirection,
): Outline {
val d = 2.0
val r2: Double = size.width / d
@ -73,7 +74,7 @@ class CurlyCornerShape(
topStart: CornerSize,
topEnd: CornerSize,
bottomEnd: CornerSize,
bottomStart: CornerSize
bottomStart: CornerSize,
) = RoundedCornerShape(
topStart = topStart,
topEnd = topEnd,

View File

@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize
import com.caverock.androidsvg.SVG
import me.ash.reader.data.preference.LocalDarkTheme
import me.ash.reader.data.model.preference.LocalDarkTheme
import me.ash.reader.ui.svg.parseDynamicColor
import me.ash.reader.ui.theme.palette.LocalTonalPalettes

View File

@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
@Composable
fun RYExtensibleVisibility(
visible: Boolean,
content: @Composable AnimatedVisibilityScope.() -> Unit
content: @Composable AnimatedVisibilityScope.() -> Unit,
) {
AnimatedVisibility(
visible = visible,

View File

@ -35,7 +35,7 @@ fun RYSwitch(
modifier: Modifier = Modifier,
activated: Boolean,
enable: Boolean = true,
onClick: (() -> Unit)? = null
onClick: (() -> Unit)? = null,
) {
val tonalPalettes = LocalTonalPalettes.current
@ -74,7 +74,7 @@ fun SwitchHeadline(
activated: Boolean,
onClick: () -> Unit,
title: String,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
val tonalPalettes = LocalTonalPalettes.current

View File

@ -21,7 +21,7 @@ fun Tips(
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
.padding(horizontal = 24.dp, vertical = 16.dp),
) {
Icon(
imageVector = Icons.Outlined.Info,

View File

@ -18,7 +18,7 @@ const val INJECTION_TOKEN = "/android_asset_font/"
fun WebView(
modifier: Modifier = Modifier,
content: String,
onReceivedError: (error: WebResourceError?) -> Unit = {}
onReceivedError: (error: WebResourceError?) -> Unit = {},
) {
val context = LocalContext.current
val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
@ -28,7 +28,7 @@ fun WebView(
override fun shouldInterceptRequest(
view: WebView?,
url: String?
url: String?,
): WebResourceResponse? {
if (url != null && url.contains(INJECTION_TOKEN)) {
try {
@ -63,7 +63,7 @@ fun WebView(
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
request: WebResourceRequest?,
): Boolean {
if (null == request?.url) return false
val url = request.url.toString()
@ -79,7 +79,7 @@ fun WebView(
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
error: WebResourceError?,
) {
super.onReceivedError(view, request, error)
onReceivedError(error)
@ -88,7 +88,7 @@ fun WebView(
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
error: SslError?,
) {
handler?.cancel()
}

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
class AnnotatedParagraphStringBuilder {
// Private for a reason
private val builder: AnnotatedString.Builder = AnnotatedString.Builder()
@ -60,7 +61,7 @@ class AnnotatedParagraphStringBuilder {
builder.pushStringAnnotation(tag = tag, annotation = annotation)
fun pushComposableStyle(
style: @Composable () -> SpanStyle
style: @Composable () -> SpanStyle,
): Int {
composableStyles.add(
ComposableStyleWithStartEnd(
@ -72,7 +73,7 @@ class AnnotatedParagraphStringBuilder {
}
fun popComposableStyle(
index: Int
index: Int,
) {
poppedComposableStyles.add(
composableStyles.removeAt(index).copy(end = builder.length)
@ -122,20 +123,25 @@ fun AnnotatedParagraphStringBuilder.ensureDoubleNewline() {
lastTwoChars.isEmpty() -> {
// Nothing to do
}
length == 1 && lastTwoChars.peekLatest()?.isWhitespace() == true -> {
// Nothing to do
}
length == 2 &&
lastTwoChars.peekLatest()?.isWhitespace() == true &&
lastTwoChars.peekSecondLatest()?.isWhitespace() == true -> {
// Nothing to do
}
lastTwoChars.peekLatest() == '\n' && lastTwoChars.peekSecondLatest() == '\n' -> {
// Nothing to do
}
lastTwoChars.peekLatest() == '\n' -> {
append('\n')
}
else -> {
append("\n\n")
}
@ -147,12 +153,15 @@ private fun AnnotatedParagraphStringBuilder.ensureSingleNewline() {
lastTwoChars.isEmpty() -> {
// Nothing to do
}
length == 1 && lastTwoChars.peekLatest()?.isWhitespace() == true -> {
// Nothing to do
}
lastTwoChars.peekLatest() == '\n' -> {
// Nothing to do
}
else -> {
append('\n')
}
@ -187,5 +196,5 @@ private fun <T> List<T>.peekSecondLatest(): T? {
data class ComposableStyleWithStartEnd(
val style: @Composable () -> SpanStyle,
val start: Int,
val end: Int = -1
val end: Int = -1,
)

View File

@ -206,6 +206,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
is Element -> {
val element = node
when (element.tagName()) {
@ -232,6 +233,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"br" -> append('\n')
"h1" -> {
withParagraph {
@ -242,6 +244,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"h2" -> {
withParagraph {
withComposableStyle(
@ -251,6 +254,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"h3" -> {
withParagraph {
withComposableStyle(
@ -260,6 +264,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"h4" -> {
withParagraph {
withComposableStyle(
@ -269,6 +274,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"h5" -> {
withParagraph {
withComposableStyle(
@ -278,6 +284,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"h6" -> {
withParagraph {
withComposableStyle(
@ -287,6 +294,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"strong", "b" -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendTextChildren(
@ -298,6 +306,7 @@ private fun TextComposer.appendTextChildren(
)
}
}
"i", "em", "cite", "dfn" -> {
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
appendTextChildren(
@ -309,6 +318,7 @@ private fun TextComposer.appendTextChildren(
)
}
}
"tt" -> {
withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) {
appendTextChildren(
@ -320,6 +330,7 @@ private fun TextComposer.appendTextChildren(
)
}
}
"u" -> {
withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) {
appendTextChildren(
@ -331,6 +342,7 @@ private fun TextComposer.appendTextChildren(
)
}
}
"sup" -> {
withStyle(SpanStyle(baselineShift = BaselineShift.Superscript)) {
appendTextChildren(
@ -342,6 +354,7 @@ private fun TextComposer.appendTextChildren(
)
}
}
"sub" -> {
withStyle(SpanStyle(baselineShift = BaselineShift.Subscript)) {
appendTextChildren(
@ -353,6 +366,7 @@ private fun TextComposer.appendTextChildren(
)
}
}
"font" -> {
val fontFamily: FontFamily? = element.attr("face")?.asFontFamily()
withStyle(SpanStyle(fontFamily = fontFamily)) {
@ -365,6 +379,7 @@ private fun TextComposer.appendTextChildren(
)
}
}
"pre" -> {
appendTextChildren(
element.childNodes(),
@ -375,6 +390,7 @@ private fun TextComposer.appendTextChildren(
baseUrl = baseUrl,
)
}
"code" -> {
if (element.parent()?.tagName() == "pre") {
terminateCurrentText()
@ -400,6 +416,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"blockquote" -> {
withParagraph {
withComposableStyle(
@ -415,6 +432,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"a" -> {
withComposableStyle(
style = { linkTextStyle().toSpanStyle() }
@ -430,6 +448,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"img" -> {
val imageCandidates = getImageSource(baseUrl, element)
if (imageCandidates.hasImage) {
@ -502,6 +521,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"ul" -> {
element.children()
.filter { it.tagName() == "li" }
@ -519,6 +539,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"ol" -> {
element.children()
.filter { it.tagName() == "li" }
@ -536,6 +557,7 @@ private fun TextComposer.appendTextChildren(
}
}
}
"table" -> {
appendTable {
/*
@ -581,6 +603,7 @@ private fun TextComposer.appendTextChildren(
append("\n\n")
}
}
"iframe" -> {
val video: Video? = getVideo(element.attr("abs:src"))
@ -629,9 +652,11 @@ private fun TextComposer.appendTextChildren(
}
}
}
"video" -> {
// not implemented yet. remember to disable selection
}
else -> {
appendTextChildren(
nodes = element.childNodes(),
@ -707,8 +732,9 @@ internal fun getImageSource(baseUrl: String, element: Element) = ImageCandidates
internal class ImageCandidates(
val baseUrl: String,
val srcSet: String,
val absSrc: String
val absSrc: String,
) {
val hasImage: Boolean = srcSet.isNotBlank() || absSrc.isNotBlank()
/**
@ -728,9 +754,11 @@ internal class ImageCandidates(
descriptor.endsWith("w", ignoreCase = true) -> {
descriptor.substringBefore("w").toFloat() / maxSize.width.pxOrElse { 1 }
}
descriptor.endsWith("x", ignoreCase = true) -> {
descriptor.substringBefore("x").toFloat() / pixelDensity
}
else -> {
return@fold acc
}

View File

@ -24,8 +24,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.text.SpanStyle
class TextComposer(
val paragraphEmitter: (AnnotatedParagraphStringBuilder) -> Unit
val paragraphEmitter: (AnnotatedParagraphStringBuilder) -> Unit,
) {
val spanStack: MutableList<Span> = mutableListOf()
// The identity of this will change - do not reference it in blocks
@ -48,6 +49,7 @@ class TextComposer(
tag = span.tag,
annotation = span.annotation
)
is SpanWithComposableStyle -> builder.pushComposableStyle(span.spanStyle)
}
}
@ -75,8 +77,8 @@ class TextComposer(
link: String? = null,
onLinkClick: (String) -> Unit,
block: (
onClick: (() -> Unit)?
) -> R
onClick: (() -> Unit)?,
) -> R,
): R {
val url = link ?: findClosestLink()
//builder.ensureDoubleNewline()
@ -117,7 +119,7 @@ class TextComposer(
}
inline fun <R : Any> TextComposer.withParagraph(
crossinline block: TextComposer.() -> R
crossinline block: TextComposer.() -> R,
): R {
ensureDoubleNewline()
return block(this)
@ -125,7 +127,7 @@ inline fun <R : Any> TextComposer.withParagraph(
inline fun <R : Any> TextComposer.withStyle(
style: SpanStyle,
crossinline block: TextComposer.() -> R
crossinline block: TextComposer.() -> R,
): R {
spanStack.add(SpanWithStyle(style))
val index = pushStyle(style)
@ -139,7 +141,7 @@ inline fun <R : Any> TextComposer.withStyle(
inline fun <R : Any> TextComposer.withComposableStyle(
noinline style: @Composable () -> SpanStyle,
crossinline block: TextComposer.() -> R
crossinline block: TextComposer.() -> R,
): R {
spanStack.add(SpanWithComposableStyle(style))
val index = pushComposableStyle(style)
@ -154,7 +156,7 @@ inline fun <R : Any> TextComposer.withComposableStyle(
inline fun <R : Any> TextComposer.withAnnotation(
tag: String,
annotation: String,
crossinline block: TextComposer.() -> R
crossinline block: TextComposer.() -> R,
): R {
spanStack.add(SpanWithAnnotation(tag = tag, annotation = annotation))
val index = pushStringAnnotation(tag = tag, annotation = annotation)
@ -169,14 +171,14 @@ inline fun <R : Any> TextComposer.withAnnotation(
sealed class Span
data class SpanWithStyle(
val spanStyle: SpanStyle
val spanStyle: SpanStyle,
) : Span()
data class SpanWithAnnotation(
val tag: String,
val annotation: String
val annotation: String,
) : Span()
data class SpanWithComposableStyle(
val spanStyle: @Composable () -> SpanStyle
val spanStyle: @Composable () -> SpanStyle,
) : Span()

View File

@ -44,8 +44,9 @@ data class Video(
val src: String,
val imageUrl: String,
// Youtube needs a different link than embed links
val link: String
val link: String,
) {
val width: Int
get() = 480

Some files were not shown because too many files have changed in this diff Show More