Refactor data layer

This commit is contained in:
Ash 2022-05-17 20:39:07 +08:00
parent ee55f671bc
commit b33e0a7ac5
31 changed files with 335 additions and 433 deletions

View File

@ -5,9 +5,6 @@
<uses-permission android:name="android.permission.INTERNET" />
<!-- Disable automatic updates in F-Droid -->
<!-- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />-->
<application
android:name=".App"
android:allowBackup="true"

View File

@ -16,12 +16,9 @@ import me.ash.reader.data.source.AppNetworkDataSource
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.ui.ext.*
import okhttp3.Cache
import okhttp3.OkHttpClient
import org.conscrypt.Conscrypt
import java.io.File
import java.security.Security
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltAndroidApp
@ -50,6 +47,9 @@ class App : Application(), Configuration.Provider {
@Inject
lateinit var rssHelper: RssHelper
@Inject
lateinit var notificationHelper: NotificationHelper
@Inject
lateinit var appRepository: AppRepository
@ -62,9 +62,6 @@ class App : Application(), Configuration.Provider {
@Inject
lateinit var localRssRepository: LocalRssRepository
// @Inject
// lateinit var feverRssRepository: FeverRssRepository
@Inject
lateinit var opmlRepository: OpmlRepository
@ -92,7 +89,7 @@ class App : Application(), Configuration.Provider {
applicationScope.launch(dispatcherDefault) {
accountInit()
workerInit()
if (BuildConfig.FLAVOR != "fdroid") {
if (notFdroid) {
checkUpdate()
}
}
@ -128,28 +125,3 @@ class App : Application(), Configuration.Provider {
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
}
fun cachingHttpClient(
cacheDirectory: File? = null,
cacheSize: Long = 10L * 1024L * 1024L,
trustAllCerts: Boolean = true,
connectTimeoutSecs: Long = 30L,
readTimeoutSecs: Long = 30L
): OkHttpClient {
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
if (cacheDirectory != null) {
builder.cache(Cache(cacheDirectory, cacheSize))
}
builder
.connectTimeout(connectTimeoutSecs, TimeUnit.SECONDS)
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
.followRedirects(true)
// if (trustAllCerts) {
// builder.trustAllCerts()
// }
return builder.build()
}

View File

@ -5,7 +5,7 @@ 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.entity.ImportantCount
import me.ash.reader.data.model.ImportantCount
import java.util.*
@Dao

View File

@ -1,23 +0,0 @@
package me.ash.reader.data.entity
data class LatestRelease(
val html_url: String? = null,
val tag_name: String? = null,
val name: String? = null,
val draft: Boolean? = null,
val prerelease: Boolean? = null,
val created_at: String? = null,
val published_at: String? = null,
val assets: List<AssetsItem>? = null,
val body: String? = null,
)
data class AssetsItem(
val name: String? = null,
val content_type: String? = null,
val size: Int? = null,
val download_count: Int? = null,
val created_at: String? = null,
val updated_at: String? = null,
val browser_download_url: String? = null,
)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity
package me.ash.reader.data.model
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FiberManualRecord
@ -6,7 +6,10 @@ import androidx.compose.material.icons.rounded.FiberManualRecord
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subject
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
class Filter(
val index: Int,
@ -33,5 +36,13 @@ class Filter(
iconOutline = Icons.Rounded.Subject,
iconFilled = Icons.Rounded.Subject,
)
val values = listOf(Starred, Unread, All)
}
}
@Composable
fun Filter.getName(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread)
Filter.Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all)
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity
package me.ash.reader.data.model
data class ImportantCount(
val important: Int,

View File

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

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity
package me.ash.reader.data.model
class Version(identifiers: List<String>) {
private var major: Int = 0

View File

@ -1,3 +1,23 @@
/*
* Feeder: Android RSS reader app
* https://gitlab.com/spacecowboy/Feeder
*
* Copyright (C) 2022 Jonas Kalderstam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.ash.reader.data.module
import android.content.Context
@ -7,11 +27,20 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import me.ash.reader.BuildConfig
import me.ash.reader.cachingHttpClient
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.File
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
@Module
@InstallIn(SingletonComponent::class)
@ -28,6 +57,56 @@ object OkHttpClientModule {
.build()
}
fun cachingHttpClient(
cacheDirectory: File? = null,
cacheSize: Long = 10L * 1024L * 1024L,
trustAllCerts: Boolean = true,
connectTimeoutSecs: Long = 30L,
readTimeoutSecs: Long = 30L
): OkHttpClient {
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
if (cacheDirectory != null) {
builder.cache(Cache(cacheDirectory, cacheSize))
}
builder
.connectTimeout(connectTimeoutSecs, TimeUnit.SECONDS)
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
.followRedirects(true)
if (trustAllCerts) {
builder.trustAllCerts()
}
return builder.build()
}
fun OkHttpClient.Builder.trustAllCerts() {
try {
val trustManager = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
val sslSocketFactory = sslContext.socketFactory
sslSocketFactory(sslSocketFactory, trustManager)
.hostnameVerifier(HostnameVerifier { _, _ -> true })
} catch (e: NoSuchAlgorithmException) {
// ignore
} catch (e: KeyManagementException) {
// ignore
}
}
object UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(

View File

@ -2,11 +2,11 @@ package me.ash.reader.data.repository
import android.content.Context
import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.paging.PagingSource
import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
@ -15,9 +15,9 @@ 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.ImportantCount
import me.ash.reader.ui.ext.currentAccountId
import java.util.*
import java.util.concurrent.TimeUnit
abstract class AbstractRssRepository constructor(
private val context: Context,
@ -130,10 +130,6 @@ abstract class AbstractRssRepository constructor(
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
}
fun peekWork(): String {
return workManager.getWorkInfosByTag("sync").get().size.toString()
}
suspend fun updateGroup(group: Group) {
groupDao.update(group)
}
@ -207,34 +203,3 @@ abstract class AbstractRssRepository constructor(
}
}
}
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val rssRepository: RssRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ")
return rssRepository.get().sync(this)
}
companion object {
const val WORK_NAME = "article.sync"
val UUID: UUID
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.build()
).addTag(WORK_NAME).build().also {
UUID = it.id
}
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
}
}

View File

@ -4,13 +4,11 @@ import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.withContext
import me.ash.reader.R
import me.ash.reader.data.entity.toVersion
import me.ash.reader.data.module.ApplicationScope
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.source.AppNetworkDataSource
@ -23,8 +21,6 @@ class AppRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val appNetworkDataSource: AppNetworkDataSource,
@ApplicationScope
private val applicationScope: CoroutineScope,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
@DispatcherMain
@ -55,14 +51,8 @@ class AppRepository @Inject constructor(
val currentVersion = context.getCurrentVersion()
val latestLog = latest.body ?: ""
val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
val latestSize = latest.assets
?.first()
?.size
?: 0
val latestDownloadUrl = latest.assets
?.first()
?.browser_download_url
?: ""
val latestSize = latest.assets?.first()?.size ?: 0
val latestDownloadUrl = latest.assets?.first()?.browser_download_url ?: ""
Log.i("RLog", "current version $currentVersion")
if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) {

View File

@ -1,163 +0,0 @@
//package me.ash.reader.data.repository
//
//import android.content.Context
//import android.util.Log
//import androidx.work.WorkManager
//import dagger.hilt.android.qualifiers.ApplicationContext
//import kotlinx.coroutines.CoroutineDispatcher
//import kotlinx.coroutines.CoroutineScope
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.sync.withLock
//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.Group
//import me.ash.reader.data.module.ApplicationScope
//import me.ash.reader.data.module.DispatcherDefault
//import me.ash.reader.data.module.DispatcherIO
//import me.ash.reader.data.source.FeverApiDataSource
//import me.ash.reader.data.source.RssNetworkDataSource
//import me.ash.reader.ui.ext.currentAccountId
//import me.ash.reader.ui.ext.spacerDollar
//import net.dankito.readability4j.extended.Readability4JExtended
//import java.util.*
//import javax.inject.Inject
//import kotlin.collections.set
//
//class FeverRssRepository @Inject constructor(
// @ApplicationContext
// private val context: Context,
// private val articleDao: ArticleDao,
// private val feedDao: FeedDao,
// private val groupDao: GroupDao,
// private val rssHelper: RssHelper,
// private val feverApiDataSource: FeverApiDataSource,
// private val accountDao: AccountDao,
// rssNetworkDataSource: RssNetworkDataSource,
// @ApplicationScope
// private val applicationScope: CoroutineScope,
// @DispatcherDefault
// private val dispatcherDefault: CoroutineDispatcher,
// @DispatcherIO
// private val dispatcherIO: CoroutineDispatcher,
// workManager: WorkManager,
//) : AbstractRssRepository(
// context, accountDao, articleDao, groupDao,
// feedDao, rssNetworkDataSource, workManager,
// dispatcherIO
//) {
// override suspend fun updateArticleInfo(article: Article) {
// articleDao.update(article)
// }
//
// override suspend fun subscribe(feed: Feed, articles: List<Article>) {
// feedDao.insert(feed)
// articleDao.insertList(articles.map {
// it.copy(feedId = feed.id)
// })
// }
//
// override suspend fun addGroup(name: String): String {
// return UUID.randomUUID().toString().also {
// groupDao.insert(
// Group(
// id = it,
// name = name,
// accountId = context.currentAccountId
// )
// )
// }
// }
//
// override suspend fun sync() {
// applicationScope.launch(dispatcherDefault) {
// mutex.withLock {
// val accountId = context.currentAccountId
//
// updateSyncState {
// it.copy(
// feedCount = 1,
// syncedCount = 1,
// currentFeedName = "Fever"
// )
// }
//
// if (feedDao.queryAll(accountId).isNullOrEmpty()) {
// // Temporary add feeds
// val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
// val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
// Log.i("RLog", "Fever groups: $feverGroupsBody")
// feverGroupsBody.groups.forEach {
// groupDao.insert(
// Group(
// id = accountId.spacerDollar(it.id),
// name = it.title,
// accountId = accountId,
// )
// )
// }
// val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
// feverGroupsBody.feeds_groups.forEach { item ->
// item.feed_ids
// .split(",")
// .map { it.toInt() }
// .forEach { id ->
// feverFeedsGroupsMap[id] = item.group_id
// }
// }
// val feeds = feverFeeds.map {
// Feed(
// id = accountId.spacerDollar(it.id),
// name = it.title,
// url = it.url,
// groupId = feverFeedsGroupsMap[it.id].toString(),
// accountId = accountId
// )
// }
// feedDao.insertList(feeds)
// }
//
// // Add articles
// val articles = mutableListOf<Article>()
// feverApiDataSource.itemsBySince(since = 1647444325925621L)
// .execute().body()!!.items
// .forEach {
// articles.add(
// Article(
// id = accountId.spacerDollar(it.id),
// date = Date(it.created_on_time * 1000),
// title = it.title,
// author = it.author,
// rawDescription = it.html,
// shortDescription = (
// Readability4JExtended("", it.html)
// .parse().textContent ?: ""
// ).take(100).trim(),
// link = it.url,
// accountId = accountId,
// feedId = it.feed_id.toString(),
// isUnread = it.is_read == 0,
// isStarred = it.is_saved == 1,
// )
// )
// }
// articleDao.insertList(articles)
//
// // Complete sync
// accountDao.update(accountDao.queryById(accountId)!!.apply {
// updateAt = Date()
// })
// updateSyncState {
// it.copy(
// feedCount = 0,
// syncedCount = 0,
// currentFeedName = ""
// )
// }
// }
// }
// }
//}

View File

@ -1,12 +1,7 @@
package me.ash.reader.data.repository
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkManager
@ -15,8 +10,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import me.ash.reader.MainActivity
import me.ash.reader.R
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
@ -30,8 +23,6 @@ import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.*
import javax.inject.Inject
@ -41,6 +32,7 @@ class LocalRssRepository @Inject constructor(
private val articleDao: ArticleDao,
private val feedDao: FeedDao,
private val rssHelper: RssHelper,
private val notificationHelper: NotificationHelper,
private val accountDao: AccountDao,
private val groupDao: GroupDao,
@DispatcherDefault
@ -52,16 +44,6 @@ class LocalRssRepository @Inject constructor(
context, accountDao, articleDao, groupDao,
feedDao, workManager, dispatcherIO
) {
private val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(context).apply {
createNotificationChannel(
NotificationChannel(
NotificationGroupName.ARTICLE_UPDATE,
NotificationGroupName.ARTICLE_UPDATE,
NotificationManager.IMPORTANCE_DEFAULT
)
)
}
override suspend fun updateArticleInfo(article: Article) {
articleDao.update(article)
@ -98,7 +80,7 @@ class LocalRssRepository @Inject constructor(
.awaitAll()
.forEach {
if (it.isNotify) {
notify(
notificationHelper.notify(
FeedWithArticle(
it.feedWithArticle.feed,
articleDao.insertIfNotExist(it.feedWithArticle.articles)
@ -183,75 +165,4 @@ class LocalRssRepository @Inject constructor(
isNotify = articles.isNotEmpty() && feed.isNotification
)
}
private fun notify(
feedWithArticle: FeedWithArticle,
) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(
feedWithArticle.feed.id,
feedWithArticle.feed.name
)
)
feedWithArticle.articles.forEach { article ->
val builder = NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setContentTitle(article.title)
.setContentIntent(
PendingIntent.getActivity(
context,
Random().nextInt() + article.id.hashCode(),
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(
ExtraName.ARTICLE_ID,
article.id
)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setGroup(feedWithArticle.feed.id)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(article.shortDescription)
.setSummaryText(feedWithArticle.feed.name)
)
notificationManager.notify(
Random().nextInt() + article.id.hashCode(),
builder.build().apply {
flags = Notification.FLAG_AUTO_CANCEL
}
)
}
if (feedWithArticle.articles.size > 1) {
notificationManager.notify(
Random().nextInt() + feedWithArticle.feed.id.hashCode(),
NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setStyle(
NotificationCompat.InboxStyle()
.setSummaryText(feedWithArticle.feed.name)
)
.setGroup(feedWithArticle.feed.id)
.setGroupSummary(true)
.build()
)
}
}
}

View File

@ -0,0 +1,103 @@
package me.ash.reader.data.repository
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
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.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.*
import javax.inject.Inject
class NotificationHelper @Inject constructor(
@ApplicationContext
private val context: Context,
) {
private val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(context).apply {
createNotificationChannel(
NotificationChannel(
NotificationGroupName.ARTICLE_UPDATE,
NotificationGroupName.ARTICLE_UPDATE,
NotificationManager.IMPORTANCE_DEFAULT
)
)
}
fun notify(
feedWithArticle: FeedWithArticle,
) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(
feedWithArticle.feed.id,
feedWithArticle.feed.name
)
)
feedWithArticle.articles.forEach { article ->
val builder = NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setContentTitle(article.title)
.setContentIntent(
PendingIntent.getActivity(
context,
Random().nextInt() + article.id.hashCode(),
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(
ExtraName.ARTICLE_ID,
article.id
)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setGroup(feedWithArticle.feed.id)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(article.shortDescription)
.setSummaryText(feedWithArticle.feed.name)
)
notificationManager.notify(
Random().nextInt() + article.id.hashCode(),
builder.build().apply {
flags = Notification.FLAG_AUTO_CANCEL
}
)
}
if (feedWithArticle.articles.size > 1) {
notificationManager.notify(
Random().nextInt() + feedWithArticle.feed.id.hashCode(),
NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setStyle(
NotificationCompat.InboxStyle()
.setSummaryText(feedWithArticle.feed.name)
)
.setGroup(feedWithArticle.feed.id)
.setGroupSummary(true)
.build()
)
}
}
}

View File

@ -43,7 +43,7 @@ class OpmlRepository @Inject constructor(
repeatList.add(it)
}
}
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
feedDao.insertList((groupWithFeed.feeds subtract repeatList.toSet()).toList())
}
}

View File

@ -183,27 +183,4 @@ class RssHelper @Inject constructor(
}
)
}
private fun parseDate(
inputDate: String, patterns: Array<String> = arrayOf(
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm:ss",
"yyyyMMdd",
"yyyy/MM/dd",
"yyyy年MM月dd日",
"yyyy MM dd",
)
): Date? {
val df = SimpleDateFormat()
for (pattern in patterns) {
df.applyPattern(pattern)
df.isLenient = false
val date = df.parse(inputDate, ParsePosition(0))
if (date != null) {
return date
}
}
return null
}
}

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.repository
import android.content.Context
import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.*
import java.util.concurrent.TimeUnit
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val rssRepository: RssRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ")
return rssRepository.get().sync(this)
}
companion object {
const val WORK_NAME = "article.sync"
val UUID: UUID
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.build()
).addTag(WORK_NAME).build().also {
UUID = it.id
}
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
}
}

View File

@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.ash.reader.data.entity.LatestRelease
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
@ -15,12 +14,6 @@ import retrofit2.http.Streaming
import retrofit2.http.Url
import java.io.File
sealed class Download {
object NotYet : Download()
data class Progress(val percent: Int) : Download()
data class Finished(val file: File) : Download()
}
interface AppNetworkDataSource {
@GET
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
@ -93,3 +86,31 @@ fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
}
}
}.flowOn(Dispatchers.IO).distinctUntilChanged()
data class LatestRelease(
val html_url: String? = null,
val tag_name: String? = null,
val name: String? = null,
val draft: Boolean? = null,
val prerelease: Boolean? = null,
val created_at: String? = null,
val published_at: String? = null,
val assets: List<AssetsItem>? = null,
val body: String? = null,
)
data class AssetsItem(
val name: String? = null,
val content_type: String? = null,
val size: Int? = null,
val download_count: Int? = null,
val created_at: String? = null,
val updated_at: String? = null,
val browser_download_url: String? = null,
)
sealed class Download {
object NotYet : Download()
data class Progress(val percent: Int) : Download()
data class Finished(val file: File) : Download()
}

View File

@ -7,8 +7,8 @@ import android.content.Intent
import android.util.Log
import android.widget.Toast
import androidx.core.content.FileProvider
import me.ash.reader.data.entity.Version
import me.ash.reader.data.entity.toVersion
import me.ash.reader.data.model.Version
import me.ash.reader.data.model.toVersion
import java.io.File
fun Context.findActivity(): Activity? = when (this) {

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.core.os.ConfigurationCompat
import me.ash.reader.R
import java.text.DateFormat
import java.text.ParsePosition
import java.text.SimpleDateFormat
import java.util.*
@ -41,3 +42,26 @@ fun Date.formatAsString(
}
}
}
private fun String.parseToDate(
patterns: Array<String> = arrayOf(
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm:ss",
"yyyyMMdd",
"yyyy/MM/dd",
"yyyy年MM月dd日",
"yyyy MM dd",
)
): Date? {
val df = SimpleDateFormat()
for (pattern in patterns) {
df.applyPattern(pattern)
df.isLenient = false
val date = df.parse(this, ParsePosition(0))
if (date != null) {
return date
}
}
return null
}

View File

@ -1,13 +0,0 @@
package me.ash.reader.ui.ext
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
import me.ash.reader.data.entity.Filter
@Composable
fun Filter.getName(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread)
Filter.Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all)
}

View File

@ -0,0 +1,11 @@
@file:Suppress("SpellCheckingInspection")
package me.ash.reader.ui.ext
import me.ash.reader.BuildConfig
const val GITHUB = "github"
const val FDROID = "fdroid"
const val isFdroid = BuildConfig.FLAVOR == FDROID
const val notFdroid = !isFdroid

View File

@ -12,7 +12,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.LocalDarkTheme
import me.ash.reader.ui.ext.*
import me.ash.reader.ui.page.home.HomeViewAction

View File

@ -12,10 +12,10 @@ 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.entity.Filter
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.ui.ext.getName
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.theme.palette.onDark
@ -39,11 +39,7 @@ fun FilterBar(
tonalElevation = filterBarTonalElevation,
) {
Spacer(modifier = Modifier.width(filterBarPadding))
listOf(
Filter.Starred,
Filter.Unread,
Filter.All,
).forEach { item ->
Filter.values.forEach { item ->
NavigationBarItem(
// modifier = Modifier.height(60.dp),
alwaysShowLabel = when (filterBarStyle) {

View File

@ -7,7 +7,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.model.Filter
import me.ash.reader.data.entity.Group
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.repository.RssRepository

View File

@ -28,7 +28,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map
import me.ash.reader.R
import me.ash.reader.data.entity.toVersion
import me.ash.reader.data.model.getName
import me.ash.reader.data.model.toVersion
import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.Banner

View File

@ -25,13 +25,13 @@ import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.model.getName
import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.SwipeRefresh
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterBar

View File

@ -19,7 +19,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map
import me.ash.reader.R
import me.ash.reader.data.entity.toVersion
import me.ash.reader.data.model.toVersion
import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton

View File

@ -22,7 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.model.Filter
import me.ash.reader.data.entity.Group
import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.*

View File

@ -25,7 +25,7 @@ import me.ash.reader.R
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.*
import me.ash.reader.ui.ext.surfaceColorAtElevation

View File

@ -5,9 +5,9 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.BuildConfig
import me.ash.reader.data.repository.AppRepository
import me.ash.reader.data.source.Download
import me.ash.reader.ui.ext.notFdroid
import javax.inject.Inject
@HiltViewModel
@ -33,7 +33,7 @@ class UpdateViewModel @Inject constructor(
preProcessor: suspend () -> Unit = {},
postProcessor: suspend (Boolean) -> Unit = {}
) {
if (BuildConfig.FLAVOR != "fdroid") {
if (notFdroid) {
viewModelScope.launch {
preProcessor()
appRepository.checkUpdate().let {