Compare commits
28 Commits
predictive
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
74d0c50da7 | ||
|
54e022169a | ||
|
09d9f4d55b | ||
|
1ffdb2f4f7 | ||
|
39dc3bceee | ||
|
ec05bddba8 | ||
|
e0cd9acb03 | ||
|
133f196430 | ||
|
4caca8db39 | ||
|
99eca4b109 | ||
|
1826d2f948 | ||
|
94323b84a0 | ||
|
976678812e | ||
|
e55f2c976a | ||
|
368ed14801 | ||
|
3ba1eed774 | ||
|
6e55c12ca8 | ||
|
629f489ee3 | ||
|
76ec77a502 | ||
|
697e94eb88 | ||
|
47a911ae0d | ||
|
a89ccc7fd9 | ||
|
84a04d89ff | ||
|
0d514cd00a | ||
|
d34fa76f5b | ||
|
f36a24efcd | ||
|
3ab910582a | ||
|
82e06daea4 |
@ -32,10 +32,10 @@ android {
|
||||
applicationId = "me.ash.reader"
|
||||
minSdk = 26
|
||||
targetSdk = 33
|
||||
versionCode = 26
|
||||
versionName = "0.11.0"
|
||||
versionCode = 27
|
||||
versionName = "0.11.1"
|
||||
|
||||
buildConfigField("String", "USER_AGENT_STRING", "\"ReadYou/${'$'}{versionName}(${versionCode})\"")
|
||||
buildConfigField("String", "USER_AGENT_STRING", "\"ReadYou/${versionName}(${versionCode})\"")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@ -176,6 +176,12 @@ dependencies {
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.appcompat)
|
||||
|
||||
// Markdown
|
||||
implementation(libs.jetbrains.markdown)
|
||||
|
||||
// Nostr
|
||||
implementation(libs.rust.nostr)
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.junit.ext)
|
||||
|
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
@ -37,6 +37,14 @@
|
||||
# Provider API
|
||||
-keep class me.ash.reader.** { *; }
|
||||
|
||||
# Nostr
|
||||
-keep class com.sun.jna.** { *; }
|
||||
-keep class * implements com.sun.jna.** { *; }
|
||||
-dontwarn java.awt.Component
|
||||
-dontwarn java.awt.GraphicsEnvironment
|
||||
-dontwarn java.awt.HeadlessException
|
||||
-dontwarn java.awt.Window
|
||||
|
||||
# https://github.com/flutter/flutter/issues/127388
|
||||
-dontwarn org.kxml2.io.KXml**
|
||||
|
||||
|
@ -5,11 +5,13 @@ class FeverSecurityKey private constructor() : SecurityKey() {
|
||||
var serverUrl: String? = null
|
||||
var username: String? = null
|
||||
var password: String? = null
|
||||
var clientCertificateAlias: String? = null
|
||||
|
||||
constructor(serverUrl: String?, username: String?, password: String?) : this() {
|
||||
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
|
||||
this.serverUrl = serverUrl
|
||||
this.username = username
|
||||
this.password = password
|
||||
this.clientCertificateAlias = clientCertificateAlias
|
||||
}
|
||||
|
||||
constructor(value: String? = DESUtils.empty) : this() {
|
||||
@ -17,6 +19,7 @@ class FeverSecurityKey private constructor() : SecurityKey() {
|
||||
serverUrl = it.serverUrl
|
||||
username = it.username
|
||||
password = it.password
|
||||
clientCertificateAlias = it.clientCertificateAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
|
||||
var serverUrl: String? = null
|
||||
var username: String? = null
|
||||
var password: String? = null
|
||||
var clientCertificateAlias: String? = null
|
||||
|
||||
constructor(serverUrl: String?, username: String?, password: String?) : this() {
|
||||
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
|
||||
this.serverUrl = serverUrl
|
||||
this.username = username
|
||||
this.password = password
|
||||
this.clientCertificateAlias = clientCertificateAlias
|
||||
}
|
||||
|
||||
constructor(value: String? = DESUtils.empty) : this() {
|
||||
@ -17,6 +19,7 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
|
||||
serverUrl = it.serverUrl
|
||||
username = it.username
|
||||
password = it.password
|
||||
clientCertificateAlias = it.clientCertificateAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
|
||||
var serverUrl: String? = null
|
||||
var username: String? = null
|
||||
var password: String? = null
|
||||
var clientCertificateAlias: String? = null
|
||||
|
||||
constructor(serverUrl: String?, username: String?, password: String?) : this() {
|
||||
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
|
||||
this.serverUrl = serverUrl
|
||||
this.username = username
|
||||
this.password = password
|
||||
this.clientCertificateAlias = clientCertificateAlias
|
||||
}
|
||||
|
||||
constructor(value: String? = DESUtils.empty) : this() {
|
||||
@ -17,6 +19,7 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
|
||||
serverUrl = it.serverUrl
|
||||
username = it.username
|
||||
password = it.password
|
||||
clientCertificateAlias = it.clientCertificateAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import androidx.paging.PagingSource
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkManager
|
||||
import com.rometools.rome.feed.synd.SyndFeed
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import me.ash.reader.domain.model.account.Account
|
||||
import me.ash.reader.domain.model.article.ArticleWithFeed
|
||||
import me.ash.reader.domain.model.feed.Feed
|
||||
@ -28,9 +30,13 @@ import me.ash.reader.domain.repository.GroupDao
|
||||
import me.ash.reader.infrastructure.android.NotificationHelper
|
||||
import me.ash.reader.infrastructure.preference.KeepArchivedPreference
|
||||
import me.ash.reader.infrastructure.preference.SyncIntervalPreference
|
||||
import me.ash.reader.infrastructure.rss.FetchedFeed
|
||||
import me.ash.reader.infrastructure.rss.NostrFeed
|
||||
import me.ash.reader.infrastructure.rss.RssHelper
|
||||
import me.ash.reader.infrastructure.rss.SyndFeedDelegate
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.ext.decodeHTML
|
||||
import me.ash.reader.ui.ext.isNostrUri
|
||||
import me.ash.reader.ui.ext.spacerDollar
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
@ -59,19 +65,26 @@ abstract class AbstractRssRepository(
|
||||
open suspend fun clearAuthorization() {}
|
||||
|
||||
open suspend fun subscribe(
|
||||
feedLink: String, searchedFeed: SyndFeed, groupId: String,
|
||||
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
|
||||
isNotification: Boolean, isFullContent: Boolean
|
||||
) {
|
||||
val accountId = context.currentAccountId
|
||||
val feed = Feed(
|
||||
id = accountId.spacerDollar(UUID.randomUUID().toString()),
|
||||
name = searchedFeed.title.decodeHTML()!!,
|
||||
name = with(searchedFeed.title){ if (this.isNostrUri()) this else this.decodeHTML()!!},
|
||||
url = feedLink,
|
||||
groupId = groupId,
|
||||
accountId = accountId,
|
||||
icon = searchedFeed.icon?.link
|
||||
icon = searchedFeed.getIconLink()
|
||||
)
|
||||
val articles = searchedFeed.entries.map { rssHelper.buildArticleFromSyndEntry(feed, accountId, it) }
|
||||
val articles = when(searchedFeed) {
|
||||
is NostrFeed -> searchedFeed.getArticles().map {
|
||||
rssHelper.buildArticleFromNostrEvent(feed, accountId, it, searchedFeed.getFeedAuthor())
|
||||
}
|
||||
is SyndFeedDelegate -> searchedFeed.getArticles().map {
|
||||
rssHelper.buildArticleFromSyndEntry(feed, accountId, it)
|
||||
}
|
||||
}
|
||||
feedDao.insert(feed)
|
||||
articleDao.insertList(articles.map {
|
||||
it.copy(feedId = feed.id)
|
||||
@ -101,21 +114,18 @@ abstract class AbstractRssRepository(
|
||||
val preTime = System.currentTimeMillis()
|
||||
val preDate = Date(preTime)
|
||||
val accountId = context.currentAccountId
|
||||
feedDao.queryAll(accountId)
|
||||
.chunked(16)
|
||||
.forEach {
|
||||
it.map { feed -> async { syncFeed(feed, preDate) } }
|
||||
.awaitAll()
|
||||
.forEach {
|
||||
if (it.feed.isNotification) {
|
||||
notificationHelper.notify(it.apply {
|
||||
articles = articleDao.insertListIfNotExist(it.articles)
|
||||
})
|
||||
} else {
|
||||
articleDao.insertListIfNotExist(it.articles)
|
||||
}
|
||||
val semaphore = Semaphore(16)
|
||||
feedDao.queryAll(accountId).mapIndexed { _, feed ->
|
||||
async(Dispatchers.IO) {
|
||||
semaphore.withPermit {
|
||||
val feedWithArticle = syncFeed(feed, preDate)
|
||||
val newArticles = articleDao.insertListIfNotExist(feedWithArticle.articles)
|
||||
if (feedWithArticle.feed.isNotification) {
|
||||
notificationHelper.notify(feedWithArticle.copy(articles = newArticles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
||||
accountDao.queryById(accountId)?.let { account ->
|
||||
@ -177,17 +187,29 @@ abstract class AbstractRssRepository(
|
||||
|
||||
private suspend fun syncFeed(feed: Feed, preDate: Date = Date()): FeedWithArticle {
|
||||
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
||||
val articles = rssHelper.queryRssXml(feed, "", preDate)
|
||||
if (feed.icon == null) {
|
||||
val iconLink = rssHelper.queryRssIconLink(feed.url)
|
||||
if (iconLink != null) {
|
||||
rssHelper.saveRssIcon(feedDao, feed, iconLink)
|
||||
}
|
||||
if (feed.url.isNostrUri()) {
|
||||
val syncedFeed = rssHelper.syncNostrFeed(feed, "", preDate)
|
||||
return FeedWithArticle(
|
||||
feed = syncedFeed.feed
|
||||
.apply { isNotification = feed.isNotification && syncedFeed.articles.isNotEmpty() },
|
||||
articles = syncedFeed.articles
|
||||
)
|
||||
}
|
||||
return FeedWithArticle(
|
||||
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
|
||||
articles = articles
|
||||
)
|
||||
else {
|
||||
val articles = rssHelper.queryRssXml(feed, "", preDate)
|
||||
if (feed.icon == null) {
|
||||
val iconLink = rssHelper.queryRssIconLink(feed.url)
|
||||
if (iconLink != null) {
|
||||
rssHelper.saveRssIcon(feedDao, feed, iconLink)
|
||||
}
|
||||
}
|
||||
|
||||
return FeedWithArticle(
|
||||
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
|
||||
articles = articles
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun clearKeepArchivedArticles() {
|
||||
@ -200,39 +222,29 @@ abstract class AbstractRssRepository(
|
||||
}
|
||||
|
||||
fun cancelSync() {
|
||||
workManager.cancelAllWork()
|
||||
SyncWorker.cancelPeriodicWork(workManager)
|
||||
SyncWorker.cancelOneTimeWork(workManager)
|
||||
}
|
||||
|
||||
fun doSyncOneTime() {
|
||||
workManager.cancelAllWork()
|
||||
SyncWorker.enqueueOneTimeWork(workManager)
|
||||
}
|
||||
|
||||
suspend fun doSync(isOnStart: Boolean = false) {
|
||||
workManager.cancelAllWork()
|
||||
suspend fun initSync() {
|
||||
accountDao.queryById(context.currentAccountId)?.let {
|
||||
if (isOnStart) {
|
||||
if (it.syncOnStart.value) {
|
||||
SyncWorker.enqueueOneTimeWork(workManager)
|
||||
}
|
||||
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
|
||||
SyncWorker.enqueuePeriodicWork(
|
||||
workManager = workManager,
|
||||
syncInterval = it.syncInterval,
|
||||
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
|
||||
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
|
||||
)
|
||||
}
|
||||
val syncOnStart = it.syncOnStart.value
|
||||
if (syncOnStart) {
|
||||
doSyncOneTime()
|
||||
}
|
||||
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
|
||||
SyncWorker.enqueuePeriodicWork(
|
||||
workManager = workManager,
|
||||
syncInterval = it.syncInterval,
|
||||
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
|
||||
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
|
||||
)
|
||||
} else {
|
||||
SyncWorker.enqueueOneTimeWork(workManager)
|
||||
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
|
||||
SyncWorker.enqueuePeriodicWork(
|
||||
workManager = workManager,
|
||||
syncInterval = it.syncInterval,
|
||||
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
|
||||
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
|
||||
)
|
||||
}
|
||||
SyncWorker.cancelPeriodicWork(workManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import android.util.Log
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkManager
|
||||
import com.rometools.rome.feed.synd.SyndFeed
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
@ -27,6 +26,7 @@ import me.ash.reader.infrastructure.di.IODispatcher
|
||||
import me.ash.reader.infrastructure.di.MainDispatcher
|
||||
import me.ash.reader.infrastructure.exception.FeverAPIException
|
||||
import me.ash.reader.infrastructure.html.Readability
|
||||
import me.ash.reader.infrastructure.rss.FetchedFeed
|
||||
import me.ash.reader.infrastructure.rss.RssHelper
|
||||
import me.ash.reader.infrastructure.rss.provider.fever.FeverAPI
|
||||
import me.ash.reader.infrastructure.rss.provider.fever.FeverDTO
|
||||
@ -70,11 +70,13 @@ class FeverRssService @Inject constructor(
|
||||
private suspend fun getFeverAPI() =
|
||||
FeverSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
||||
FeverAPI.getInstance(
|
||||
context = context,
|
||||
serverUrl = serverUrl!!,
|
||||
username = username!!,
|
||||
password = password!!,
|
||||
httpUsername = null,
|
||||
httpPassword = null,
|
||||
clientCertificateAlias = clientCertificateAlias,
|
||||
)
|
||||
}
|
||||
|
||||
@ -86,7 +88,7 @@ class FeverRssService @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun subscribe(
|
||||
feedLink: String, searchedFeed: SyndFeed, groupId: String,
|
||||
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
|
||||
isNotification: Boolean, isFullContent: Boolean,
|
||||
) {
|
||||
throw FeverAPIException("Unsupported")
|
||||
|
@ -5,7 +5,6 @@ import android.util.Log
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkManager
|
||||
import com.rometools.rome.feed.synd.SyndFeed
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
@ -25,6 +24,7 @@ import me.ash.reader.infrastructure.di.DefaultDispatcher
|
||||
import me.ash.reader.infrastructure.di.IODispatcher
|
||||
import me.ash.reader.infrastructure.di.MainDispatcher
|
||||
import me.ash.reader.infrastructure.html.Readability
|
||||
import me.ash.reader.infrastructure.rss.FetchedFeed
|
||||
import me.ash.reader.infrastructure.rss.RssHelper
|
||||
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI
|
||||
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofCategoryIdToStreamId
|
||||
@ -72,11 +72,13 @@ class GoogleReaderRssService @Inject constructor(
|
||||
private suspend fun getGoogleReaderAPI() =
|
||||
GoogleReaderSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
||||
GoogleReaderAPI.getInstance(
|
||||
context = context,
|
||||
serverUrl = serverUrl!!,
|
||||
username = username!!,
|
||||
password = password!!,
|
||||
httpUsername = null,
|
||||
httpPassword = null,
|
||||
clientCertificateAlias = clientCertificateAlias,
|
||||
)
|
||||
}
|
||||
|
||||
@ -97,7 +99,7 @@ class GoogleReaderRssService @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun subscribe(
|
||||
feedLink: String, searchedFeed: SyndFeed, groupId: String,
|
||||
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
|
||||
isNotification: Boolean, isFullContent: Boolean,
|
||||
) {
|
||||
val accountId = context.currentAccountId
|
||||
|
@ -33,15 +33,25 @@ class SyncWorker @AssistedInject constructor(
|
||||
companion object {
|
||||
|
||||
private const val IS_SYNCING = "isSyncing"
|
||||
const val WORK_NAME = "ReadYou"
|
||||
lateinit var uuid: UUID
|
||||
private const val WORK_NAME_PERIODIC = "ReadYou"
|
||||
private const val WORK_NAME_ONETIME = "SYNC_ONETIME"
|
||||
const val WORK_TAG = "SYNC_TAG"
|
||||
|
||||
fun cancelOneTimeWork(workManager: WorkManager) {
|
||||
workManager.cancelUniqueWork(WORK_NAME_ONETIME)
|
||||
}
|
||||
|
||||
fun cancelPeriodicWork(workManager: WorkManager) {
|
||||
workManager.cancelUniqueWork(WORK_NAME_PERIODIC)
|
||||
}
|
||||
|
||||
fun enqueueOneTimeWork(
|
||||
workManager: WorkManager,
|
||||
) {
|
||||
workManager.enqueue(OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.addTag(WORK_NAME)
|
||||
.build()
|
||||
workManager.enqueueUniqueWork(
|
||||
WORK_NAME_ONETIME,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
OneTimeWorkRequestBuilder<SyncWorker>().addTag(WORK_TAG).build()
|
||||
)
|
||||
}
|
||||
|
||||
@ -52,15 +62,16 @@ class SyncWorker @AssistedInject constructor(
|
||||
syncOnlyOnWiFi: SyncOnlyOnWiFiPreference,
|
||||
) {
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
WORK_NAME_PERIODIC,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
PeriodicWorkRequestBuilder<SyncWorker>(syncInterval.value, TimeUnit.MINUTES)
|
||||
.setConstraints(Constraints.Builder()
|
||||
.setRequiresCharging(syncOnlyWhenCharging.value)
|
||||
.setRequiredNetworkType(if (syncOnlyOnWiFi.value) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||
.build()
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiresCharging(syncOnlyWhenCharging.value)
|
||||
.setRequiredNetworkType(if (syncOnlyOnWiFi.value) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.addTag(WORK_NAME)
|
||||
.addTag(WORK_TAG)
|
||||
.setInitialDelay(syncInterval.value, TimeUnit.MINUTES)
|
||||
.build()
|
||||
)
|
||||
|
@ -22,6 +22,7 @@ import me.ash.reader.ui.ext.del
|
||||
import me.ash.reader.ui.ext.getLatestApk
|
||||
import me.ash.reader.ui.ext.isGitHub
|
||||
import okhttp3.OkHttpClient
|
||||
import rust.nostr.sdk.Client
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -58,6 +59,9 @@ class AndroidApp : Application(), Configuration.Provider {
|
||||
@Inject
|
||||
lateinit var rssHelper: RssHelper
|
||||
|
||||
@Inject
|
||||
lateinit var nostrClient: Client
|
||||
|
||||
@Inject
|
||||
lateinit var notificationHelper: NotificationHelper
|
||||
|
||||
@ -133,7 +137,7 @@ class AndroidApp : Application(), Configuration.Provider {
|
||||
}
|
||||
|
||||
private suspend fun workerInit() {
|
||||
rssService.get().doSync(isOnStart = true)
|
||||
rssService.get().initSync()
|
||||
}
|
||||
|
||||
private suspend fun checkUpdate() {
|
||||
|
@ -0,0 +1,19 @@
|
||||
package me.ash.reader.infrastructure.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import rust.nostr.sdk.Client
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NostrClientModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNostrClient(): Client {
|
||||
return Client()
|
||||
}
|
||||
}
|
@ -20,7 +20,9 @@
|
||||
|
||||
package me.ash.reader.infrastructure.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.security.KeyChain
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@ -31,15 +33,18 @@ import okhttp3.Cache
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.platform.Platform
|
||||
import java.io.File
|
||||
import java.net.Socket
|
||||
import java.security.KeyManagementException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.Principal
|
||||
import java.security.PrivateKey
|
||||
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.X509KeyManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
@ -54,6 +59,7 @@ object OkHttpClientModule {
|
||||
fun provideOkHttpClient(
|
||||
@ApplicationContext context: Context,
|
||||
): OkHttpClient = cachingHttpClient(
|
||||
context = context,
|
||||
cacheDirectory = context.cacheDir.resolve("http")
|
||||
).newBuilder()
|
||||
.addNetworkInterceptor(UserAgentInterceptor)
|
||||
@ -61,11 +67,13 @@ object OkHttpClientModule {
|
||||
}
|
||||
|
||||
fun cachingHttpClient(
|
||||
context: Context,
|
||||
cacheDirectory: File? = null,
|
||||
cacheSize: Long = 10L * 1024L * 1024L,
|
||||
trustAllCerts: Boolean = true,
|
||||
connectTimeoutSecs: Long = 30L,
|
||||
readTimeoutSecs: Long = 30L,
|
||||
clientCertificateAlias: String? = null,
|
||||
): OkHttpClient {
|
||||
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
|
||||
|
||||
@ -78,31 +86,75 @@ fun cachingHttpClient(
|
||||
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
|
||||
if (trustAllCerts) {
|
||||
builder.trustAllCerts()
|
||||
if (!clientCertificateAlias.isNullOrBlank() || trustAllCerts) {
|
||||
builder.setupSsl(context, clientCertificateAlias, trustAllCerts)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.trustAllCerts() {
|
||||
fun OkHttpClient.Builder.setupSsl(
|
||||
context: Context,
|
||||
clientCertificateAlias: String?,
|
||||
trustAllCerts: Boolean
|
||||
) {
|
||||
try {
|
||||
val trustManager = object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
}
|
||||
val clientKeyManager = clientCertificateAlias?.let { clientAlias ->
|
||||
object : X509KeyManager {
|
||||
override fun getClientAliases(keyType: String?, issuers: Array<Principal>?) =
|
||||
throw UnsupportedOperationException("getClientAliases")
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
}
|
||||
override fun chooseClientAlias(
|
||||
keyType: Array<String>?,
|
||||
issuers: Array<Principal>?,
|
||||
socket: Socket?
|
||||
) = clientCertificateAlias
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
override fun getServerAliases(keyType: String?, issuers: Array<Principal>?) =
|
||||
throw UnsupportedOperationException("getServerAliases")
|
||||
|
||||
override fun chooseServerAlias(
|
||||
keyType: String?,
|
||||
issuers: Array<Principal>?,
|
||||
socket: Socket?
|
||||
) = throw UnsupportedOperationException("chooseServerAlias")
|
||||
|
||||
override fun getCertificateChain(alias: String?): Array<X509Certificate>? {
|
||||
return if (alias == clientAlias) KeyChain.getCertificateChain(context, clientAlias) else null
|
||||
}
|
||||
|
||||
override fun getPrivateKey(alias: String?): PrivateKey? {
|
||||
return if (alias == clientAlias) KeyChain.getPrivateKey(context, clientAlias) else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val trustManager = if (trustAllCerts) {
|
||||
hostnameVerifier { _, _ -> true }
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
object : X509TrustManager {
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<out X509Certificate>?,
|
||||
authType: String?
|
||||
) = Unit
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<out X509Certificate>?,
|
||||
authType: String?
|
||||
) = Unit
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
} else {
|
||||
Platform.get().platformTrustManager()
|
||||
}
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
|
||||
sslContext.init(arrayOf(clientKeyManager), arrayOf(trustManager), null)
|
||||
val sslSocketFactory = sslContext.socketFactory
|
||||
|
||||
sslSocketFactory(sslSocketFactory, trustManager)
|
||||
.hostnameVerifier(HostnameVerifier { _, _ -> true })
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
// ignore
|
||||
} catch (e: KeyManagementException) {
|
||||
|
@ -15,12 +15,8 @@ val LocalFlowTopBarTonalElevation =
|
||||
compositionLocalOf<FlowTopBarTonalElevationPreference> { FlowTopBarTonalElevationPreference.default }
|
||||
|
||||
sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||
object Level0 : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Level1 : FlowTopBarTonalElevationPreference(ElevationTokens.Level1)
|
||||
object Level2 : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
|
||||
object Level3 : FlowTopBarTonalElevationPreference(ElevationTokens.Level3)
|
||||
object Level4 : FlowTopBarTonalElevationPreference(ElevationTokens.Level4)
|
||||
object Level5 : FlowTopBarTonalElevationPreference(ElevationTokens.Level5)
|
||||
object None : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Elevated : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
@ -30,27 +26,19 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||
None -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Elevated -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Level0
|
||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
||||
val default = Elevated
|
||||
val values = listOf(None, Elevated)
|
||||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKey.keys[flowTopBarTonalElevation]?.key as Preferences.Key<Int>]) {
|
||||
ElevationTokens.Level0 -> Level0
|
||||
ElevationTokens.Level1 -> Level1
|
||||
ElevationTokens.Level2 -> Level2
|
||||
ElevationTokens.Level3 -> Level3
|
||||
ElevationTokens.Level4 -> Level4
|
||||
ElevationTokens.Level5 -> Level5
|
||||
ElevationTokens.Level0 -> None
|
||||
ElevationTokens.Level2 -> Elevated
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
package me.ash.reader.infrastructure.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.ui.ext.DataStoreKey
|
||||
import me.ash.reader.ui.ext.DataStoreKey.Companion.hideEmptyGroups
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
val LocalHideEmptyGroups =
|
||||
compositionLocalOf<HideEmptyGroupsPreference> { HideEmptyGroupsPreference.default }
|
||||
|
||||
sealed class HideEmptyGroupsPreference(val value: Boolean) : Preference() {
|
||||
data object ON : HideEmptyGroupsPreference(true)
|
||||
data object OFF : HideEmptyGroupsPreference(false)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
context.dataStore.put(
|
||||
hideEmptyGroups,
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle(context: Context, scope: CoroutineScope) = scope.launch {
|
||||
context.dataStore.put(
|
||||
hideEmptyGroups,
|
||||
!value
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = ON
|
||||
val values = listOf(ON, OFF)
|
||||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKey.keys[hideEmptyGroups]?.key as Preferences.Key<Boolean>]) {
|
||||
true -> ON
|
||||
false -> OFF
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
}
|
@ -56,7 +56,6 @@ fun Preferences.toSettings(): Settings {
|
||||
readingRenderer = ReadingRendererPreference.fromPreferences(this),
|
||||
readingBionicReading = ReadingBionicReadingPreference.fromPreferences(this),
|
||||
readingTheme = ReadingThemePreference.fromPreferences(this),
|
||||
readingDarkTheme = ReadingDarkThemePreference.fromPreferences(this),
|
||||
readingPageTonalElevation = ReadingPageTonalElevationPreference.fromPreferences(this),
|
||||
readingAutoHideToolbar = ReadingAutoHideToolbarPreference.fromPreferences(this),
|
||||
readingTextFontSize = ReadingTextFontSizePreference.fromPreferences(this),
|
||||
@ -82,6 +81,7 @@ fun Preferences.toSettings(): Settings {
|
||||
swipeStartAction = SwipeStartActionPreference.fromPreferences(this),
|
||||
swipeEndAction = SwipeEndActionPreference.fromPreferences(this),
|
||||
markAsReadOnScroll = MarkAsReadOnScrollPreference.fromPreferences(this),
|
||||
hideEmptyGroups = HideEmptyGroupsPreference.fromPreferences(this),
|
||||
pullToSwitchArticle = PullToSwitchArticlePreference.fromPreference(this),
|
||||
openLink = OpenLinkPreference.fromPreferences(this),
|
||||
openLinkSpecificBrowser = OpenLinkSpecificBrowserPreference.fromPreferences(this),
|
||||
|
@ -1,77 +0,0 @@
|
||||
package me.ash.reader.infrastructure.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.ui.ext.DataStoreKey
|
||||
import me.ash.reader.ui.ext.DataStoreKey.Companion.readingDarkTheme
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
val LocalReadingDarkTheme =
|
||||
compositionLocalOf<ReadingDarkThemePreference> { ReadingDarkThemePreference.default }
|
||||
|
||||
sealed class ReadingDarkThemePreference(val value: Int) : Preference() {
|
||||
object UseAppTheme : ReadingDarkThemePreference(0)
|
||||
object ON : ReadingDarkThemePreference(1)
|
||||
object OFF : ReadingDarkThemePreference(2)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
context.dataStore.put(
|
||||
DataStoreKey.readingDarkTheme,
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
UseAppTheme -> context.getString(R.string.use_app_theme)
|
||||
ON -> context.getString(R.string.on)
|
||||
OFF -> context.getString(R.string.off)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun isDarkTheme(): Boolean = when (this) {
|
||||
UseAppTheme -> LocalDarkTheme.current.isDarkTheme()
|
||||
ON -> true
|
||||
OFF -> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = UseAppTheme
|
||||
val values = listOf(UseAppTheme, ON, OFF)
|
||||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKey.keys[readingDarkTheme]?.key as Preferences.Key<Int>]) {
|
||||
0 -> UseAppTheme
|
||||
1 -> ON
|
||||
2 -> OFF
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
operator fun ReadingDarkThemePreference.not(): ReadingDarkThemePreference =
|
||||
when (this) {
|
||||
ReadingDarkThemePreference.UseAppTheme -> if (LocalDarkTheme.current.isDarkTheme()) {
|
||||
ReadingDarkThemePreference.OFF
|
||||
} else {
|
||||
ReadingDarkThemePreference.ON
|
||||
}
|
||||
|
||||
ReadingDarkThemePreference.ON -> ReadingDarkThemePreference.OFF
|
||||
ReadingDarkThemePreference.OFF -> ReadingDarkThemePreference.ON
|
||||
}
|
@ -15,42 +15,30 @@ val LocalReadingPageTonalElevation =
|
||||
compositionLocalOf<ReadingPageTonalElevationPreference> { ReadingPageTonalElevationPreference.default }
|
||||
|
||||
sealed class ReadingPageTonalElevationPreference(val value: Int) : Preference() {
|
||||
object Level0 : ReadingPageTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Level1 : ReadingPageTonalElevationPreference(ElevationTokens.Level1)
|
||||
object Level2 : ReadingPageTonalElevationPreference(ElevationTokens.Level2)
|
||||
object Level3 : ReadingPageTonalElevationPreference(ElevationTokens.Level3)
|
||||
object Level4 : ReadingPageTonalElevationPreference(ElevationTokens.Level4)
|
||||
object Level5 : ReadingPageTonalElevationPreference(ElevationTokens.Level5)
|
||||
data object Outlined : ReadingPageTonalElevationPreference(ElevationTokens.Level0)
|
||||
data object Elevated : ReadingPageTonalElevationPreference(ElevationTokens.Level2)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
context.dataStore.put(DataStoreKey.readingPageTonalElevation, value)
|
||||
context.dataStore.put(readingPageTonalElevation, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||
Outlined -> "${ElevationTokens.Level0}dp"
|
||||
Elevated -> "${ElevationTokens.Level2}dp"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = Level0
|
||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
||||
val default = Outlined
|
||||
val values = listOf(Outlined, Elevated)
|
||||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKey.keys[readingPageTonalElevation]?.key as Preferences.Key<Int>]) {
|
||||
ElevationTokens.Level0 -> Level0
|
||||
ElevationTokens.Level1 -> Level1
|
||||
ElevationTokens.Level2 -> Level2
|
||||
ElevationTokens.Level3 -> Level3
|
||||
ElevationTokens.Level4 -> Level4
|
||||
ElevationTokens.Level5 -> Level5
|
||||
ElevationTokens.Level0 -> Outlined
|
||||
ElevationTokens.Level2 -> Elevated
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,6 @@ data class Settings(
|
||||
val readingRenderer: ReadingRendererPreference = ReadingRendererPreference.default,
|
||||
val readingBionicReading: ReadingBionicReadingPreference = ReadingBionicReadingPreference.default,
|
||||
val readingTheme: ReadingThemePreference = ReadingThemePreference.default,
|
||||
val readingDarkTheme: ReadingDarkThemePreference = ReadingDarkThemePreference.default,
|
||||
val readingPageTonalElevation: ReadingPageTonalElevationPreference = ReadingPageTonalElevationPreference.default,
|
||||
val readingAutoHideToolbar: ReadingAutoHideToolbarPreference = ReadingAutoHideToolbarPreference.default,
|
||||
val readingTextFontSize: Int = ReadingTextFontSizePreference.default,
|
||||
@ -80,6 +79,7 @@ data class Settings(
|
||||
val swipeStartAction: SwipeStartActionPreference = SwipeStartActionPreference.default,
|
||||
val swipeEndAction: SwipeEndActionPreference = SwipeEndActionPreference.default,
|
||||
val markAsReadOnScroll: MarkAsReadOnScrollPreference = MarkAsReadOnScrollPreference.default,
|
||||
val hideEmptyGroups: HideEmptyGroupsPreference = HideEmptyGroupsPreference.default,
|
||||
val pullToSwitchArticle: PullToSwitchArticlePreference = PullToSwitchArticlePreference.default,
|
||||
val openLink: OpenLinkPreference = OpenLinkPreference.default,
|
||||
val openLinkSpecificBrowser: OpenLinkSpecificBrowserPreference = OpenLinkSpecificBrowserPreference.default,
|
||||
@ -146,7 +146,6 @@ fun SettingsProvider(
|
||||
LocalReadingRenderer provides settings.readingRenderer,
|
||||
LocalReadingBionicReading provides settings.readingBionicReading,
|
||||
LocalReadingTheme provides settings.readingTheme,
|
||||
LocalReadingDarkTheme provides settings.readingDarkTheme,
|
||||
LocalReadingPageTonalElevation provides settings.readingPageTonalElevation,
|
||||
LocalReadingAutoHideToolbar provides settings.readingAutoHideToolbar,
|
||||
LocalReadingTextFontSize provides settings.readingTextFontSize,
|
||||
@ -172,6 +171,7 @@ fun SettingsProvider(
|
||||
LocalArticleListSwipeStartAction provides settings.swipeStartAction,
|
||||
LocalArticleListSwipeEndAction provides settings.swipeEndAction,
|
||||
LocalMarkAsReadOnScroll provides settings.markAsReadOnScroll,
|
||||
LocalHideEmptyGroups provides settings.hideEmptyGroups,
|
||||
LocalPullToSwitchArticle provides settings.pullToSwitchArticle,
|
||||
LocalOpenLink provides settings.openLink,
|
||||
LocalOpenLinkSpecificBrowser provides settings.openLinkSpecificBrowser,
|
||||
|
@ -0,0 +1,279 @@
|
||||
package me.ash.reader.infrastructure.rss
|
||||
|
||||
import android.util.Log
|
||||
import com.rometools.rome.feed.synd.SyndEntry
|
||||
import com.rometools.rome.feed.synd.SyndFeed
|
||||
import rust.nostr.sdk.Client
|
||||
import rust.nostr.sdk.Event
|
||||
import rust.nostr.sdk.Filter
|
||||
import rust.nostr.sdk.Kind
|
||||
import rust.nostr.sdk.KindEnum
|
||||
import rust.nostr.sdk.Nip19Profile
|
||||
import rust.nostr.sdk.Nip21
|
||||
import rust.nostr.sdk.Nip21Enum
|
||||
import rust.nostr.sdk.NostrSdkException
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import rust.nostr.sdk.RelayMetadata
|
||||
import rust.nostr.sdk.TagKind
|
||||
import rust.nostr.sdk.extractRelayList
|
||||
import rust.nostr.sdk.getNip05Profile
|
||||
import java.time.Duration
|
||||
|
||||
sealed interface FetchedFeed {
|
||||
fun getIconLink(): String
|
||||
//The function below is for compatibility with SyndFeed
|
||||
fun getIconUrl(): String
|
||||
fun getFeedLink(): String
|
||||
var title: String
|
||||
fun getFeedAuthor(): String
|
||||
fun getArticles(): List<*>
|
||||
}
|
||||
|
||||
class SyndFeedDelegate(
|
||||
private val syndFeed: SyndFeed
|
||||
): FetchedFeed {
|
||||
|
||||
override fun getIconLink(): String {
|
||||
return syndFeed.icon.link
|
||||
}
|
||||
|
||||
override fun getIconUrl(): String {
|
||||
return syndFeed.icon.url
|
||||
}
|
||||
|
||||
override fun getFeedLink(): String {
|
||||
return syndFeed.link
|
||||
}
|
||||
|
||||
override var title: String
|
||||
get() = syndFeed.title
|
||||
set(value) {
|
||||
syndFeed.title = value
|
||||
}
|
||||
|
||||
override fun getFeedAuthor(): String {
|
||||
return syndFeed.author
|
||||
}
|
||||
|
||||
override fun getArticles(): List<SyndEntry> {
|
||||
return syndFeed.entries
|
||||
}
|
||||
}
|
||||
|
||||
class NostrFeed(
|
||||
private val nostrClient: Client
|
||||
): FetchedFeed {
|
||||
private val LOG_TAG = "ReadYou"
|
||||
private lateinit var feedFetchResult: NostrFeedResult
|
||||
|
||||
// The default relays to get info from, separated by purpose.
|
||||
private val defaultFetchRelays = listOf("wss://relay.nostr.band", "wss://relay.damus.io")
|
||||
private val defaultMetadataRelays = listOf("wss://purplepag.es", "wss://user.kindpag.es")
|
||||
private val defaultArticleFetchRelays = setOf("wss://nos.lol") + defaultFetchRelays
|
||||
|
||||
override fun getIconLink(): String {
|
||||
return feedFetchResult.authorPictureLink
|
||||
}
|
||||
|
||||
override fun getIconUrl(): String {
|
||||
return feedFetchResult.authorPictureLink
|
||||
}
|
||||
|
||||
override fun getFeedLink(): String {
|
||||
return feedFetchResult.nostrUri
|
||||
}
|
||||
|
||||
override var title: String
|
||||
get() = feedFetchResult.feedTitle
|
||||
set(value) {
|
||||
feedFetchResult.feedTitle = value
|
||||
}
|
||||
|
||||
override fun getFeedAuthor(): String {
|
||||
return feedFetchResult.authorName
|
||||
}
|
||||
|
||||
override fun getArticles(): List<Event> {
|
||||
return feedFetchResult.articles
|
||||
}
|
||||
|
||||
private suspend fun nreq(nostrUri: String): NostrFeedResult {
|
||||
|
||||
val profile = getProfileMetadata(nostrUri)
|
||||
val publishRelays = getUserPublishRelays(profile.publicKey)
|
||||
|
||||
val articles = fetchArticlesForAuthor(
|
||||
profile.publicKey,
|
||||
publishRelays
|
||||
)
|
||||
return NostrFeedResult(
|
||||
nostrUri = nostrUri,
|
||||
authorName = profile.name,
|
||||
feedTitle = profile.name,
|
||||
authorPictureLink = profile.imageUrl,
|
||||
articles = articles
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun parseNostrUri(nostrUri: String): Nip19Profile {
|
||||
if (nostrUri.contains("@")) { // It means it is a Nip05 address
|
||||
val rawString = nostrUri.removePrefix("nostr:")
|
||||
val parsedNip5 = getNip05Profile(rawString)
|
||||
val (pubkey, relays) = parsedNip5.publicKey() to parsedNip5.relays()
|
||||
return Nip19Profile(pubkey, relays)
|
||||
} else {
|
||||
val parsedProfile = Nip21.parse(nostrUri).asEnum()
|
||||
when(parsedProfile) {
|
||||
is Nip21Enum.Pubkey -> return Nip19Profile(parsedProfile.publicKey)
|
||||
is Nip21Enum.Profile -> return Nip19Profile(parsedProfile.profile.publicKey(), parsedProfile.profile.relays())
|
||||
else -> throw Throwable(message = "Could not find the user's info: $nostrUri")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getProfileMetadata(nostrUri: String): AuthorNostrData {
|
||||
val possibleNostrProfile = parseNostrUri(nostrUri)
|
||||
val publicKey = possibleNostrProfile.publicKey()
|
||||
val relayList =
|
||||
possibleNostrProfile.relays()
|
||||
.takeIf {
|
||||
it.size < 4
|
||||
}.orEmpty()
|
||||
.ifEmpty { getUserPublishRelays(publicKey) }
|
||||
Log.d(LOG_TAG, "getProfileMetadata: Relays from Nip19 -> ${relayList.joinToString(separator = ", ")}")
|
||||
relayList
|
||||
.ifEmpty { defaultFetchRelays }
|
||||
.forEach { relayUrl ->
|
||||
nostrClient.addReadRelay(relayUrl)
|
||||
}
|
||||
nostrClient.connect()
|
||||
val profileInfo =
|
||||
try {
|
||||
nostrClient.fetchMetadata(
|
||||
publicKey = publicKey,
|
||||
timeout = Duration.ofSeconds(5L),
|
||||
)
|
||||
} catch (e: NostrSdkException) {
|
||||
// We will use a default relay regardless of whether it is added above, to keep things simple.
|
||||
nostrClient.addReadRelay(defaultFetchRelays.random())
|
||||
nostrClient.connect()
|
||||
nostrClient.fetchMetadata(
|
||||
publicKey = publicKey,
|
||||
timeout = Duration.ofSeconds(5L),
|
||||
)
|
||||
}
|
||||
Log.d(LOG_TAG, "getProfileMetadata: ${profileInfo.asPrettyJson()}")
|
||||
|
||||
// Check if all relays in relaylist can be connected to
|
||||
return AuthorNostrData(
|
||||
uri = possibleNostrProfile.toNostrUri(),
|
||||
name = profileInfo.getName().toString(),
|
||||
publicKey = publicKey,
|
||||
imageUrl = profileInfo.getPicture().toString(),
|
||||
relayList = nostrClient.relays().map { relayEntry -> relayEntry.key },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private suspend fun getUserPublishRelays(userPubkey: PublicKey): List<String> {
|
||||
val userRelaysFilter =
|
||||
Filter()
|
||||
.author(userPubkey)
|
||||
.kind(
|
||||
Kind.fromEnum(KindEnum.RelayList),
|
||||
)
|
||||
|
||||
nostrClient.removeAllRelays()
|
||||
defaultMetadataRelays.forEach { relayUrl ->
|
||||
nostrClient.addReadRelay(relayUrl)
|
||||
}
|
||||
nostrClient.connect()
|
||||
val potentialUserRelays =
|
||||
nostrClient.fetchEventsFrom(
|
||||
urls = defaultMetadataRelays,
|
||||
filters = listOf(userRelaysFilter),
|
||||
timeout = Duration.ofSeconds(5),
|
||||
)
|
||||
val relayList = extractRelayList(potentialUserRelays.toVec().first())
|
||||
val relaysToUse =
|
||||
if (relayList.any { (_, relayType) -> relayType == RelayMetadata.WRITE }) {
|
||||
relayList.filter { it.value == RelayMetadata.WRITE }.map { entry -> entry.key }
|
||||
} else if (relayList.size < 7) {
|
||||
relayList.map { entry -> entry.key } // This represents the relay URL, just as the operation above.
|
||||
} else {
|
||||
defaultArticleFetchRelays.map { it }
|
||||
}
|
||||
|
||||
return relaysToUse
|
||||
}
|
||||
|
||||
private suspend fun fetchArticlesForAuthor(
|
||||
author: PublicKey,
|
||||
relays: List<String>,
|
||||
): List<Event> {
|
||||
val articlesByAuthorFilter =
|
||||
Filter()
|
||||
.author(author)
|
||||
.kind(Kind.fromEnum(KindEnum.LongFormTextNote))
|
||||
Log.d(LOG_TAG, "Relay List size: ${relays.size}")
|
||||
|
||||
nostrClient.removeAllRelays()
|
||||
val relaysToUse =
|
||||
relays.take(3).plus(defaultArticleFetchRelays.random())
|
||||
.ifEmpty { defaultFetchRelays }
|
||||
relaysToUse.forEach { relay -> nostrClient.addReadRelay(relay) }
|
||||
nostrClient.connect()
|
||||
Log.d(LOG_TAG, "FETCHING ARTICLES")
|
||||
val articleEventSet =
|
||||
nostrClient.fetchEventsFrom(
|
||||
urls = relaysToUse,
|
||||
filters =
|
||||
listOf(
|
||||
articlesByAuthorFilter,
|
||||
),
|
||||
timeout = Duration.ofSeconds(10L),
|
||||
).toVec()
|
||||
val articleEvents = articleEventSet.distinctBy { it.tags().find(TagKind.Title) }
|
||||
Log.d(LOG_TAG, "fetchArticlesForAuthor: Article Set Size: ${articleEvents.size}")
|
||||
nostrClient.removeAllRelays() // This is necessary to avoid piling relays to fetch from(on each fetch).
|
||||
return articleEvents
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun fetchFeedFrom(uri: String, nostrClient: Client): NostrFeed {
|
||||
nostrClient.use {
|
||||
val feedInstance = NostrFeed(nostrClient)
|
||||
val feedResult = feedInstance.nreq(uri)
|
||||
feedInstance.feedFetchResult = feedResult
|
||||
return if (feedInstance.getArticles().isNotEmpty()){
|
||||
feedInstance
|
||||
} else throw EmptyNostrDataException("No feed found for $uri")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun fetchFeedMetadata(uri: String, nostrClient: Client): AuthorNostrData {
|
||||
val feedFetcher = NostrFeed(nostrClient)
|
||||
return feedFetcher.getProfileMetadata(uri)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AuthorNostrData(
|
||||
val uri: String,
|
||||
val name: String,
|
||||
val publicKey: PublicKey,
|
||||
val imageUrl: String,
|
||||
val relayList: List<String>
|
||||
)
|
||||
|
||||
class EmptyNostrDataException(override val message: String?): Exception(message)
|
||||
|
||||
class NostrFeedResult(
|
||||
val nostrUri: String,
|
||||
val authorName: String,
|
||||
var feedTitle: String,
|
||||
val authorPictureLink: String,
|
||||
val articles: List<Event>
|
||||
)
|
@ -10,7 +10,9 @@ import me.ash.reader.domain.model.group.Group
|
||||
import me.ash.reader.domain.model.group.GroupWithFeed
|
||||
import me.ash.reader.infrastructure.di.IODispatcher
|
||||
import me.ash.reader.ui.ext.extractDomain
|
||||
import me.ash.reader.ui.ext.isNostrUri
|
||||
import me.ash.reader.ui.ext.spacerDollar
|
||||
import rust.nostr.sdk.Client
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
@ -48,7 +50,21 @@ class OPMLDataSource @Inject constructor(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
groupWithFeedList.addFeedToDefault(
|
||||
val feedUrl = outline.extractUrl()
|
||||
val feedToAdd = if (feedUrl?.isNostrUri() == true) {
|
||||
val feedMetadata = NostrFeed.fetchFeedMetadata(feedUrl, Client())
|
||||
Feed(
|
||||
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
|
||||
name = outline.extractName(),
|
||||
url = outline.extractUrl() ?: continue,
|
||||
icon = feedMetadata.imageUrl,
|
||||
groupId = defaultGroup.id,
|
||||
accountId = targetAccountId,
|
||||
isNotification = outline.extractPresetNotification(),
|
||||
isFullContent = outline.extractPresetFullContent(),
|
||||
)
|
||||
}
|
||||
else {
|
||||
Feed(
|
||||
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
|
||||
name = outline.extractName(),
|
||||
@ -58,7 +74,8 @@ class OPMLDataSource @Inject constructor(
|
||||
isNotification = outline.extractPresetNotification(),
|
||||
isFullContent = outline.extractPresetFullContent(),
|
||||
)
|
||||
)
|
||||
}
|
||||
groupWithFeedList.addFeedToDefault(feedToAdd)
|
||||
}
|
||||
} else {
|
||||
var groupId = defaultGroup.id
|
||||
@ -74,7 +91,21 @@ class OPMLDataSource @Inject constructor(
|
||||
}
|
||||
for (subOutline in outline.subElements) {
|
||||
if (subOutline != null && subOutline.attributes != null) {
|
||||
groupWithFeedList.addFeed(
|
||||
val feedUrl = outline.extractUrl()
|
||||
val feedToAdd = if (feedUrl?.isNostrUri() == true) {
|
||||
val feedMetadata = NostrFeed.fetchFeedMetadata(feedUrl, Client())
|
||||
Feed(
|
||||
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
|
||||
name = subOutline.extractName(),
|
||||
url = subOutline.extractUrl() ?: continue,
|
||||
icon = feedMetadata.imageUrl,
|
||||
groupId = groupId,
|
||||
accountId = targetAccountId,
|
||||
isNotification = subOutline.extractPresetNotification(),
|
||||
isFullContent = subOutline.extractPresetFullContent(),
|
||||
)
|
||||
}
|
||||
else {
|
||||
Feed(
|
||||
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
|
||||
name = subOutline.extractName(),
|
||||
@ -84,7 +115,8 @@ class OPMLDataSource @Inject constructor(
|
||||
isNotification = subOutline.extractPresetNotification(),
|
||||
isFullContent = subOutline.extractPresetFullContent(),
|
||||
)
|
||||
)
|
||||
}
|
||||
groupWithFeedList.addFeed(feedToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package me.ash.reader.infrastructure.rss
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.rometools.rome.feed.synd.SyndEntry
|
||||
import com.rometools.rome.feed.synd.SyndFeed
|
||||
import com.rometools.rome.feed.synd.SyndImageImpl
|
||||
import com.rometools.rome.io.SyndFeedInput
|
||||
import com.rometools.rome.io.XmlReader
|
||||
@ -12,18 +11,30 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.ash.reader.domain.model.article.Article
|
||||
import me.ash.reader.domain.model.feed.Feed
|
||||
import me.ash.reader.domain.model.feed.FeedWithArticle
|
||||
import me.ash.reader.domain.repository.FeedDao
|
||||
import me.ash.reader.infrastructure.di.IODispatcher
|
||||
import me.ash.reader.infrastructure.html.Readability
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.ext.decodeHTML
|
||||
import me.ash.reader.ui.ext.extractDomain
|
||||
import me.ash.reader.ui.ext.htmlFromMarkdown
|
||||
import me.ash.reader.ui.ext.isFuture
|
||||
import me.ash.reader.ui.ext.isNostrUri
|
||||
import me.ash.reader.ui.ext.spacerDollar
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.executeAsync
|
||||
import rust.nostr.sdk.Alphabet
|
||||
import rust.nostr.sdk.Client
|
||||
import rust.nostr.sdk.Coordinate
|
||||
import rust.nostr.sdk.Event
|
||||
import rust.nostr.sdk.Kind
|
||||
import rust.nostr.sdk.KindEnum
|
||||
import rust.nostr.sdk.SingleLetterTag
|
||||
import rust.nostr.sdk.TagKind
|
||||
import java.io.InputStream
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -39,15 +50,24 @@ class RssHelper @Inject constructor(
|
||||
@IODispatcher
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val nostrClient: Client
|
||||
) {
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun searchFeed(feedLink: String): SyndFeed {
|
||||
suspend fun searchFeed(feedLink: String): FetchedFeed? {
|
||||
return withContext(ioDispatcher) {
|
||||
SyndFeedInput().build(XmlReader(inputStream(okHttpClient, feedLink))).also {
|
||||
it.icon = SyndImageImpl()
|
||||
it.icon.link = queryRssIconLink(feedLink)
|
||||
it.icon.url = it.icon.link
|
||||
if(feedLink.isNostrUri()) {
|
||||
NostrFeed.fetchFeedFrom(feedLink, nostrClient)
|
||||
}
|
||||
else {
|
||||
val parsedSyndFeed = SyndFeedInput()
|
||||
.build(XmlReader(inputStream(okHttpClient, feedLink)))
|
||||
.also {
|
||||
it.icon = SyndImageImpl()
|
||||
it.icon.link = queryRssIconLink(feedLink)
|
||||
it.icon.url = it.icon.link
|
||||
}
|
||||
SyndFeedDelegate(parsedSyndFeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,6 +146,90 @@ class RssHelper @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun syncNostrFeed(
|
||||
feed: Feed,
|
||||
latestLink: String?,
|
||||
preDate: Date = Date()
|
||||
): FeedWithArticle =
|
||||
try {
|
||||
val accountId = context.currentAccountId
|
||||
Client().use {
|
||||
val nostrFeed = NostrFeed.fetchFeedFrom(feed.url, it)
|
||||
val updatedArticles = nostrFeed.getArticles()
|
||||
.map { buildArticleFromNostrEvent(feed, accountId, it, nostrFeed.getFeedAuthor(), preDate) }
|
||||
val updatedFeed = feed.copy(
|
||||
icon = nostrFeed.getIconUrl()
|
||||
)
|
||||
return FeedWithArticle(updatedFeed, updatedArticles)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e("RLog", "syncNostrFeedNew[${feed.name}]: ${e.message}")
|
||||
FeedWithArticle(feed, emptyList())
|
||||
}
|
||||
|
||||
fun buildArticleFromNostrEvent(
|
||||
feed: Feed,
|
||||
accountId: Int,
|
||||
articleEvent: Event,
|
||||
authorName: String,
|
||||
// imageUrl: String,
|
||||
preDate: Date = Date()
|
||||
): Article {
|
||||
val articleTitle = articleEvent.tags().find(TagKind.Title)?.content()
|
||||
val articleImage = articleEvent.tags().find(TagKind.Image)?.content()
|
||||
val articleSummary = articleEvent.tags().find(TagKind.Summary)?.content()
|
||||
val timeStamp = articleEvent.tags().find(TagKind.PublishedAt)?.content()?.toLong()
|
||||
?: Instant.EPOCH.epochSecond
|
||||
val articleDate = Date.from(Instant.ofEpochSecond(timeStamp)).takeIf { !it.isFuture(preDate) } ?: preDate
|
||||
val articleNostrAddress =
|
||||
Coordinate(
|
||||
Kind.fromEnum(KindEnum.LongFormTextNote),
|
||||
articleEvent.author(),
|
||||
articleEvent.tags().find(
|
||||
TagKind.SingleLetter(
|
||||
SingleLetterTag.lowercase(Alphabet.D),
|
||||
),
|
||||
)?.content().toString(),
|
||||
).toBech32()
|
||||
// Highlighter is a service for reading Nostr articles on the web.
|
||||
//For the external link, we can still give it a value of nostr:<articleAddress>
|
||||
val externalLink = "nostr:$articleNostrAddress"//""https://highlighter.com/a/$articleNostrAddress"
|
||||
val articleContent = articleEvent.content()
|
||||
val parsedContent = htmlFromMarkdown(articleContent)
|
||||
val actualContent = Readability.parseToText(
|
||||
parsedContent,
|
||||
uri = null
|
||||
)
|
||||
|
||||
Log.i(
|
||||
"RLog",
|
||||
"Nostr Feed:\n" +
|
||||
"name: ${feed.name}\n" +
|
||||
"feedUrl: ${feed.url}\n" +
|
||||
"url: ${externalLink}\n" +
|
||||
"title: ${articleTitle}\n" +
|
||||
"desc: ${articleSummary}\n" +
|
||||
"content: ${articleContent}\n"
|
||||
)
|
||||
|
||||
return Article(
|
||||
id = accountId.spacerDollar(UUID.randomUUID().toString()),
|
||||
accountId = accountId,
|
||||
feedId = feed.id,
|
||||
date = articleDate,
|
||||
title = articleTitle ?: feed.name,
|
||||
author = authorName,
|
||||
rawDescription = parsedContent,
|
||||
shortDescription = articleSummary ?: actualContent.take(110),
|
||||
fullContent = parsedContent,
|
||||
img = articleImage,
|
||||
link = externalLink,
|
||||
updateAt = articleDate
|
||||
)
|
||||
}
|
||||
|
||||
fun findThumbnail(syndEntry: SyndEntry): String? {
|
||||
if (syndEntry.enclosures?.firstOrNull()?.url != null) {
|
||||
return syndEntry.enclosures.first().url
|
||||
|
@ -1,14 +1,18 @@
|
||||
package me.ash.reader.infrastructure.rss.provider
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import me.ash.reader.infrastructure.di.UserAgentInterceptor
|
||||
import me.ash.reader.infrastructure.di.cachingHttpClient
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
abstract class ProviderAPI {
|
||||
abstract class ProviderAPI(context: Context, clientCertificateAlias: String?) {
|
||||
|
||||
protected val client: OkHttpClient = cachingHttpClient()
|
||||
protected val client: OkHttpClient = cachingHttpClient(
|
||||
context = context,
|
||||
clientCertificateAlias = clientCertificateAlias,
|
||||
)
|
||||
.newBuilder()
|
||||
.addNetworkInterceptor(UserAgentInterceptor)
|
||||
.build()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package me.ash.reader.infrastructure.rss.provider.fever
|
||||
|
||||
import android.content.Context
|
||||
import me.ash.reader.infrastructure.exception.FeverAPIException
|
||||
import me.ash.reader.infrastructure.rss.provider.ProviderAPI
|
||||
import me.ash.reader.ui.ext.encodeBase64
|
||||
@ -10,11 +11,13 @@ import okhttp3.executeAsync
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FeverAPI private constructor(
|
||||
context: Context,
|
||||
private val serverUrl: String,
|
||||
private val apiKey: String,
|
||||
private val httpUsername: String? = null,
|
||||
private val httpPassword: String? = null,
|
||||
) : ProviderAPI() {
|
||||
clientCertificateAlias: String? = null,
|
||||
) : ProviderAPI(context, clientCertificateAlias) {
|
||||
|
||||
private suspend inline fun <reified T> postRequest(query: String?): T {
|
||||
val response = client.newCall(
|
||||
@ -104,14 +107,16 @@ class FeverAPI private constructor(
|
||||
private val instances: ConcurrentHashMap<String, FeverAPI> = ConcurrentHashMap()
|
||||
|
||||
fun getInstance(
|
||||
context: Context,
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
password: String,
|
||||
httpUsername: String? = null,
|
||||
httpPassword: String? = null,
|
||||
clientCertificateAlias: String? = null,
|
||||
): FeverAPI = "$username:$password".md5().run {
|
||||
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword") {
|
||||
FeverAPI(serverUrl, this, httpUsername, httpPassword)
|
||||
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword$clientCertificateAlias") {
|
||||
FeverAPI(context, serverUrl, this, httpUsername, httpPassword, clientCertificateAlias)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package me.ash.reader.infrastructure.rss.provider.greader
|
||||
|
||||
import android.content.Context
|
||||
import me.ash.reader.infrastructure.di.USER_AGENT_STRING
|
||||
import me.ash.reader.infrastructure.exception.GoogleReaderAPIException
|
||||
import me.ash.reader.infrastructure.exception.RetryException
|
||||
@ -10,12 +11,14 @@ import okhttp3.executeAsync
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class GoogleReaderAPI private constructor(
|
||||
context: Context,
|
||||
private val serverUrl: String,
|
||||
private val username: String,
|
||||
private val password: String,
|
||||
private val httpUsername: String? = null,
|
||||
private val httpPassword: String? = null,
|
||||
) : ProviderAPI() {
|
||||
clientCertificateAlias: String? = null,
|
||||
) : ProviderAPI(context, clientCertificateAlias) {
|
||||
|
||||
enum class Stream(val tag: String) {
|
||||
ALL_ITEMS("user/-/state/com.google/reading-list"),
|
||||
@ -350,13 +353,15 @@ class GoogleReaderAPI private constructor(
|
||||
private val instances: ConcurrentHashMap<String, GoogleReaderAPI> = ConcurrentHashMap()
|
||||
|
||||
fun getInstance(
|
||||
context: Context,
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
password: String,
|
||||
httpUsername: String? = null,
|
||||
httpPassword: String? = null,
|
||||
): GoogleReaderAPI = instances.getOrPut("$serverUrl$username$password$httpUsername$httpPassword") {
|
||||
GoogleReaderAPI(serverUrl, username, password, httpUsername, httpPassword)
|
||||
clientCertificateAlias: String? = null
|
||||
): GoogleReaderAPI = instances.getOrPut("$serverUrl$username$password$httpUsername$httpPassword$clientCertificateAlias") {
|
||||
GoogleReaderAPI(context, serverUrl, username, password, httpUsername, httpPassword, clientCertificateAlias)
|
||||
}
|
||||
|
||||
fun clearInstance() {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.ash.reader.ui.component.base
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
@ -22,6 +24,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
@ -46,6 +49,7 @@ fun RYOutlineTextField(
|
||||
errorMessage: String = "",
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@ -59,7 +63,11 @@ fun RYOutlineTextField(
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
modifier = if (onClick != null) {
|
||||
Modifier.focusProperties { canFocus = false }
|
||||
} else {
|
||||
Modifier.focusRequester(focusRequester)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedContainerColor = Color.Transparent
|
||||
@ -115,5 +123,18 @@ fun RYOutlineTextField(
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
readOnly = onClick != null,
|
||||
interactionSource = onClick?.let {
|
||||
remember { MutableInteractionSource() }
|
||||
.also { interactionSource ->
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect {
|
||||
if (it is PressInteraction.Release) {
|
||||
onClick.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||
import me.ash.reader.infrastructure.preference.LocalOpenLink
|
||||
import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingFonts
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingImageHorizontalPadding
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingImageRoundedCorners
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||
@ -27,6 +28,8 @@ import me.ash.reader.infrastructure.preference.LocalReadingTextFontSize
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingTextHorizontalPadding
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingTextLetterSpacing
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingTextLineHeight
|
||||
import me.ash.reader.infrastructure.preference.ReadingFontsPreference
|
||||
import me.ash.reader.ui.ext.ExternalFonts
|
||||
import me.ash.reader.ui.ext.openURL
|
||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||
import me.ash.reader.ui.theme.palette.alwaysLight
|
||||
@ -55,6 +58,7 @@ fun RYWebView(
|
||||
val linkTextColor: Int = MaterialTheme.colorScheme.primary.toArgb()
|
||||
val subheadBold: Boolean = LocalReadingSubheadBold.current.value
|
||||
val subheadUpperCase: Boolean = LocalReadingSubheadUpperCase.current.value
|
||||
val readingFonts = LocalReadingFonts.current
|
||||
val fontSize: Int = LocalReadingTextFontSize.current
|
||||
val letterSpacing: Float = LocalReadingTextLetterSpacing.current
|
||||
val lineHeight: Float = LocalReadingTextLineHeight.current
|
||||
@ -69,6 +73,7 @@ fun RYWebView(
|
||||
mutableStateOf(
|
||||
WebViewLayout.get(
|
||||
context = context,
|
||||
readingFontsPreference = readingFonts,
|
||||
webViewClient = WebViewClient(
|
||||
context = context,
|
||||
refererDomain = refererDomain,
|
||||
@ -81,6 +86,11 @@ fun RYWebView(
|
||||
)
|
||||
}
|
||||
|
||||
val fontPath =
|
||||
if (readingFonts is ReadingFontsPreference.External) ExternalFonts.FontType.ReadingFont.toPath(
|
||||
context
|
||||
) else null
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { webView },
|
||||
@ -95,6 +105,7 @@ fun RYWebView(
|
||||
WebViewHtml.HTML.format(
|
||||
WebViewStyle.get(
|
||||
fontSize = fontSize,
|
||||
fontPath = fontPath,
|
||||
lineHeight = lineHeight,
|
||||
letterSpacing = letterSpacing,
|
||||
textMargin = textMargin,
|
||||
|
@ -65,7 +65,8 @@ class WebViewClient(
|
||||
var imgs = document.getElementsByTagName("img");
|
||||
for(var i = 0; i < imgs.length; i++){
|
||||
imgs[i].pos = i;
|
||||
imgs[i].onclick = function() {
|
||||
imgs[i].onclick = function(event) {
|
||||
event.preventDefault();
|
||||
window.${JavaScriptInterface.NAME}.onImgTagClick(this.src, this.alt);
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,15 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import me.ash.reader.infrastructure.preference.BasicFontsPreference
|
||||
import me.ash.reader.infrastructure.preference.ReadingFontsPreference
|
||||
|
||||
object WebViewLayout {
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun get(
|
||||
context: Context,
|
||||
readingFontsPreference: ReadingFontsPreference,
|
||||
webViewClient: WebViewClient,
|
||||
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
|
||||
) = WebView(context).apply {
|
||||
@ -20,6 +23,20 @@ object WebViewLayout {
|
||||
isVerticalScrollBarEnabled = true
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
with(this.settings) {
|
||||
|
||||
standardFontFamily = when (readingFontsPreference) {
|
||||
ReadingFontsPreference.Cursive -> "cursive"
|
||||
ReadingFontsPreference.Monospace -> "monospace"
|
||||
ReadingFontsPreference.SansSerif -> "sans-serif"
|
||||
ReadingFontsPreference.Serif -> "serif"
|
||||
ReadingFontsPreference.External -> {
|
||||
allowFileAccess = true
|
||||
allowFileAccessFromFileURLs = true
|
||||
"sans-serif"
|
||||
}
|
||||
|
||||
else -> "sans-serif"
|
||||
}
|
||||
domStorageEnabled = true
|
||||
javaScriptEnabled = true
|
||||
addJavascriptInterface(object : JavaScriptInterface {
|
||||
|
@ -4,8 +4,24 @@ object WebViewStyle {
|
||||
|
||||
private fun argbToCssColor(argb: Int): String = String.format("#%06X", 0xFFFFFF and argb)
|
||||
|
||||
private fun applyFontFace(
|
||||
fontPath: String? = null
|
||||
): String = if (fontPath != null) """
|
||||
@font-face {
|
||||
font-family: external;
|
||||
src: url("file://$fontPath")
|
||||
}
|
||||
""".trimIndent() else ""
|
||||
|
||||
private fun applyFontFamily(
|
||||
fontPath: String? = null
|
||||
): String = if (fontPath != null) """
|
||||
--font-family: external;
|
||||
""".trimIndent() else ""
|
||||
|
||||
fun get(
|
||||
fontSize: Int,
|
||||
fontPath: String? = null,
|
||||
lineHeight: Float,
|
||||
letterSpacing: Float,
|
||||
textMargin: Int,
|
||||
@ -24,21 +40,22 @@ object WebViewStyle {
|
||||
selectionTextColor: Int,
|
||||
selectionBgColor: Int,
|
||||
): String = """
|
||||
${applyFontFace(fontPath)}
|
||||
:root {
|
||||
/* --font-family: Inter; */
|
||||
${applyFontFamily(fontPath)}
|
||||
--font-size: ${fontSize}px;
|
||||
--line-height: ${lineHeight * 1.5f};
|
||||
--letter-spacing: ${letterSpacing}px;
|
||||
--text-margin: ${textMargin}px;
|
||||
--text-color: ${argbToCssColor(textColor)};
|
||||
--text-bold: ${if(textBold) "600" else "normal"};
|
||||
--text-bold: ${if (textBold) "600" else "normal"};
|
||||
--text-align: ${textAlign};
|
||||
--bold-text-color: ${argbToCssColor(boldTextColor)};
|
||||
--link-text-color: ${argbToCssColor(linkTextColor)};
|
||||
--selection-text-color: ${argbToCssColor(selectionTextColor)};
|
||||
--selection-bg-color: ${argbToCssColor(selectionBgColor)};
|
||||
--subhead-bold: ${if(subheadBold) "600" else "normal"};
|
||||
--subhead-upper-case: ${if(subheadUpperCase) "uppercase" else "none"};
|
||||
--subhead-bold: ${if (subheadBold) "600" else "normal"};
|
||||
--subhead-upper-case: ${if (subheadUpperCase) "uppercase" else "none"};
|
||||
--img-margin: ${imgMargin}px;
|
||||
--img-border-radius: ${imgBorderRadius}px;
|
||||
--content-padding;
|
||||
@ -312,7 +329,7 @@ figure {
|
||||
text-align: var(--text-align) !important;
|
||||
margin: 0 !important;
|
||||
opacity: 0.8 !important;
|
||||
font-size: 0.8em !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
figure * {
|
||||
@ -323,7 +340,7 @@ figure p,
|
||||
caption,
|
||||
figcaption {
|
||||
opacity: 0.8 !important;
|
||||
font-size: 0.8em !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
|
@ -136,7 +136,6 @@ data class DataStoreKey<T>(
|
||||
// Reading page
|
||||
const val readingRenderer = "readingRender"
|
||||
const val readingBionicReading = "readingBionicReading"
|
||||
const val readingDarkTheme = "readingDarkTheme"
|
||||
const val readingPageTonalElevation = "readingPageTonalElevation"
|
||||
const val readingTextFontSize = "readingTextFontSize"
|
||||
const val readingTextLineHeight = "readingTextLineHeight"
|
||||
@ -163,6 +162,7 @@ data class DataStoreKey<T>(
|
||||
const val swipeStartAction = "swipeStartAction"
|
||||
const val swipeEndAction = "swipeEndAction"
|
||||
const val markAsReadOnScroll = "markAsReadOnScroll"
|
||||
const val hideEmptyGroups = "hideEmptyGroups"
|
||||
const val pullToSwitchArticle = "pullToSwitchArticle"
|
||||
const val openLink = "openLink"
|
||||
const val openLinkAppSpecificBrowser = "openLinkAppSpecificBrowser"
|
||||
@ -212,7 +212,6 @@ data class DataStoreKey<T>(
|
||||
// Reading page
|
||||
readingRenderer to DataStoreKey(intPreferencesKey(readingRenderer), Int::class.java),
|
||||
readingBionicReading to DataStoreKey(booleanPreferencesKey(readingBionicReading), Boolean::class.java),
|
||||
readingDarkTheme to DataStoreKey(intPreferencesKey(readingDarkTheme), Int::class.java),
|
||||
readingPageTonalElevation to DataStoreKey(intPreferencesKey(readingPageTonalElevation), Int::class.java),
|
||||
readingTextFontSize to DataStoreKey(intPreferencesKey(readingTextFontSize), Int::class.java),
|
||||
readingTextLineHeight to DataStoreKey(floatPreferencesKey(readingTextLineHeight), Float::class.java),
|
||||
@ -241,6 +240,7 @@ data class DataStoreKey<T>(
|
||||
booleanPreferencesKey(markAsReadOnScroll),
|
||||
Boolean::class.java
|
||||
),
|
||||
hideEmptyGroups to DataStoreKey(booleanPreferencesKey(hideEmptyGroups), Boolean::class.java),
|
||||
pullToSwitchArticle to DataStoreKey(booleanPreferencesKey(pullToSwitchArticle), Boolean::class.java),
|
||||
openLink to DataStoreKey(intPreferencesKey(openLink), Int::class.java),
|
||||
openLinkAppSpecificBrowser to DataStoreKey(stringPreferencesKey(openLinkAppSpecificBrowser), String::class.java),
|
||||
|
@ -2,6 +2,9 @@ package me.ash.reader.ui.ext
|
||||
|
||||
import android.text.Html
|
||||
import android.util.Base64
|
||||
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||
import org.intellij.markdown.html.HtmlGenerator
|
||||
import org.intellij.markdown.parser.MarkdownParser
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
import java.text.Bidi
|
||||
@ -18,7 +21,7 @@ fun String.formatUrl(): String {
|
||||
if (this.startsWith("//")) {
|
||||
return "https:$this"
|
||||
}
|
||||
val regex = Regex("^(https?|ftp|file).*")
|
||||
val regex = Regex("^(https?|ftp|file|nostr).*")
|
||||
return if (!regex.matches(this)) {
|
||||
"https://$this"
|
||||
} else {
|
||||
@ -61,3 +64,16 @@ fun String?.extractDomain(): String? {
|
||||
val domainMatchResult = domainRegex.find(this)
|
||||
return domainMatchResult?.value
|
||||
}
|
||||
|
||||
private val markDownParser = MarkdownParser(CommonMarkFlavourDescriptor())
|
||||
|
||||
fun htmlFromMarkdown(markdown: String): String {
|
||||
val parsedMarkdown = markDownParser.buildMarkdownTreeFromString(markdown)
|
||||
val htmlContent = HtmlGenerator(markdown, parsedMarkdown, CommonMarkFlavourDescriptor())
|
||||
.generateHtml()
|
||||
|
||||
return htmlContent
|
||||
}
|
||||
|
||||
const val NOSTR_URI_PREFIX = "nostr:"
|
||||
fun String.isNostrUri(): Boolean = startsWith(NOSTR_URI_PREFIX) && length > NOSTR_URI_PREFIX.length
|
@ -20,7 +20,6 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import me.ash.reader.domain.model.general.Filter
|
||||
import me.ash.reader.infrastructure.preference.LocalDarkTheme
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingDarkTheme
|
||||
import me.ash.reader.ui.ext.animatedComposable
|
||||
import me.ash.reader.ui.ext.collectAsStateValue
|
||||
import me.ash.reader.ui.ext.findActivity
|
||||
@ -41,7 +40,6 @@ import me.ash.reader.ui.page.settings.color.DarkThemePage
|
||||
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
|
||||
import me.ash.reader.ui.page.settings.color.flow.FlowPageStylePage
|
||||
import me.ash.reader.ui.page.settings.color.reading.BionicReadingPage
|
||||
import me.ash.reader.ui.page.settings.color.reading.ReadingDarkThemePage
|
||||
import me.ash.reader.ui.page.settings.color.reading.ReadingImagePage
|
||||
import me.ash.reader.ui.page.settings.color.reading.ReadingStylePage
|
||||
import me.ash.reader.ui.page.settings.color.reading.ReadingTextPage
|
||||
@ -132,15 +130,8 @@ fun HomeEntry(
|
||||
}
|
||||
}
|
||||
|
||||
val useDarkTheme = if (isReadingPage) {
|
||||
LocalReadingDarkTheme.current.isDarkTheme()
|
||||
} else {
|
||||
LocalDarkTheme.current.isDarkTheme()
|
||||
}
|
||||
|
||||
AppTheme(
|
||||
useDarkTheme = if (isReadingPage) LocalReadingDarkTheme.current.isDarkTheme()
|
||||
else LocalDarkTheme.current.isDarkTheme()
|
||||
useDarkTheme = LocalDarkTheme.current.isDarkTheme()
|
||||
) {
|
||||
|
||||
NavHost(
|
||||
@ -210,9 +201,6 @@ fun HomeEntry(
|
||||
animatedComposable(route = RouteName.READING_BIONIC_READING) {
|
||||
BionicReadingPage(navController)
|
||||
}
|
||||
animatedComposable(route = RouteName.READING_DARK_THEME) {
|
||||
ReadingDarkThemePage(navController)
|
||||
}
|
||||
animatedComposable(route = RouteName.READING_PAGE_TITLE) {
|
||||
ReadingTitlePage(navController)
|
||||
}
|
||||
|
@ -46,11 +46,11 @@ class HomeViewModel @Inject constructor(
|
||||
private val _filterUiState = MutableStateFlow(FilterState())
|
||||
val filterUiState = _filterUiState.asStateFlow()
|
||||
|
||||
val syncWorkLiveData = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_NAME)
|
||||
val syncWorkLiveData = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_TAG)
|
||||
|
||||
fun sync() {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
rssService.get().doSync()
|
||||
rssService.get().doSyncOneTime()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ fun FeedItem(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(if (isEnded()) ShapeBottom32 else RectangleShape)
|
||||
.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick()
|
||||
@ -88,9 +88,7 @@ fun FeedItem(
|
||||
}
|
||||
if ((feed.important ?: 0) != 0) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
|
||||
alpha = badgeAlpha
|
||||
),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
contentColor = MaterialTheme.colorScheme.outline,
|
||||
content = {
|
||||
Text(
|
||||
|
@ -66,6 +66,7 @@ import me.ash.reader.infrastructure.preference.LocalFeedsFilterBarTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalFeedsGroupListExpand
|
||||
import me.ash.reader.infrastructure.preference.LocalFeedsGroupListTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalFeedsTopBarTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalHideEmptyGroups
|
||||
import me.ash.reader.infrastructure.preference.LocalNewVersionNumber
|
||||
import me.ash.reader.infrastructure.preference.LocalSkipVersionNumber
|
||||
import me.ash.reader.ui.component.FilterBar
|
||||
@ -198,9 +199,11 @@ fun FeedsPage(
|
||||
feedsViewModel.fetchAccount()
|
||||
}
|
||||
|
||||
val hideEmptyGroups = LocalHideEmptyGroups.current.value
|
||||
|
||||
LaunchedEffect(filterUiState, isSyncing) {
|
||||
snapshotFlow { filterUiState }.collect {
|
||||
feedsViewModel.pullFeeds(it)
|
||||
feedsViewModel.pullFeeds(it, hideEmptyGroups)
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,7 +213,7 @@ fun FeedsPage(
|
||||
|
||||
RYScaffold(
|
||||
topBarTonalElevation = topBarTonalElevation.value.dp,
|
||||
containerTonalElevation = groupListTonalElevation.value.dp,
|
||||
// containerTonalElevation = groupListTonalElevation.value.dp,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier.clickable(
|
||||
|
@ -49,7 +49,7 @@ class FeedsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun pullFeeds(filterState: FilterState) {
|
||||
fun pullFeeds(filterState: FilterState, hideEmptyGroups: Boolean) {
|
||||
val isStarred = filterState.filter.isStarred()
|
||||
val isUnread = filterState.filter.isUnread()
|
||||
_feedsUiState.update {
|
||||
@ -77,7 +77,7 @@ class FeedsViewModel @Inject constructor(
|
||||
while (groupIterator.hasNext()) {
|
||||
val groupWithFeed = groupIterator.next()
|
||||
val groupImportant = importantMap[groupWithFeed.group.id] ?: 0
|
||||
if ((isStarred || isUnread) && groupImportant == 0) {
|
||||
if (hideEmptyGroups && (isStarred || isUnread) && groupImportant == 0) {
|
||||
groupIterator.remove()
|
||||
continue
|
||||
}
|
||||
@ -87,7 +87,7 @@ class FeedsViewModel @Inject constructor(
|
||||
val feed = feedIterator.next()
|
||||
val feedImportant = importantMap[feed.id] ?: 0
|
||||
groupWithFeed.group.feeds++
|
||||
if ((isStarred || isUnread) && feedImportant == 0) {
|
||||
if (hideEmptyGroups && (isStarred || isUnread) && feedImportant == 0) {
|
||||
feedIterator.remove()
|
||||
continue
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ fun GroupItem(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(if (isExpanded() && !roundedBottomCorner()) ShapeTop32 else Shape32)
|
||||
.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
groupOnClick()
|
||||
@ -84,7 +84,7 @@ fun GroupItem(
|
||||
.padding(end = 20.dp)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceTint.copy(alpha = indicatorAlpha))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||
.clickable { onExpanded() },
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
@ -33,6 +33,8 @@ import me.ash.reader.R
|
||||
import me.ash.reader.domain.model.account.Account
|
||||
import me.ash.reader.ui.component.base.RYDialog
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.theme.palette.FixedColorRoles
|
||||
import me.ash.reader.ui.theme.palette.LocalFixedColorRoles
|
||||
import me.ash.reader.ui.theme.palette.alwaysLight
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@ -75,20 +77,21 @@ fun AccountsTab(
|
||||
}
|
||||
.padding(8.dp),
|
||||
) {
|
||||
val selected = account.id == context.currentAccountId
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(52.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (account.id == context.currentAccountId) {
|
||||
MaterialTheme.colorScheme.primaryContainer alwaysLight true
|
||||
if (selected) {
|
||||
LocalFixedColorRoles.current.primaryFixed
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceDim alwaysLight true
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AccountTypeIcon(account = account)
|
||||
AccountTypeIcon(account = account, selected = selected)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
@ -125,15 +128,19 @@ fun AccountsTab(
|
||||
@Composable
|
||||
fun AccountTypeIcon(
|
||||
account: Account,
|
||||
selected: Boolean
|
||||
) {
|
||||
val icon = account.type.toIcon().takeIf { it is ImageVector }?.let { it as ImageVector }
|
||||
val iconPainter = account.type.toIcon().takeIf { it is Painter }?.let { it as Painter }
|
||||
val contentColor =
|
||||
if (selected) LocalFixedColorRoles.current.onPrimaryFixed else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = account.name,
|
||||
tint = MaterialTheme.colorScheme.onSurface alwaysLight true
|
||||
tint = contentColor
|
||||
)
|
||||
} else {
|
||||
iconPainter?.let {
|
||||
@ -141,7 +148,7 @@ fun AccountTypeIcon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = it,
|
||||
contentDescription = account.name,
|
||||
tint = MaterialTheme.colorScheme.onSurface alwaysLight true
|
||||
tint = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.animation.with
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CreateNewFolder
|
||||
@ -81,7 +80,7 @@ fun SubscribeDialog(
|
||||
icon = {
|
||||
FeedIcon(
|
||||
feedName = subscribeUiState.searchedFeed?.title ?: stringResource(R.string.subscribe),
|
||||
iconUrl = subscribeUiState.searchedFeed?.icon?.url,
|
||||
iconUrl = subscribeUiState.searchedFeed?.getIconUrl(),
|
||||
placeholderIcon = Icons.Rounded.RssFeed,
|
||||
)
|
||||
},
|
||||
|
@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds.subscribe
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.rometools.rome.feed.synd.SyndFeed
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
@ -20,6 +19,7 @@ import me.ash.reader.domain.service.OpmlService
|
||||
import me.ash.reader.domain.service.RssService
|
||||
import me.ash.reader.infrastructure.android.AndroidStringsHelper
|
||||
import me.ash.reader.infrastructure.di.ApplicationScope
|
||||
import me.ash.reader.infrastructure.rss.FetchedFeed
|
||||
import me.ash.reader.infrastructure.rss.RssHelper
|
||||
import me.ash.reader.ui.ext.formatUrl
|
||||
import java.io.InputStream
|
||||
@ -59,7 +59,7 @@ class SubscribeViewModel @Inject constructor(
|
||||
fun importFromInputStream(inputStream: InputStream) {
|
||||
applicationScope.launch {
|
||||
opmlService.saveToDatabase(inputStream)
|
||||
rssService.get().doSync()
|
||||
rssService.get().doSyncOneTime()
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,7 +245,7 @@ data class SubscribeUiState(
|
||||
val errorMessage: String = "",
|
||||
val linkContent: String = "",
|
||||
val lockLinkInput: Boolean = false,
|
||||
val searchedFeed: SyndFeed? = null,
|
||||
val searchedFeed: FetchedFeed? = null,
|
||||
val allowNotificationPreset: Boolean = false,
|
||||
val parseFullContentPreset: Boolean = false,
|
||||
val selectedGroupId: String = "",
|
||||
|
@ -1,6 +1,8 @@
|
||||
package me.ash.reader.ui.page.home.flow
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@ -19,6 +21,7 @@ import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
@ -26,21 +29,29 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.eventFlow
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.work.WorkInfo
|
||||
@ -67,6 +78,8 @@ import me.ash.reader.ui.component.base.RYExtensibleVisibility
|
||||
import me.ash.reader.ui.component.base.RYScaffold
|
||||
import me.ash.reader.ui.ext.collectAsStateValue
|
||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||
import me.ash.reader.ui.motion.materialSharedAxisYIn
|
||||
import me.ash.reader.ui.motion.materialSharedAxisYOut
|
||||
import me.ash.reader.ui.page.common.RouteName
|
||||
import me.ash.reader.ui.page.home.HomeViewModel
|
||||
|
||||
@ -80,10 +93,10 @@ fun FlowPage(
|
||||
homeViewModel: HomeViewModel,
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
|
||||
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
|
||||
val articleListFeedIcon = LocalFlowArticleListFeedIcon.current
|
||||
val articleListDateStickyHeader = LocalFlowArticleListDateStickyHeader.current
|
||||
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
|
||||
val filterBarStyle = LocalFlowFilterBarStyle.current
|
||||
val filterBarFilled = LocalFlowFilterBarFilled.current
|
||||
val filterBarPadding = LocalFlowFilterBarPadding.current
|
||||
@ -99,6 +112,18 @@ fun FlowPage(
|
||||
val listState =
|
||||
if (pagingItems.itemCount > 0) flowUiState.listState else rememberLazyListState()
|
||||
|
||||
val isTopBarElevated = topBarTonalElevation.value > 0
|
||||
val isScrolled by remember(listState) { derivedStateOf { listState.firstVisibleItemIndex != 0 } }
|
||||
val topBarContainerColor by animateColorAsState(with(MaterialTheme.colorScheme) {
|
||||
if (isScrolled && isTopBarElevated) surfaceContainer else surface
|
||||
}, label = "")
|
||||
|
||||
val titleText = when {
|
||||
filterUiState.group != null -> filterUiState.group.name
|
||||
filterUiState.feed != null -> filterUiState.feed.name
|
||||
else -> filterUiState.filter.toName()
|
||||
}
|
||||
|
||||
if (markAsReadOnScroll) {
|
||||
LaunchedEffect(listState.isScrollInProgress) {
|
||||
if (!listState.isScrollInProgress) {
|
||||
@ -127,7 +152,7 @@ fun FlowPage(
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var markAsRead by remember { mutableStateOf(false) }
|
||||
var onSearch by remember { mutableStateOf(false) }
|
||||
var onSearch by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val owner = LocalLifecycleOwner.current
|
||||
|
||||
@ -148,6 +173,13 @@ fun FlowPage(
|
||||
}
|
||||
|
||||
DisposableEffect(owner) {
|
||||
scope.launch {
|
||||
owner.lifecycle.eventFlow.collect {
|
||||
if (it == Lifecycle.Event.ON_PAUSE) {
|
||||
flowViewModel.commitDiff()
|
||||
}
|
||||
}
|
||||
}
|
||||
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
|
||||
workInfoList.let {
|
||||
isSyncing = it.any { workInfo -> workInfo.state == WorkInfo.State.RUNNING }
|
||||
@ -206,15 +238,10 @@ fun FlowPage(
|
||||
}
|
||||
|
||||
LaunchedEffect(onSearch) {
|
||||
snapshotFlow { onSearch }.collect {
|
||||
if (it) {
|
||||
delay(100) // ???
|
||||
focusRequester.requestFocus()
|
||||
} else {
|
||||
keyboardController?.hide()
|
||||
if (homeUiState.searchContent.isNotBlank()) {
|
||||
homeViewModel.inputSearchContent("")
|
||||
}
|
||||
if (!onSearch) {
|
||||
keyboardController?.hide()
|
||||
if (homeUiState.searchContent.isNotBlank()) {
|
||||
homeViewModel.inputSearchContent("")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -232,7 +259,6 @@ fun FlowPage(
|
||||
}
|
||||
|
||||
RYScaffold(
|
||||
topBarTonalElevation = topBarTonalElevation.value.dp,
|
||||
containerTonalElevation = articleListTonalElevation.value.dp,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@ -247,7 +273,20 @@ fun FlowPage(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
title = {},
|
||||
title = {
|
||||
AnimatedVisibility(
|
||||
isScrolled,
|
||||
enter = materialSharedAxisYIn(initialOffsetY = { it / 4 }),
|
||||
exit = materialSharedAxisYOut(targetOffsetY = { it / 4 })
|
||||
) {
|
||||
Text(
|
||||
text = titleText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
FeedbackIconButton(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
@ -298,12 +337,14 @@ fun FlowPage(
|
||||
flowUiState.listState.animateScrollToItem(0)
|
||||
}
|
||||
onSearch = !onSearch
|
||||
if (onSearch) {
|
||||
delay(100)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
topBarTonalElevation.value.dp
|
||||
),
|
||||
containerColor = topBarContainerColor
|
||||
)
|
||||
)
|
||||
},
|
||||
@ -320,11 +361,7 @@ fun FlowPage(
|
||||
item {
|
||||
DisplayText(
|
||||
modifier = Modifier.padding(start = if (articleListFeedIcon.value) 30.dp else 0.dp),
|
||||
text = when {
|
||||
filterUiState.group != null -> filterUiState.group.name
|
||||
filterUiState.feed != null -> filterUiState.feed.name
|
||||
else -> filterUiState.filter.toName()
|
||||
},
|
||||
text = titleText,
|
||||
desc = "",
|
||||
)
|
||||
RYExtensibleVisibility(visible = markAsRead) {
|
||||
@ -387,7 +424,6 @@ fun FlowPage(
|
||||
articleListTonalElevation = articleListTonalElevation.value,
|
||||
isSwipeEnabled = { listState.isScrollInProgress },
|
||||
onClick = {
|
||||
onSearch = false
|
||||
navController.navigate("${RouteName.READING}/${it.article.id}") {
|
||||
launchSingleTop = true
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class FlowViewModel @Inject constructor(
|
||||
|
||||
fun sync() {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
rssService.get().doSync()
|
||||
rssService.get().doSyncOneTime()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,7 @@ package me.ash.reader.ui.page.home.reading
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.VisibilityThreshold
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -33,15 +30,14 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
|
||||
import me.ash.reader.infrastructure.preference.ReadingPageTonalElevationPreference
|
||||
import me.ash.reader.infrastructure.preference.ReadingRendererPreference
|
||||
import me.ash.reader.ui.component.base.CanBeDisabledIconButton
|
||||
import me.ash.reader.ui.component.base.RYExtensibleVisibility
|
||||
import me.ash.reader.ui.component.webview.BionicReadingIcon
|
||||
|
||||
@Composable
|
||||
@ -60,6 +56,7 @@ fun BottomBar(
|
||||
onReadAloud: () -> Unit = {},
|
||||
) {
|
||||
val tonalElevation = LocalReadingPageTonalElevation.current
|
||||
val isOutlined = tonalElevation == ReadingPageTonalElevationPreference.Outlined
|
||||
val renderer = LocalReadingRenderer.current
|
||||
|
||||
Box(
|
||||
@ -70,16 +67,20 @@ fun BottomBar(
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isShow,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
enter = expandVertically(expandFrom = Alignment.Top),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Top)
|
||||
) {
|
||||
val view = LocalView.current
|
||||
Column {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
thickness = 0.5f.dp
|
||||
)
|
||||
Surface() {
|
||||
if (isOutlined) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
thickness = 0.5f.dp
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.run { if (isOutlined) surface else surfaceContainer }
|
||||
) {
|
||||
// TODO: Component styles await refactoring
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
@ -5,9 +5,7 @@ import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
@ -16,9 +14,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -28,11 +24,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.isSpecified
|
||||
import androidx.compose.ui.unit.sp
|
||||
@ -40,7 +34,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
|
||||
@ -123,7 +116,7 @@ fun ReadingPage(
|
||||
TopBar(
|
||||
navController = navController,
|
||||
isShow = isShowToolBar,
|
||||
showDivider = showTopDivider,
|
||||
isScrolled = showTopDivider,
|
||||
title = readerState.title,
|
||||
link = readerState.link,
|
||||
onClick = { bringToTop = true },
|
||||
@ -194,7 +187,7 @@ fun ReadingPage(
|
||||
|
||||
|
||||
showTopDivider = snapshotFlow {
|
||||
scrollState.value != 0 || listState.firstVisibleItemIndex != 0
|
||||
scrollState.value >= 120 || listState.firstVisibleItemIndex != 0
|
||||
}.collectAsStateValue(initial = false)
|
||||
|
||||
CompositionLocalProvider(
|
||||
|
@ -22,6 +22,7 @@ import me.ash.reader.infrastructure.di.ApplicationScope
|
||||
import me.ash.reader.infrastructure.di.IODispatcher
|
||||
import me.ash.reader.infrastructure.rss.RssHelper
|
||||
import me.ash.reader.infrastructure.storage.AndroidImageDownloader
|
||||
import me.ash.reader.ui.ext.isNostrUri
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -47,6 +48,8 @@ class ReadingViewModel @Inject constructor(
|
||||
private val currentFeed: Feed?
|
||||
get() = readingUiState.value.articleWithFeed?.feed
|
||||
|
||||
private var initialArticleItems: List<ArticleFlowItem> = emptyList()
|
||||
|
||||
fun initData(articleId: String) {
|
||||
setLoading()
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
@ -96,10 +99,14 @@ class ReadingViewModel @Inject constructor(
|
||||
private suspend fun internalRenderFullContent() {
|
||||
setLoading()
|
||||
runCatching {
|
||||
rssHelper.parseFullContent(
|
||||
currentArticle?.link ?: "",
|
||||
currentArticle?.title ?: ""
|
||||
)
|
||||
if (currentArticle?.link?.isNostrUri() == true) {
|
||||
currentArticle?.fullContent.toString()
|
||||
} else {
|
||||
rssHelper.parseFullContent(
|
||||
currentArticle?.link ?: "",
|
||||
currentArticle?.title ?: ""
|
||||
)
|
||||
}
|
||||
}.onSuccess { content ->
|
||||
_readerState.update { it.copy(content = ReaderState.FullContent(content = content)) }
|
||||
}.onFailure { th ->
|
||||
@ -146,7 +153,11 @@ class ReadingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun prefetchArticleId(pagingItems: ItemSnapshotList<ArticleFlowItem>) {
|
||||
val items = pagingItems.items
|
||||
if (initialArticleItems.isEmpty()) {
|
||||
initialArticleItems = pagingItems.items
|
||||
}
|
||||
|
||||
val items = initialArticleItems
|
||||
val currentId = currentArticle?.id
|
||||
val index = items.indexOfFirst { item ->
|
||||
item is ArticleFlowItem.Article && item.articleWithFeed.article.id == currentId
|
||||
|
@ -1,6 +1,9 @@
|
||||
package me.ash.reader.ui.page.home.reading
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -15,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Palette
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
@ -27,20 +29,22 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.navigation.NavHostController
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalSharedContent
|
||||
import me.ash.reader.infrastructure.preference.ReadingPageTonalElevationPreference
|
||||
import me.ash.reader.ui.component.base.FeedbackIconButton
|
||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||
import me.ash.reader.ui.page.common.RouteName
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@ -48,7 +52,7 @@ import me.ash.reader.ui.page.common.RouteName
|
||||
fun TopBar(
|
||||
navController: NavHostController,
|
||||
isShow: Boolean,
|
||||
showDivider: Boolean = false,
|
||||
isScrolled: Boolean = false,
|
||||
title: String? = "",
|
||||
link: String? = "",
|
||||
onClick: (() -> Unit)? = null,
|
||||
@ -56,6 +60,13 @@ fun TopBar(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val sharedContent = LocalSharedContent.current
|
||||
val isOutlined =
|
||||
LocalReadingPageTonalElevation.current == ReadingPageTonalElevationPreference.Outlined
|
||||
|
||||
val containerColor by animateColorAsState(with(MaterialTheme.colorScheme) {
|
||||
if (isOutlined || !isScrolled) surface else surfaceContainer
|
||||
}, label = "", animationSpec = spring(stiffness = Spring.StiffnessMediumLow))
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -63,29 +74,30 @@ fun TopBar(
|
||||
.zIndex(1f),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(modifier = if (onClick == null) Modifier else Modifier.clickable(
|
||||
onClick = onClick,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.drawBehind { drawRect(containerColor) }
|
||||
) {
|
||||
Surface(
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(
|
||||
WindowInsets.statusBars
|
||||
.asPaddingValues()
|
||||
.calculateTopPadding()
|
||||
)
|
||||
) {}
|
||||
),
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isShow,
|
||||
enter = expandVertically(expandFrom = Alignment.Top),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Top)
|
||||
enter = expandVertically(expandFrom = Alignment.Bottom),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom)
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {},
|
||||
modifier = Modifier,
|
||||
modifier = if (onClick == null) Modifier else Modifier.clickable(
|
||||
onClick = onClick,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
navigationIcon = {
|
||||
FeedbackIconButton(
|
||||
@ -95,7 +107,8 @@ fun TopBar(
|
||||
) {
|
||||
onClose()
|
||||
}
|
||||
}, actions = {
|
||||
},
|
||||
actions = {
|
||||
FeedbackIconButton(
|
||||
modifier = Modifier.size(22.dp),
|
||||
imageVector = Icons.Outlined.Palette,
|
||||
@ -114,10 +127,11 @@ fun TopBar(
|
||||
) {
|
||||
sharedContent.share(context, title, link)
|
||||
}
|
||||
}, colors = TopAppBarDefaults.topAppBarColors()
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
|
||||
)
|
||||
}
|
||||
if (showDivider) {
|
||||
if (isOutlined && isScrolled) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
thickness = 0.5f.dp
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.addition
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
@ -55,6 +57,7 @@ fun AddFeverAccountDialog(
|
||||
var feverServerUrl by rememberSaveable { mutableStateOf("") }
|
||||
var feverUsername by rememberSaveable { mutableStateOf("") }
|
||||
var feverPassword by rememberSaveable { mutableStateOf("") }
|
||||
var feverClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
RYDialog(
|
||||
modifier = Modifier.padding(horizontal = 44.dp),
|
||||
@ -121,6 +124,19 @@ fun AddFeverAccountDialog(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
RYOutlineTextField(
|
||||
requestFocus = false,
|
||||
readOnly = accountUiState.isLoading,
|
||||
value = feverClientCertificateAlias,
|
||||
onValueChange = { feverClientCertificateAlias = it },
|
||||
label = stringResource(R.string.client_certificate),
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
feverClientCertificateAlias = alias ?: ""
|
||||
}, null, null, null, null)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -138,6 +154,7 @@ fun AddFeverAccountDialog(
|
||||
serverUrl = feverServerUrl,
|
||||
username = feverUsername,
|
||||
password = feverPassword,
|
||||
clientCertificateAlias = feverClientCertificateAlias,
|
||||
).toString(),
|
||||
)) { account, exception ->
|
||||
if (account == null) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.addition
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
@ -55,6 +57,7 @@ fun AddFreshRSSAccountDialog(
|
||||
var freshRSSServerUrl by rememberSaveable { mutableStateOf("") }
|
||||
var freshRSSUsername by rememberSaveable { mutableStateOf("") }
|
||||
var freshRSSPassword by rememberSaveable { mutableStateOf("") }
|
||||
var freshRSSClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
RYDialog(
|
||||
modifier = Modifier.padding(horizontal = 44.dp),
|
||||
@ -122,6 +125,19 @@ fun AddFreshRSSAccountDialog(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
RYOutlineTextField(
|
||||
requestFocus = false,
|
||||
readOnly = accountUiState.isLoading,
|
||||
value = freshRSSClientCertificateAlias,
|
||||
onValueChange = { freshRSSClientCertificateAlias = it },
|
||||
label = stringResource(R.string.client_certificate),
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
freshRSSClientCertificateAlias = alias ?: ""
|
||||
}, null, null, null, null)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -142,6 +158,7 @@ fun AddFreshRSSAccountDialog(
|
||||
serverUrl = freshRSSServerUrl,
|
||||
username = freshRSSUsername,
|
||||
password = freshRSSPassword,
|
||||
clientCertificateAlias = freshRSSClientCertificateAlias,
|
||||
).toString(),
|
||||
)) { account, exception ->
|
||||
if (account == null) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.addition
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
@ -56,6 +58,7 @@ fun AddGoogleReaderAccountDialog(
|
||||
var googleReaderServerUrl by rememberSaveable { mutableStateOf("") }
|
||||
var googleReaderUsername by rememberSaveable { mutableStateOf("") }
|
||||
var googleReaderPassword by rememberSaveable { mutableStateOf("") }
|
||||
var googleReaderClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
RYDialog(
|
||||
modifier = Modifier.padding(horizontal = 44.dp),
|
||||
@ -123,6 +126,19 @@ fun AddGoogleReaderAccountDialog(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
RYOutlineTextField(
|
||||
requestFocus = false,
|
||||
readOnly = accountUiState.isLoading,
|
||||
value = googleReaderClientCertificateAlias,
|
||||
onValueChange = { googleReaderClientCertificateAlias = it },
|
||||
label = stringResource(R.string.client_certificate),
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
googleReaderClientCertificateAlias = alias ?: ""
|
||||
}, null, null, null, null)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -143,6 +159,7 @@ fun AddGoogleReaderAccountDialog(
|
||||
serverUrl = googleReaderServerUrl,
|
||||
username = googleReaderUsername,
|
||||
password = googleReaderPassword,
|
||||
clientCertificateAlias = googleReaderClientCertificateAlias.takeIf { it.isNotEmpty() },
|
||||
).toString(),
|
||||
)) { account, exception ->
|
||||
if (account == null) {
|
||||
|
@ -1,7 +1,16 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.connection
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.R
|
||||
@ -17,6 +26,8 @@ fun LazyItemScope.FeverConnection(
|
||||
account: Account,
|
||||
viewModel: AccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val securityKey by remember {
|
||||
derivedStateOf { FeverSecurityKey(account.securityKey) }
|
||||
}
|
||||
@ -56,6 +67,16 @@ fun LazyItemScope.FeverConnection(
|
||||
passwordDialogVisible = true
|
||||
},
|
||||
) {}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.client_certificate),
|
||||
desc = securityKey.clientCertificateAlias,
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
securityKey.clientCertificateAlias = alias
|
||||
save(account, viewModel, securityKey)
|
||||
}, null, null, null, null)
|
||||
},
|
||||
) {}
|
||||
|
||||
TextFieldDialog(
|
||||
visible = serverUrlDialogVisible,
|
||||
|
@ -1,7 +1,16 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.connection
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.R
|
||||
@ -17,6 +26,8 @@ fun LazyItemScope.FreshRSSConnection(
|
||||
account: Account,
|
||||
viewModel: AccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val securityKey by remember {
|
||||
derivedStateOf { FreshRSSSecurityKey(account.securityKey) }
|
||||
}
|
||||
@ -56,6 +67,16 @@ fun LazyItemScope.FreshRSSConnection(
|
||||
passwordDialogVisible = true
|
||||
},
|
||||
) {}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.client_certificate),
|
||||
desc = securityKey.clientCertificateAlias,
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
securityKey.clientCertificateAlias = alias
|
||||
save(account, viewModel, securityKey)
|
||||
}, null, null, null, null)
|
||||
},
|
||||
) {}
|
||||
|
||||
TextFieldDialog(
|
||||
visible = serverUrlDialogVisible,
|
||||
|
@ -1,7 +1,16 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.connection
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.R
|
||||
@ -17,6 +26,8 @@ fun LazyItemScope.GoogleReaderConnection(
|
||||
account: Account,
|
||||
viewModel: AccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val securityKey by remember {
|
||||
derivedStateOf { GoogleReaderSecurityKey(account.securityKey) }
|
||||
}
|
||||
@ -56,6 +67,16 @@ fun LazyItemScope.GoogleReaderConnection(
|
||||
passwordDialogVisible = true
|
||||
},
|
||||
) {}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.client_certificate),
|
||||
desc = securityKey.clientCertificateAlias,
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
securityKey.clientCertificateAlias = alias
|
||||
save(account, viewModel, securityKey)
|
||||
}, null, null, null, null)
|
||||
},
|
||||
) {}
|
||||
|
||||
TextFieldDialog(
|
||||
visible = serverUrlDialogVisible,
|
||||
|
@ -62,9 +62,7 @@ fun FeedsPagePreview(
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
groupListTonalElevation.value.dp
|
||||
) onDark MaterialTheme.colorScheme.surface,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
) {
|
||||
|
@ -123,7 +123,7 @@ fun FeedsPageStylePage(
|
||||
(!groupListExpand).put(context, scope)
|
||||
}
|
||||
}
|
||||
SettingItem(
|
||||
/* SettingItem(
|
||||
title = stringResource(R.string.tonal_elevation),
|
||||
desc = "${groupListTonalElevation.value}dp",
|
||||
onClick = {
|
||||
@ -131,7 +131,7 @@ fun FeedsPageStylePage(
|
||||
},
|
||||
) {}
|
||||
Tips(text = stringResource(R.string.tips_group_list_tonal_elevation))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))*/
|
||||
}
|
||||
|
||||
// Filter Bar
|
||||
@ -242,7 +242,7 @@ fun FeedsPageStylePage(
|
||||
topBarTonalElevationDialogVisible = false
|
||||
}
|
||||
|
||||
RadioDialog(
|
||||
/* RadioDialog(
|
||||
visible = groupListTonalElevationDialogVisible,
|
||||
title = stringResource(R.string.tonal_elevation),
|
||||
options = FeedsGroupListTonalElevationPreference.values.map {
|
||||
@ -255,5 +255,5 @@ fun FeedsPageStylePage(
|
||||
}
|
||||
) {
|
||||
groupListTonalElevationDialogVisible = false
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
@ -12,13 +12,16 @@ import androidx.compose.material.icons.rounded.DoneAll
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.domain.model.article.Article
|
||||
import me.ash.reader.domain.model.article.ArticleWithFeed
|
||||
@ -55,8 +58,19 @@ fun FlowPagePreview(
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
) {
|
||||
val preview = generateArticleWithFeedPreview()
|
||||
val feed = preview.feed
|
||||
val article = preview.article
|
||||
|
||||
TopAppBar(
|
||||
title = {},
|
||||
title = {
|
||||
Text(
|
||||
text = feed.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
FeedbackIconButton(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
@ -83,10 +97,6 @@ fun FlowPagePreview(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
val preview = generateArticleWithFeedPreview()
|
||||
val feed = preview.feed
|
||||
val article = preview.article
|
||||
|
||||
ArticleItem(
|
||||
modifier = Modifier,
|
||||
feedName = feed.name,
|
||||
|
@ -115,7 +115,7 @@ fun FlowPageStylePage(
|
||||
topBarTonalElevationDialogVisible = true
|
||||
},
|
||||
) {}
|
||||
// Tips(text = stringResource(R.string.tips_top_bar_tonal_elevation))
|
||||
Tips(text = stringResource(R.string.tips_top_bar_tonal_elevation))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
|
@ -1,72 +0,0 @@
|
||||
package me.ash.reader.ui.page.settings.color.reading
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingDarkTheme
|
||||
import me.ash.reader.infrastructure.preference.ReadingDarkThemePreference
|
||||
import me.ash.reader.ui.component.base.DisplayText
|
||||
import me.ash.reader.ui.component.base.FeedbackIconButton
|
||||
import me.ash.reader.ui.component.base.RYScaffold
|
||||
import me.ash.reader.ui.page.settings.SettingItem
|
||||
import me.ash.reader.ui.theme.palette.onLight
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReadingDarkThemePage(
|
||||
navController: NavHostController,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val darkTheme = LocalReadingDarkTheme.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
RYScaffold(
|
||||
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||
navigationIcon = {
|
||||
FeedbackIconButton(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
content = {
|
||||
LazyColumn {
|
||||
item {
|
||||
DisplayText(text = stringResource(R.string.dark_theme), desc = "")
|
||||
}
|
||||
item {
|
||||
ReadingDarkThemePreference.values.map {
|
||||
SettingItem(
|
||||
title = it.toDesc(context),
|
||||
onClick = {
|
||||
it.put(context, scope)
|
||||
},
|
||||
) {
|
||||
RadioButton(selected = it == darkTheme, onClick = {
|
||||
it.put(context, scope)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -43,7 +43,6 @@ import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingDarkTheme
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingFonts
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
|
||||
@ -76,8 +75,6 @@ fun ReadingStylePage(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val readingTheme = LocalReadingTheme.current
|
||||
val darkTheme = LocalReadingDarkTheme.current
|
||||
val darkThemeNot = !darkTheme
|
||||
val tonalElevation = LocalReadingPageTonalElevation.current
|
||||
val fonts = LocalReadingFonts.current
|
||||
val autoHideToolbar = LocalReadingAutoHideToolbar.current
|
||||
@ -89,12 +86,17 @@ fun ReadingStylePage(
|
||||
var rendererDialogVisible by remember { mutableStateOf(false) }
|
||||
var fontsDialogVisible by remember { mutableStateOf(false) }
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
uri?.let {
|
||||
ExternalFonts(context, it, ExternalFonts.FontType.ReadingFont).copyToInternalStorage()
|
||||
ReadingFontsPreference.External.put(context, scope)
|
||||
} ?: context.showToast("Cannot get activity result with launcher")
|
||||
}
|
||||
val launcher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
uri?.let {
|
||||
ExternalFonts(
|
||||
context,
|
||||
it,
|
||||
ExternalFonts.FontType.ReadingFont
|
||||
).copyToInternalStorage()
|
||||
ReadingFontsPreference.External.put(context, scope)
|
||||
} ?: context.showToast("Cannot get activity result with launcher")
|
||||
}
|
||||
|
||||
RYScaffold(
|
||||
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||
@ -115,7 +117,8 @@ fun ReadingStylePage(
|
||||
|
||||
// Preview
|
||||
item {
|
||||
Row(modifier = Modifier.horizontalScroll(rememberScrollState())
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState())
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
ReadingThemePreference.values.map {
|
||||
@ -187,22 +190,6 @@ fun ReadingStylePage(
|
||||
desc = fonts.toDesc(context),
|
||||
onClick = { fontsDialogVisible = true },
|
||||
) {}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.dark_reading_theme),
|
||||
desc = darkTheme.toDesc(context),
|
||||
separatedActions = true,
|
||||
onClick = {
|
||||
navController.navigate(RouteName.READING_DARK_THEME) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
RYSwitch(
|
||||
activated = darkTheme.isDarkTheme()
|
||||
) {
|
||||
darkThemeNot.put(context, scope)
|
||||
}
|
||||
}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.auto_hide_toolbars),
|
||||
onClick = {
|
||||
@ -224,6 +211,10 @@ fun ReadingStylePage(
|
||||
onClick = { pullToSwitchArticle.toggle(context, scope) }) {
|
||||
RYSwitch(activated = pullToSwitchArticle.value)
|
||||
}
|
||||
Subtitle(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = stringResource(R.string.toolbars)
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.tonal_elevation),
|
||||
desc = "${tonalElevation.value}dp",
|
||||
@ -231,6 +222,7 @@ fun ReadingStylePage(
|
||||
tonalElevationDialogVisible = true
|
||||
},
|
||||
) {}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
@ -291,7 +283,7 @@ fun ReadingStylePage(
|
||||
}
|
||||
)
|
||||
|
||||
/* RadioDialog(
|
||||
RadioDialog(
|
||||
visible = tonalElevationDialogVisible,
|
||||
title = stringResource(R.string.tonal_elevation),
|
||||
options = ReadingPageTonalElevationPreference.values.map {
|
||||
@ -304,7 +296,7 @@ fun ReadingStylePage(
|
||||
}
|
||||
) {
|
||||
tonalElevationDialogVisible = false
|
||||
}*/
|
||||
}
|
||||
|
||||
RadioDialog(
|
||||
visible = rendererDialogVisible,
|
||||
|
@ -26,6 +26,7 @@ import me.ash.reader.infrastructure.preference.InitialFilterPreference
|
||||
import me.ash.reader.infrastructure.preference.InitialPagePreference
|
||||
import me.ash.reader.infrastructure.preference.LocalArticleListSwipeEndAction
|
||||
import me.ash.reader.infrastructure.preference.LocalArticleListSwipeStartAction
|
||||
import me.ash.reader.infrastructure.preference.LocalHideEmptyGroups
|
||||
import me.ash.reader.infrastructure.preference.LocalInitialFilter
|
||||
import me.ash.reader.infrastructure.preference.LocalInitialPage
|
||||
import me.ash.reader.infrastructure.preference.LocalMarkAsReadOnScroll
|
||||
@ -58,6 +59,7 @@ fun InteractionPage(
|
||||
val swipeToStartAction = LocalArticleListSwipeStartAction.current
|
||||
val swipeToEndAction = LocalArticleListSwipeEndAction.current
|
||||
val markAsReadOnScroll = LocalMarkAsReadOnScroll.current
|
||||
val hideEmptyGroups = LocalHideEmptyGroups.current
|
||||
val pullToSwitchArticle = LocalPullToSwitchArticle.current
|
||||
val openLink = LocalOpenLink.current
|
||||
val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current
|
||||
@ -112,6 +114,22 @@ fun InteractionPage(
|
||||
) {}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Subtitle(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = stringResource(R.string.feeds_page),
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.hide_empty_groups),
|
||||
onClick = {
|
||||
hideEmptyGroups.toggle(context, scope)
|
||||
},
|
||||
) {
|
||||
RYSwitch(activated = hideEmptyGroups.value) {
|
||||
hideEmptyGroups.toggle(context, scope)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Subtitle(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = stringResource(R.string.article_list),
|
||||
|
@ -0,0 +1,134 @@
|
||||
package me.ash.reader.ui.page.settings.tips
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.OpenLinkPreference
|
||||
import me.ash.reader.ui.component.base.RYAsyncImage
|
||||
import me.ash.reader.ui.ext.openURL
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SponsorDialog(modifier: Modifier = Modifier, onDismissRequest: () -> Unit) {
|
||||
ModalBottomSheet(modifier = modifier, onDismissRequest = onDismissRequest) {
|
||||
SponsorDialogContent()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun githubAvatar(login: String): String = "https://github.com/${login}.png"
|
||||
|
||||
@Composable
|
||||
private fun SponsorDialogContent(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
Column(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.become_a_sponsor),
|
||||
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Medium),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.sponsor_desc),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
SponsorItem(
|
||||
model = githubAvatar("Ashinch"),
|
||||
name = "Ash",
|
||||
description = "Lead Developer",
|
||||
) {
|
||||
context.openURL("https://ash7.io/sponsor/", openLink = OpenLinkPreference.default)
|
||||
}
|
||||
SponsorItem(
|
||||
model = githubAvatar("JunkFood02"),
|
||||
name = "junkfood",
|
||||
description = "Maintainer",
|
||||
) {
|
||||
context.openURL(
|
||||
"https://github.com/sponsors/JunkFood02",
|
||||
openLink = OpenLinkPreference.default
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SponsorItem(
|
||||
modifier: Modifier = Modifier,
|
||||
model: Any?,
|
||||
name: String,
|
||||
description: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
enabled = true,
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(vertical = 12.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RYAsyncImage(
|
||||
data = model,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.aspectRatio(1f)
|
||||
.clip(CircleShape),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
Text(name, style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
FilledTonalButton(onClick = onClick, interactionSource = interactionSource) {
|
||||
Text(stringResource(R.string.sponsor))
|
||||
}
|
||||
}
|
||||
}
|
@ -85,6 +85,8 @@ fun TipsAndSupportPage(
|
||||
targetValue = pressAMP,
|
||||
animationSpec = tween()
|
||||
)
|
||||
|
||||
var showSponsorDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
currentVersion = context.getCurrentVersion().toString()
|
||||
@ -210,7 +212,7 @@ fun TipsAndSupportPage(
|
||||
) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||
context.showToast(context.getString(R.string.coming_soon))
|
||||
showSponsorDialog = true
|
||||
})
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
@ -250,6 +252,9 @@ fun TipsAndSupportPage(
|
||||
)
|
||||
|
||||
UpdateDialog()
|
||||
if (showSponsorDialog) {
|
||||
SponsorDialog { showSponsorDialog = false }
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
@ -11,6 +11,8 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import me.ash.reader.infrastructure.preference.LocalBasicFonts
|
||||
import me.ash.reader.infrastructure.preference.LocalThemeIndex
|
||||
import me.ash.reader.ui.theme.palette.FixedColorRoles
|
||||
import me.ash.reader.ui.theme.palette.LocalFixedColorRoles
|
||||
import me.ash.reader.ui.theme.palette.LocalTonalPalettes
|
||||
import me.ash.reader.ui.theme.palette.TonalPalettes
|
||||
import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
|
||||
@ -59,15 +61,21 @@ fun AppTheme(
|
||||
LocalTonalPalettes provides tonalPalettes.apply { Preparing() },
|
||||
LocalTextStyle provides LocalTextStyle.current.applyTextDirection()
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme =
|
||||
if (useDarkTheme) dynamicDarkColorScheme()
|
||||
else dynamicLightColorScheme(),
|
||||
typography = LocalBasicFonts.current.asTypography(LocalContext.current)
|
||||
.applyTextDirection(),
|
||||
shapes = Shapes,
|
||||
content = content,
|
||||
)
|
||||
val lightColors = dynamicLightColorScheme()
|
||||
val darkColors = dynamicDarkColorScheme()
|
||||
CompositionLocalProvider(
|
||||
LocalFixedColorRoles provides FixedColorRoles.fromColorSchemes(
|
||||
lightColors, darkColors
|
||||
)
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = if (useDarkTheme) darkColors else lightColors,
|
||||
typography = LocalBasicFonts.current.asTypography(LocalContext.current)
|
||||
.applyTextDirection(),
|
||||
shapes = Shapes,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ fun dynamicLightColorScheme(): ColorScheme {
|
||||
@Composable
|
||||
fun dynamicDarkColorScheme(): ColorScheme {
|
||||
val palettes = LocalTonalPalettes.current
|
||||
val amoledDarkTheme = LocalAmoledDarkTheme.current
|
||||
val useAmoledDarkTheme = LocalAmoledDarkTheme.current.value
|
||||
|
||||
return darkColorScheme(
|
||||
primary = palettes primary 80,
|
||||
@ -70,7 +70,7 @@ fun dynamicDarkColorScheme(): ColorScheme {
|
||||
onTertiaryContainer = palettes tertiary 90,
|
||||
background = palettes neutral 10,
|
||||
onBackground = palettes neutral 90,
|
||||
surface = palettes neutral if (amoledDarkTheme.value) 0 else 10,
|
||||
surface = palettes neutral 6,
|
||||
onSurface = palettes neutral 90,
|
||||
surfaceVariant = palettes neutralVariant 30,
|
||||
onSurfaceVariant = palettes neutralVariant 80,
|
||||
@ -86,7 +86,16 @@ fun dynamicDarkColorScheme(): ColorScheme {
|
||||
surfaceContainer = palettes neutral 12,
|
||||
surfaceContainerHigh = palettes neutral 17,
|
||||
surfaceContainerHighest = palettes neutral 22,
|
||||
)
|
||||
).run {
|
||||
if (useAmoledDarkTheme) copy(
|
||||
surface = Color.Black,
|
||||
surfaceContainerHighest = palettes neutral 8,
|
||||
surfaceContainerHigh = palettes neutral 6,
|
||||
surfaceContainer = palettes neutral 4,
|
||||
surfaceContainerLow = palettes neutral 4,
|
||||
surfaceContainerLowest = Color.Black,
|
||||
) else this
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -111,7 +120,6 @@ infix fun Color.alwaysLight(isAlways: Boolean): Color {
|
||||
colorScheme.error -> colorScheme.onError
|
||||
colorScheme.surface -> colorScheme.onSurface
|
||||
colorScheme.surfaceVariant -> colorScheme.onSurfaceVariant
|
||||
colorScheme.error -> colorScheme.onError
|
||||
colorScheme.primaryContainer -> colorScheme.onPrimaryContainer
|
||||
colorScheme.secondaryContainer -> colorScheme.onSecondaryContainer
|
||||
colorScheme.tertiaryContainer -> colorScheme.onTertiaryContainer
|
||||
@ -125,7 +133,6 @@ infix fun Color.alwaysLight(isAlways: Boolean): Color {
|
||||
colorScheme.onError -> colorScheme.error
|
||||
colorScheme.onSurface -> colorScheme.surface
|
||||
colorScheme.onSurfaceVariant -> colorScheme.surfaceVariant
|
||||
colorScheme.onError -> colorScheme.error
|
||||
colorScheme.onPrimaryContainer -> colorScheme.primaryContainer
|
||||
colorScheme.onSecondaryContainer -> colorScheme.secondaryContainer
|
||||
colorScheme.onTertiaryContainer -> colorScheme.tertiaryContainer
|
||||
@ -153,7 +160,6 @@ infix fun Color.alwaysDark(isAlways: Boolean): Color {
|
||||
colorScheme.error -> colorScheme.onError
|
||||
colorScheme.surface -> colorScheme.onSurface
|
||||
colorScheme.surfaceVariant -> colorScheme.onSurfaceVariant
|
||||
colorScheme.error -> colorScheme.onError
|
||||
colorScheme.primaryContainer -> colorScheme.onPrimaryContainer
|
||||
colorScheme.secondaryContainer -> colorScheme.onSecondaryContainer
|
||||
colorScheme.tertiaryContainer -> colorScheme.onTertiaryContainer
|
||||
@ -167,7 +173,6 @@ infix fun Color.alwaysDark(isAlways: Boolean): Color {
|
||||
colorScheme.onError -> colorScheme.error
|
||||
colorScheme.onSurface -> colorScheme.surface
|
||||
colorScheme.onSurfaceVariant -> colorScheme.surfaceVariant
|
||||
colorScheme.onError -> colorScheme.error
|
||||
colorScheme.onPrimaryContainer -> colorScheme.primaryContainer
|
||||
colorScheme.onSecondaryContainer -> colorScheme.secondaryContainer
|
||||
colorScheme.onTertiaryContainer -> colorScheme.tertiaryContainer
|
||||
|
@ -0,0 +1,71 @@
|
||||
package me.ash.reader.ui.theme.palette
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val LocalFixedColorRoles = staticCompositionLocalOf {
|
||||
FixedColorRoles.fromColorSchemes(
|
||||
lightColors = lightColorScheme(),
|
||||
darkColors = darkColorScheme(),
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class FixedColorRoles(
|
||||
val primaryFixed: Color,
|
||||
val primaryFixedDim: Color,
|
||||
val onPrimaryFixed: Color,
|
||||
val onPrimaryFixedVariant: Color,
|
||||
val secondaryFixed: Color,
|
||||
val secondaryFixedDim: Color,
|
||||
val onSecondaryFixed: Color,
|
||||
val onSecondaryFixedVariant: Color,
|
||||
val tertiaryFixed: Color,
|
||||
val tertiaryFixedDim: Color,
|
||||
val onTertiaryFixed: Color,
|
||||
val onTertiaryFixedVariant: Color,
|
||||
) {
|
||||
companion object {
|
||||
internal val unspecified =
|
||||
FixedColorRoles(
|
||||
primaryFixed = Color.Unspecified,
|
||||
primaryFixedDim = Color.Unspecified,
|
||||
onPrimaryFixed = Color.Unspecified,
|
||||
onPrimaryFixedVariant = Color.Unspecified,
|
||||
secondaryFixed = Color.Unspecified,
|
||||
secondaryFixedDim = Color.Unspecified,
|
||||
onSecondaryFixed = Color.Unspecified,
|
||||
onSecondaryFixedVariant = Color.Unspecified,
|
||||
tertiaryFixed = Color.Unspecified,
|
||||
tertiaryFixedDim = Color.Unspecified,
|
||||
onTertiaryFixed = Color.Unspecified,
|
||||
onTertiaryFixedVariant = Color.Unspecified,
|
||||
)
|
||||
|
||||
@Stable
|
||||
internal fun fromColorSchemes(
|
||||
lightColors: ColorScheme,
|
||||
darkColors: ColorScheme,
|
||||
): FixedColorRoles {
|
||||
return FixedColorRoles(
|
||||
primaryFixed = lightColors.primaryContainer,
|
||||
onPrimaryFixed = lightColors.onPrimaryContainer,
|
||||
onPrimaryFixedVariant = darkColors.primaryContainer,
|
||||
secondaryFixed = lightColors.secondaryContainer,
|
||||
onSecondaryFixed = lightColors.onSecondaryContainer,
|
||||
onSecondaryFixedVariant = darkColors.secondaryContainer,
|
||||
tertiaryFixed = lightColors.tertiaryContainer,
|
||||
onTertiaryFixed = lightColors.onTertiaryContainer,
|
||||
onTertiaryFixedVariant = darkColors.tertiaryContainer,
|
||||
primaryFixedDim = darkColors.primary,
|
||||
secondaryFixedDim = darkColors.secondary,
|
||||
tertiaryFixedDim = darkColors.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -82,7 +82,7 @@ data class TonalPalettes(
|
||||
infix fun neutral(tone: TonalValue): Color = neutral.getOrPut(tone) {
|
||||
zcamLch(
|
||||
L = tone.toZcamLightness(),
|
||||
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 12.0,
|
||||
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 8.0,
|
||||
h = hue,
|
||||
).clampToRgb().toColor()
|
||||
}
|
||||
|
@ -314,4 +314,8 @@
|
||||
<string name="browse_bionic_reading_tips">قرا عن القراية البايونيك ع <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="use_bionic_reading">ستعمل القراية البايونيك</string>
|
||||
<string name="bionic_reading_tips">شو القراية البايونيك؟</string>
|
||||
<string name="toolbars">شريط الأدوات</string>
|
||||
<string name="mark_as_read_on_scroll">علّم إنه منرقرا عال تمرير</string>
|
||||
<string name="become_a_sponsor">صير سپانسر</string>
|
||||
<string name="sponsor_desc">نحن منعمل و من جدد هيدا الآپ المجاني و مفتوح المصدر برات سعات العمل تبعنا.إذا بيعجبك الآپ، پليز فكر ب دعمنا ب تبرع زغير! ☕️</string>
|
||||
</resources>
|
@ -327,4 +327,7 @@
|
||||
<string name="bionic_reading_tips">ما هي القراءة الحيوية؟?</string>
|
||||
<string name="browse_bionic_reading_tips">تعرف على المزيد على <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="mark_as_read_on_scroll">وضع علامة مقروء على التمرير</string>
|
||||
<string name="become_a_sponsor">كن داعما</string>
|
||||
<string name="sponsor_desc">نحن نبني ونحافظ على هذا التطبيق المجاني والمفتوح المصدر في غير ساعات العمل لدينا. إذا كنت تستمتع به، فيرجى التفكير في دعمنا بتبرع صغير! ☕️</string>
|
||||
<string name="toolbars">شريط الأدوات</string>
|
||||
</resources>
|
@ -312,4 +312,8 @@
|
||||
<string name="only_available_on_webview">Предлага се само в WebView</string>
|
||||
<string name="native_component">Собствен компонент</string>
|
||||
<string name="use_bionic_reading">Използвай на бионично четене</string>
|
||||
<string name="mark_as_read_on_scroll">Маркирай като прочетено при превъртане</string>
|
||||
<string name="become_a_sponsor">Станете спонсор</string>
|
||||
<string name="sponsor_desc">Ние създаваме и поддържаме това безплатно приложение с отворен код в извънработно време. Ако ви харесва, моля, помислете дали да ни подкрепите с малко дарение! ☕️</string>
|
||||
<string name="toolbars">Ленти с инструменти</string>
|
||||
</resources>
|
@ -317,4 +317,8 @@
|
||||
<string name="about">Informace</string>
|
||||
<string name="browse_bionic_reading_tips">Více informací najdete na <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="bionic_reading_tips">Co je bionické čtení?</string>
|
||||
<string name="mark_as_read_on_scroll">Označit jako přečtené při posouvání</string>
|
||||
<string name="become_a_sponsor">Staňte se sponzorem</string>
|
||||
<string name="sponsor_desc">Tuto bezplatnou aplikaci s otevřeným zdrojovým kódem vytváříme a udržujeme v našem volném čase. Pokud se vám líbí, zvažte prosím, zda nás podpoříte malým darem! ☕️</string>
|
||||
<string name="toolbars">Nástrojové lišty</string>
|
||||
</resources>
|
@ -314,4 +314,8 @@
|
||||
<string name="about">Über</string>
|
||||
<string name="bionic_reading_tips">Was ist bionisches Lesen?</string>
|
||||
<string name="browse_bionic_reading_tips">Erfahren Sie mehr unter <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="toolbars">Werkzeugleisten</string>
|
||||
<string name="mark_as_read_on_scroll">Beim Scrollen als gelesen markieren</string>
|
||||
<string name="become_a_sponsor">Sponsor werden</string>
|
||||
<string name="sponsor_desc">Wir entwickeln und pflegen diese kostenlose Open-Source-Anwendung in unserer Freizeit. Wenn sie Ihnen gefällt, unterstützen Sie uns bitte mit einer kleinen Spende! ☕️</string>
|
||||
</resources>
|
4
app/src/main/res/values-el/strings.xml
Normal file
4
app/src/main/res/values-el/strings.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="all">Όλα</string>
|
||||
</resources>
|
@ -321,4 +321,7 @@
|
||||
<string name="use_bionic_reading">Utiliza la lectura biónica</string>
|
||||
<string name="about">Acerca de</string>
|
||||
<string name="mark_as_read_on_scroll">Marcar como leído al desplazarse</string>
|
||||
<string name="become_a_sponsor">Conviértete en patrocinador</string>
|
||||
<string name="sponsor_desc">Construimos y mantenemos esta aplicación gratuita y de código abierto en nuestro tiempo libre. Si la disfrutas, ¡considera apoyarnos con una pequeña donación! ☕️</string>
|
||||
<string name="toolbars">Barras de herramientas</string>
|
||||
</resources>
|
@ -26,4 +26,145 @@
|
||||
<string name="confirm">Kinnita</string>
|
||||
<string name="cancel">Katkesta</string>
|
||||
<string name="allow">Luba</string>
|
||||
<string name="create_new_group">Loo uus grupp</string>
|
||||
<string name="name">Nimi</string>
|
||||
<string name="defaults">Vaikimisi</string>
|
||||
<string name="unknown">Teadmata</string>
|
||||
<string name="empty">Tühi</string>
|
||||
<string name="back">Tagasi</string>
|
||||
<string name="go_to">Mine</string>
|
||||
<string name="settings">Seadistused</string>
|
||||
<string name="refresh">Värskenda andmeid</string>
|
||||
<string name="search">Otsi</string>
|
||||
<string name="searching">Otsime…</string>
|
||||
<string name="subscribe">Telli</string>
|
||||
<string name="already_subscribed">Tellimus on juba olemas</string>
|
||||
<string name="clear">Tühjenda</string>
|
||||
<string name="paste">Aseta</string>
|
||||
<string name="feed_or_site_url">Uudistevoo või veebisaidi aadress</string>
|
||||
<string name="import_from_opml">Impordi uudisvoog OPML-failist</string>
|
||||
<string name="preset">Eelseadistused</string>
|
||||
<string name="selected">Valitud</string>
|
||||
<string name="allow_notification">Luba teavitused</string>
|
||||
<string name="all_allow_notification_tips">Luba kõikidel „%1$s“ rühma uudisvoogudel saata teavitusi</string>
|
||||
<string name="all_deny_notification_toast">Kõik „%1$s“ rühma uudisvoogude teavitused on keelatud</string>
|
||||
<string name="rename">Muuda nime</string>
|
||||
<string name="change_url">Muuda võrguaadressi</string>
|
||||
<string name="rename_toast">Muutsime uueks nimeks „%1$s“</string>
|
||||
<string name="open_with">Ava rakendusega %1$s</string>
|
||||
<string name="options">Valikud</string>
|
||||
<string name="delete">Kustuta</string>
|
||||
<string name="delete_toast">„%1$s“ on kustutatud</string>
|
||||
<string name="unsubscribe">Lõpeta tellimus</string>
|
||||
<string name="delete_group">Kustuta grupp</string>
|
||||
<string name="unsubscribe_tips">Lõpetada „%1$s“ tellimus ja kustutada kõik tema arhiveeritud artiklid</string>
|
||||
<string name="today">Täna</string>
|
||||
<string name="delete_group_tips">Kustuta „%1$s“ grupp ja kõik sealsed uudisvood ja nende arhiveeritud artiklid</string>
|
||||
<string name="group_option_tips">Järgnevad valikud kehtivad kõikidele uudisvoogudele selles grupis</string>
|
||||
<string name="yesterday">Eile</string>
|
||||
<string name="date_at_time">%1$s kell %2$s</string>
|
||||
<string name="mark_as_read">Märgi loetuks</string>
|
||||
<string name="mark_all_as_read">Märgi kõik loetuks</string>
|
||||
<string name="mark_as_unread">Märgi mitteloetuks</string>
|
||||
<string name="mark_as_starred">Märgi lemmikuks</string>
|
||||
<string name="mark_as_unstar">Eemalda lemmiku märge</string>
|
||||
<string name="mark_as_read_one_day">Märgi loetuks, kui vanus on üle 1 päeva</string>
|
||||
<string name="mark_as_read_three_days">Märgi loetuks, kui vanus on üle 3 päeva</string>
|
||||
<string name="mark_as_read_seven_days">Märgi loetuks, kui vanus on üle 7 päeva</string>
|
||||
<string name="one_day">1 pv</string>
|
||||
<string name="three_days">3 pv</string>
|
||||
<string name="seven_days">7 pv</string>
|
||||
<string name="close">Sulge</string>
|
||||
<string name="languages">Keeled</string>
|
||||
<string name="help_translate">Aita tõlkida</string>
|
||||
<string name="use_device_languages">Kasuta nutiseadme keelt</string>
|
||||
<string name="tips_and_support">Nõuanded ja kasutajatugi</string>
|
||||
<string name="wallpaper_colors">Taustapildi värvid</string>
|
||||
<string name="open_source_licenses">Avatud lähtekoodiga tarkvara litsentsid</string>
|
||||
<string name="change_log">Muudatuste logi</string>
|
||||
<string name="update">Uuenda</string>
|
||||
<string name="skip_this_version">Jäta see versioon vahele</string>
|
||||
<string name="checking_updates">Kontrollin uuendusi…</string>
|
||||
<string name="is_latest_version">See on rakenduse viimane versioon</string>
|
||||
<string name="check_failure">Uuenduste kontrollimine ei õnnestunud</string>
|
||||
<string name="download_failure">Uuenduste allalaadimine ei õnnestunud</string>
|
||||
<string name="rate_limit">Päringutel kehtib mahupiirang</string>
|
||||
<string name="help">Abiteave</string>
|
||||
<string name="on_start">Rakenduse käivitamisel</string>
|
||||
<string name="font_size">Kirjatüübi suurus</string>
|
||||
<string name="letter_spacing">Tähevahed</string>
|
||||
<string name="line_spacing">Ridade vahed</string>
|
||||
<string name="alignment">Joondumine</string>
|
||||
<string name="all_allow_notification_toast">Kõik „%1$s“ rühma uudisvoogude teavitused on lubatud</string>
|
||||
<string name="parse_full_content">Töötle terviksisu</string>
|
||||
<string name="all_parse_full_content_tips">Töötleme „%1$s“ grupi kõikide artiklite terviksisu</string>
|
||||
<string name="all_parse_full_content_toast">Töötleme „%1$s“ grupi kõikide artiklite terviksisu</string>
|
||||
<string name="all_deny_parse_full_content_toast">„%1$s“ grupi kõikide artiklite terviksisu töötlemine on lõppenud</string>
|
||||
<string name="clear_articles">Eemalda artiklid</string>
|
||||
<string name="clear_articles_in_feed_toast">Eemaldasime kõik „%1$s“ uudisvoo arhiveeritud artiklid</string>
|
||||
<string name="clear_articles_in_group_toast">Eemaldasime kõik „%1$s“ grupi arhiveeritud artiklid</string>
|
||||
<string name="clear_articles_feed_tips">Eemalda kõik „%1$s“ uudisvoo arhiveeritud artiklid</string>
|
||||
<string name="clear_articles_group_tips">Eemalda kõik „%1$s“ grupi arhiveeritud artiklid</string>
|
||||
<string name="add_to_group">Lisa gruppi</string>
|
||||
<string name="move_to_group">Teisalda gruppi</string>
|
||||
<string name="all_move_to_group_tips">Teisalda kõik uudisvood „%1$s“ grupist „%2$s“ gruppi</string>
|
||||
<string name="all_move_to_group_toast">Teisaldasime kõik uudisvood „%1$s“ gruppi</string>
|
||||
<string name="search_for_in">Otsing: %1$s asukohas „%2$s“</string>
|
||||
<string name="search_for">Otsing: %1$s</string>
|
||||
<string name="get_new_updates">Otsi uuendusi</string>
|
||||
<string name="get_new_updates_desc">Uus versioon %1$s on saadaval</string>
|
||||
<string name="specific_browser_name">Brauser: %1$s</string>
|
||||
<string name="default_browser">Sundkorras kasuta vaikimisi brauserit</string>
|
||||
<string name="always_ask">Alati küsi</string>
|
||||
<string name="open_link_specific_browser">Brauser</string>
|
||||
<string name="open_link_something_wrong">Vea tekkemise tõttu eirame „Ava link“ seadistust.</string>
|
||||
<string name="open_link_ask_dialog_title">Ava rakendusega…</string>
|
||||
<string name="include_additional_info">Kaasa täiendav teave</string>
|
||||
<string name="in_coding">Arendamisel</string>
|
||||
<string name="accounts">Kasutajakontod</string>
|
||||
<string name="accounts_desc">Kohalik, FreshRSS</string>
|
||||
<string name="color_and_style">Värv ja stiil</string>
|
||||
<string name="color_and_style_desc">Kujundus, värvivalik, kirjatüübid</string>
|
||||
<string name="interaction">Liidese kasutamine</string>
|
||||
<string name="coming_soon">Avaldatakse varsti</string>
|
||||
<string name="interaction_desc">Avaleht, haptiline tagasiside</string>
|
||||
<string name="tips_and_support_desc">Rakenduse teave, avatud lähtekoodi litsentsid</string>
|
||||
<string name="welcome">Tere tulemnast</string>
|
||||
<string name="agree">Nõustun</string>
|
||||
<string name="no_palettes">Värvipalette pole</string>
|
||||
<string name="only_android_8.1_plus">Vaid Android 8.1+ jaoks</string>
|
||||
<string name="basic_colors">Põhivärvid</string>
|
||||
<string name="primary_color">Esmane värv</string>
|
||||
<string name="primary_color_hint">Näiteks #666666 või 666666</string>
|
||||
<string name="appearance">Välimus</string>
|
||||
<string name="style">Vaadete kujundus</string>
|
||||
<string name="dark_theme">Tume kujundus</string>
|
||||
<string name="use_device_theme">Kasuta seadme kujundust</string>
|
||||
<string name="on">Sees</string>
|
||||
<string name="off">Väljas</string>
|
||||
<string name="other">Muu</string>
|
||||
<string name="amoled_dark_theme">AMOLEDi tume kujundus</string>
|
||||
<string name="tonal_elevation">Tooninihe</string>
|
||||
<string name="reading_fonts">Kirjatüübid lugemiseks</string>
|
||||
<string name="basic_fonts">Põhilised kirjatüübid</string>
|
||||
<string name="feeds_page">Uudisvoogude leht</string>
|
||||
<string name="flow_page">Uudislindi leht</string>
|
||||
<string name="reading_page">Lugemisvaade</string>
|
||||
<string name="sponsor">Sponsor</string>
|
||||
<string name="update_link">https://api.github.com/repos/Ashinch/ReadYou/releases/latest</string>
|
||||
<string name="initial_filter">Algne filter</string>
|
||||
<string name="initial_page">Algne leht</string>
|
||||
<string name="preview_feed_name">Reddit</string>
|
||||
<string name="value">väärtus</string>
|
||||
<string name="browse_tos_tips">Loe <i><u>kasutustingimusi ja privaatsuspoliitikat</u></i></string>
|
||||
<string name="terms_of_service">Kasutustingimused</string>
|
||||
<string name="tos_tips">Jätkamiseks palun tutvu ja nõustu Read You kasutustingimuste ja privaatsuspoliitikaga.</string>
|
||||
<string name="horizontal_padding">Rõhtne veeris</string>
|
||||
<string name="article_date">Artikli avaldamise aeg</string>
|
||||
<string name="article_desc">Artikli kirjeldused</string>
|
||||
<string name="article_images">Artikli pildid</string>
|
||||
<string name="feed_names">Uudisvoogude nimed</string>
|
||||
<string name="feed_favicons">Uudisvoogude favikonid</string>
|
||||
<string name="article_date_sticky_header">Kleepuv avaldamise kuupäeva päis (katseline)</string>
|
||||
<string name="article_list">Artiklite loend</string>
|
||||
</resources>
|
@ -315,4 +315,8 @@
|
||||
<string name="only_available_on_webview">Hanya tersedia di WebView</string>
|
||||
<string name="use_bionic_reading">Gunakan Bionic Reading</string>
|
||||
<string name="bionic_reading_tips">Apa itu Bionic Reading?</string>
|
||||
<string name="mark_as_read_on_scroll">Baca sebagai dibaca saat digulir</string>
|
||||
<string name="become_a_sponsor">Jadilah sponsor</string>
|
||||
<string name="toolbars">Bilah alat</string>
|
||||
<string name="sponsor_desc">Kami membangun dan memelihara aplikasi sumber terbuka gratis ini di luar jam kerja. Jika Anda menikmatinya, mohon pertimbangkan untuk mendukung kami dengan donasi kecil! ☕️</string>
|
||||
</resources>
|
@ -318,4 +318,7 @@
|
||||
<string name="native_component">Componente nativo</string>
|
||||
<string name="only_available_on_webview">Disponibile esclusivamente in WebView</string>
|
||||
<string name="mark_as_read_on_scroll">Contrassegna come letto allo scroll</string>
|
||||
<string name="become_a_sponsor">Diventa uno sponsor</string>
|
||||
<string name="sponsor_desc">Sviluppiamo e miglioriamo questa applicazione, gratuita ed open source, nel nostro tempo libero. Se ti piace, considera la possibilità di sostenerci con una piccola donazione! ☕</string>
|
||||
<string name="toolbars">Barre degli strumenti</string>
|
||||
</resources>
|
@ -216,4 +216,30 @@
|
||||
<string name="always_ask">תמיד לשאול</string>
|
||||
<string name="unfold_more">הרחבה של הכול</string>
|
||||
<string name="unfold_less">צמצום של הכול</string>
|
||||
<string name="group_option_tips">האפשרויות הבאות חלות על כל העדכונים בקבוצה זו</string>
|
||||
<string name="clear_articles_feed_tips">נקה את כל המאמרים המאוחסנים בארכיון בעדכון \"%1$s\"</string>
|
||||
<string name="all_move_to_group_toast">העביר את כל העדכונים לקבוצת \"%1$s\"</string>
|
||||
<string name="all_move_to_group_tips">העבר את כל העדכונים מקבוצת \"%1$s\" לקבוצה \"%2$s\"</string>
|
||||
<string name="all_parse_full_content_toast">ניתוח תוכן מלא של כל המאמרים בקבוצת \"%1$s\"</string>
|
||||
<string name="all_deny_parse_full_content_toast">לא עוד ניתוח תוכן מלא של כל המאמרים בקבוצת \"%1$s\"</string>
|
||||
<string name="clear_articles_in_feed_toast">ניקה את כל המאמרים המאוחסנים בארכיון בעדכון \"%1$s\"</string>
|
||||
<string name="clear_articles_in_group_toast">ניקה את כל המאמרים המאוחסנים בארכיון בקבוצה \"%1$s\"</string>
|
||||
<string name="clear_articles_group_tips">נקה את כל המאמרים המאוחסנים בארכיון בקבוצת \"%1$s\"</string>
|
||||
<string name="rename_toast">השם שונה ל-\"%1$s\"</string>
|
||||
<string name="open_with">פתח את %1$s</string>
|
||||
<string name="delete_toast">\"%1$s\" נמחק</string>
|
||||
<string name="unsubscribe_tips">בטל את הרישום ל-\"%1$s\" ומחק את כל המאמרים שהועברו לארכיון בו</string>
|
||||
<string name="delete_group_tips">מחק את הקבוצה \"%1$s\" ואת כל העדכונים והמאמרים המאוחסנים בארכיון שלה</string>
|
||||
<string name="search_for_in">חפש %1$s פריטים ב-\"%2$s\"</string>
|
||||
<string name="search_for">חפש %1$s פריטים</string>
|
||||
<string name="mark_as_read">סמן כנקרא</string>
|
||||
<string name="mark_all_as_read">סמן הכל כנקרא</string>
|
||||
<string name="mark_as_unread">סמן כלא נקרא</string>
|
||||
<string name="mark_as_starred">סמן בכוכב</string>
|
||||
<string name="mark_as_unstar">בטל כוכב</string>
|
||||
<string name="mark_as_read_one_day">סמן כנקראה לפני יותר מיום אחד</string>
|
||||
<string name="mark_as_read_three_days">סמן כנקראה לפני יותר מ-3 ימים</string>
|
||||
<string name="mark_as_read_seven_days">סמן כנקראה לפני יותר מ-7 ימים</string>
|
||||
<string name="in_coding">בקידוד</string>
|
||||
<string name="tos_tips">אנא קרא והסכם לתנאי השירות ומדיניות הפרטיות כדי להמשיך.</string>
|
||||
</resources>
|
@ -116,4 +116,10 @@
|
||||
<string name="dark_theme">ഡാർക്ക് തീം</string>
|
||||
<string name="use_device_theme">ഉപകരണത്തിന്റെ തീം ഉപയോഗിക്കുക</string>
|
||||
<string name="skip_this_version">ഈ പതിപ്പ് ഒഴിവാക്കുക</string>
|
||||
<string name="appearance">സ്വരൂപം</string>
|
||||
<string name="style">ശൈലി</string>
|
||||
<string name="primary_color">പ്രാഥമിക നിറം</string>
|
||||
<string name="primary_color_hint">#666666 അല്ലെങ്കിൽ 666666 പോലെ</string>
|
||||
<string name="initial_filter">ആദ്യഘടകം</string>
|
||||
<string name="preview_feed_name">റെഡ്ഡിറ്റ്</string>
|
||||
</resources>
|
@ -30,7 +30,7 @@
|
||||
<string name="defaults">Domyślne</string>
|
||||
<string name="unknown">Nieznane</string>
|
||||
<string name="back">Wstecz</string>
|
||||
<string name="go_to">Idź Do</string>
|
||||
<string name="go_to">Idź do</string>
|
||||
<string name="settings">Ustawienia</string>
|
||||
<string name="refresh">Odśwież</string>
|
||||
<string name="search">Wyszukaj</string>
|
||||
@ -72,7 +72,7 @@
|
||||
<string name="unsubscribe">Odsubskrybuj</string>
|
||||
<string name="unsubscribe_tips">Anuluj subskrypcję kanału \"%1$s\" i usuń wszystkie zawarte w nim artykuły</string>
|
||||
<string name="delete_group">Usuń grupę</string>
|
||||
<string name="delete_group_tips">Usuń grupę \"%1$s\" i wszystkie zawarte w niej artykuły.</string>
|
||||
<string name="delete_group_tips">Usuń grupę \"%1$s\" oraz wszystkie kanały i zarchiwizowane artykuły, które ona zawiera</string>
|
||||
<string name="group_option_tips">Poniższe zmiany zostaną zastosowane dla wszystkich kanałów w tej grupie</string>
|
||||
<string name="today">Dziś</string>
|
||||
<string name="yesterday">Wczoraj</string>
|
||||
@ -92,7 +92,7 @@
|
||||
<string name="seven_days">7d</string>
|
||||
<string name="close">Zamknij</string>
|
||||
<string name="get_new_updates">Sprawdź aktualizacje</string>
|
||||
<string name="get_new_updates_desc">Wersja %1$s jest już dostępna</string>
|
||||
<string name="get_new_updates_desc">Nowa wersja %1$s jest dostępna</string>
|
||||
<string name="in_coding">W trakcie kodowania</string>
|
||||
<string name="coming_soon">Dostępne wkrótce</string>
|
||||
<string name="accounts">Konta</string>
|
||||
@ -107,117 +107,11 @@
|
||||
<string name="tips_and_support">Porady i wsparcie</string>
|
||||
<string name="tips_and_support_desc">O aplikacji i licencjach open source</string>
|
||||
<string name="welcome">Witamy</string>
|
||||
<string name="tos_tips">Korzystając z Read You akceptujesz Warunki Świadczenia Usług i Politykę Prywatności. Kliknij "Zgadzam się" aby kontynuować.</string>
|
||||
<string name="tos_tips">Aby kontynuować, przeczytaj i zaakceptuj Warunki korzystania z usługi i Politykę prywatności Read You.</string>
|
||||
<string name="browse_tos_tips">Zapoznaj się z <i><u>Warunkami Świadczenia Usługi oraz Polityką Prywatności</u></i>
|
||||
</string>
|
||||
<string name="terms_of_service">Warunki świadczenia usługi</string>
|
||||
<string name="tos_content">
|
||||
<h5>
|
||||
|
||||
Polityka prywatności
|
||||
|
||||
</h5>
|
||||
<br/>
|
||||
<p>
|
||||
|
||||
Traktujemy kwestię prywatności użytkownika z należytą powagą.
|
||||
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
<b>Read You</b>
|
||||
|
||||
nie gromadzi danych na temat użytkownika, a wszelkie poufne informacje (hasła i inne informacje dotyczące kont) są
|
||||
bezpiecznie przechowywane w lokalnej bazie danych na twoim urządzeniu.
|
||||
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
<b>Read You</b>
|
||||
|
||||
korzysta z poniższych uprawnień w celu świadczenia usług.
|
||||
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
|
||||
- Pełny dostęp do sieci (w celu uzyskania dostępu do treści wskazanych przez użytkownika)
|
||||
|
||||
</p>
|
||||
<p>
|
||||
|
||||
- Wyświetlanie połączeń sieciowych (w celu sprawdzenia czy urządzenie jest w stanie połączyć się z siecią)
|
||||
|
||||
</p>
|
||||
<p>
|
||||
|
||||
- Uruchom usługę na pierwszym planie (w celu regularnej automatycznej synchronizacji treści)
|
||||
|
||||
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<h5>
|
||||
|
||||
Usługi Stron Trzecich
|
||||
|
||||
</h5>
|
||||
<br/>
|
||||
<p>
|
||||
|
||||
Ta umowa nie tyczy się stron podmiotów trzecich z którymi użytkownik łączy się za pomocą <b>Read You</b>.
|
||||
Zasady ochrony prywatności tych serwisów można sprawdzić na ich poszczególnych stronach internetowych
|
||||
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<h5>
|
||||
|
||||
Oświadczenie
|
||||
|
||||
</h5>
|
||||
<br/>
|
||||
<p>
|
||||
<b>Read You</b>
|
||||
|
||||
jest jedynie agregatem treści. Sposób, w jaki użytkownik korzysta z <b>Read You</b> podlega lokalnym prawom i
|
||||
przepisom obowiązującym w miejscu zamieszkania. Odpowiedzialność i ewentualne konsekwencje wynikające z niewłaściwego korzystania z oprogramowania są ponoszone osobiście przez użytkownika.
|
||||
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<h5>
|
||||
|
||||
Licencja Open Source
|
||||
|
||||
</h5>
|
||||
<br/>
|
||||
<p>
|
||||
<b>Read You</b>
|
||||
|
||||
jest projektem otwarto-źródłowym na licencji GNU GPL 3.0 Open Source License[1], pozwalającej na darmowe używanie, analizowanie i modyfikowanie kodu źródłowego <b>Read You</b>. Zabroniona jest
|
||||
dystrybucja i sprzedaż zmodyfikowanego bądź pochodnego kodu w formie komercyjnego oprogramowania własnościowego. W celu uzyskania szczegółowych informacji, proszę odwiedzić stronę pełnej licencji GNU GPL 3.0 Open Source License[2].
|
||||
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<h5>
|
||||
|
||||
Załączniki
|
||||
|
||||
</h5>
|
||||
<br/>
|
||||
<p>
|
||||
|
||||
- [1] https://github.com/Ashinch/ReadYou
|
||||
|
||||
</p>
|
||||
<p>
|
||||
|
||||
- [2] https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
</p>
|
||||
</string>
|
||||
<string name="tos_content"><h5>Polityka prywatności</h5> <br/> <p>Twoją prywatność traktuję bardzo poważnie.</p> <br/> <p> <b>Read You</b> nie zbiera żadnych danych użytkownika, a wszystkie poufne informacje (hasła i inne informacje o koncie) są bezpiecznie przechowywane w lokalnej bazie danych aplikacji na Twoim urządzeniu.</p> <br/> <p> <b>Read You</b> będzie korzystać z następujących uprawnień, aby świadczyć Ci usługę.</p> <br/> <p> - Uprawnienie dostępu do sieci (aby uzyskać dostęp do treści online zgodnie z Twoimi ustawieniami)</p> <p> - Uprawnienie uzyskania stanu sieci (aby określić, czy urządzenie ma obecnie dostępne warunki sieciowe)</p> <p> - Uprawnienie usługi w tle (aby regularnie automatycznie synchronizować Twoje ulubione)</p> <br/> <br/> <h5>Usługi stron trzecich</h5> <br/> <p> Niniejsza polityka nie dotyczy usług stron trzecich, z których korzystasz w <b>Read You</b>. Możesz zapoznać się z polityką prywatności usług stron trzecich, z których korzystasz na ich stronach internetowych </p> <br/> <br/> <h5> Zastrzeżenia </h5> <br/> <p> <b>Read You</b> jest wyłącznie narzędziem do gromadzenia treści. Korzystanie z <b>Read You</b> podlega prawom i regulacjom obowiązującym w Twoim kraju i regionie, a wszelka odpowiedzialność wynikająca z Twoich działań będzie ponoszona przez Ciebie osobiście. </p> <br/> <br/> <h5> Licencja open source </h5> <br/> <p> <b>Read You</b> jest projektem typu open source na podstawie licencji GNU GPL 3.0[1], która pozwala na bezpłatne korzystanie, odwoływanie się do kodu źródłowego <b>Read You</b> i jego modyfikację, ale nie zezwala na dystrybucję i sprzedaż zmodyfikowanego i pochodnego kodu jako zamkniętego oprogramowania komercyjnego. Aby uzyskać szczegółowe informacje, zapoznaj się z pełną licencją GNU GPL 3.0[2]. </p> <br/> <br/> <h5> Załącznik </h5> <br/> <p> - [1] https://github.com/Ashinch/ReadYou </p> <p> - [2] https://www.gnu.org/licenses/gpl-3.0.html </p></string>
|
||||
<string name="agree">Zgadzam się</string>
|
||||
<string name="wallpaper_colors">Kolory z tapety</string>
|
||||
<string name="no_palettes">Brak palet</string>
|
||||
@ -232,7 +126,7 @@
|
||||
<string name="on">Włączono</string>
|
||||
<string name="off">Wyłączono</string>
|
||||
<string name="other">Inne</string>
|
||||
<string name="amoled_dark_theme">Czarny motyw AMOLED</string>
|
||||
<string name="amoled_dark_theme">Ciemny motyw AMOLED</string>
|
||||
<string name="tonal_elevation">Odcień</string>
|
||||
<string name="reading_fonts">Czcionka artykułu</string>
|
||||
<string name="basic_fonts">Podstawowe czcionki</string>
|
||||
@ -244,10 +138,10 @@
|
||||
<string name="update_link">https://api.github.com/repos/Ashinch/ReadYou/releases/latest</string>
|
||||
<string name="change_log">Lista zmian</string>
|
||||
<string name="update">Aktualizuj</string>
|
||||
<string name="skip_this_version">Pomiń tą wersję</string>
|
||||
<string name="skip_this_version">Pomiń tę wersję</string>
|
||||
<string name="checking_updates">Sprawdzam aktualizacje…</string>
|
||||
<string name="is_latest_version">Masz najnowszą wersję aplikacji</string>
|
||||
<string name="check_failure">Nie udało się sprawdzić aktualizacji</string>
|
||||
<string name="check_failure">Nie udało się sprawdzić dostępności aktualizacji</string>
|
||||
<string name="download_failure">Nie udało się pobrać aktualizacji</string>
|
||||
<string name="rate_limit">Limit częstotliwości żądań</string>
|
||||
<string name="help">Pomoc</string>
|
||||
@ -277,12 +171,12 @@
|
||||
<string name="filter_bar">Pasek filtrów</string>
|
||||
<string name="icons">Ikony</string>
|
||||
<string name="icons_and_labels">Ikony z tekstem</string>
|
||||
<string name="icons_and_label_only_selected">Ikony z tekstem (tylko zaznaczone)</string>
|
||||
<string name="tips_top_bar_tonal_elevation">Ten odcień jest dostępny tylko podczas przewijania.</string>
|
||||
<string name="tips_article_list_tonal_elevation">Ten odcień jest dostępny tylko w jasnym motywie.</string>
|
||||
<string name="tips_group_list_tonal_elevation">Ten odcień jest dostępny tylko w jasnym motywie.</string>
|
||||
<string name="icons_and_label_only_selected">Ikony z tekstem (tylko aktywne)</string>
|
||||
<string name="tips_top_bar_tonal_elevation">Odcień górnego paska jest dostępny tylko podczas przewijania</string>
|
||||
<string name="tips_article_list_tonal_elevation">Odcień listy artykułów jest dostępny tylko w jasnym motywie</string>
|
||||
<string name="tips_group_list_tonal_elevation">Odcień listy grup jest dostępny tylko w jasnym motywie</string>
|
||||
<string name="share">Udostępnij</string>
|
||||
<string name="touch_to_play_video">Dotknij aby odtworzyć film</string>
|
||||
<string name="touch_to_play_video">Stuknij, aby odtworzyć film</string>
|
||||
<string name="text">Tekst</string>
|
||||
<string name="font_size">Rozmiar czcionki</string>
|
||||
<string name="letter_spacing">Odstępy między znakami</string>
|
||||
@ -295,8 +189,8 @@
|
||||
<string name="images">Obrazy</string>
|
||||
<string name="rounded_corners">Zaokrąglone rogi</string>
|
||||
<string name="videos">Filmy</string>
|
||||
<string name="align_start">Do lewej</string>
|
||||
<string name="align_end">Do prawej</string>
|
||||
<string name="align_start">Wyrównaj początek</string>
|
||||
<string name="align_end">Wyrównaj koniec</string>
|
||||
<string name="center_text">Do środka</string>
|
||||
<string name="justify">Wyjustuj</string>
|
||||
<string name="external_fonts">Zewnętrzne czcionki</string>
|
||||
@ -330,7 +224,7 @@
|
||||
<string name="for_1_month">1 miesiąc</string>
|
||||
<string name="local">Konto lokalne</string>
|
||||
<string name="services">Usługi</string>
|
||||
<string name="fever_desc">Przestarzałe. Nie zalecane.</string>
|
||||
<string name="fever_desc">Przestarzałe. Niezalecane.</string>
|
||||
<string name="self_hosted">Własny serwer</string>
|
||||
<string name="more">Więcej</string>
|
||||
<string name="add_accounts">Dodaj konta</string>
|
||||
@ -350,13 +244,13 @@
|
||||
<string name="delete_account_toast">Konto zostało usunięte</string>
|
||||
<string name="clear_all_articles">Usuń wszystkie artykuły</string>
|
||||
<string name="block_list">Lista zablokowanych</string>
|
||||
<string name="clear_all_articles_tips">Czy na pewno chcesz usunąć wszystkie artykuły z tego konta\?</string>
|
||||
<string name="clear_all_articles_tips">Czy na pewno chcesz wyczyścić listę artykułów tego konta?</string>
|
||||
<string name="delete_account_tips">Czy na pewno chcesz usunąć to konto\?</string>
|
||||
<string name="synchronous_tips">Aby zmiany zaczęły obowiązywać, wymagane jest ponowne uruchomienie.</string>
|
||||
<string name="synchronous_tips">Aby zmiany zostały wprowadzone, wymagane jest ponowne uruchomienie</string>
|
||||
<string name="local_desc">Na tym urządzeniu</string>
|
||||
<string name="switch_account">Przełącz</string>
|
||||
<string name="add">Dodaj</string>
|
||||
<string name="accounts_tips">Kliknij nazwę konta na stronie kanału, aby je zmienić.</string>
|
||||
<string name="accounts_tips">Aby zmienić konto, stuknij jego nazwę na stronie głównej</string>
|
||||
<string name="empty">Pusty</string>
|
||||
<string name="username">Nazwa użytkownika</string>
|
||||
<string name="password">Hasło</string>
|
||||
@ -396,4 +290,36 @@
|
||||
<string name="unfold_more">Rozwiń wszystkie</string>
|
||||
<string name="unfold_less">Zwiń wszystkie</string>
|
||||
<string name="about">O aplikacji</string>
|
||||
<string name="import_from_json">Importuj z JSON</string>
|
||||
<string name="export_as_json">Eksportuj jako JSON</string>
|
||||
<string name="invalid_json_file_warning">Ten plik może nie być prawidłowym plikiem JSON. Jego importowanie może potencjalnie uszkodzić aplikację i spowodować utratę bieżących preferencji. Czy na pewno chcesz kontynuować?</string>
|
||||
<string name="line_height_multiple">Wielokrotność wysokości wiersza</string>
|
||||
<string name="invalid_protobuf_file_warning">Ten plik może nie być prawidłowym plikiem protobuf. Jego importowanie może potencjalnie uszkodzić aplikację i spowodować utratę bieżących preferencji. Czy na pewno chcesz kontynuować?</string>
|
||||
<string name="import_from_protobuf_file">Importuj z pliku protobuf</string>
|
||||
<string name="toggle_read">Przełącz stan przeczytania</string>
|
||||
<string name="grey_out_articles">Wyszarzaj artykuły</string>
|
||||
<string name="all_read">Wszystko przeczytane</string>
|
||||
<string name="export_as_protobuf_file">Eksportuj jako plik protobuf</string>
|
||||
<string name="keep_archived_tips">Zarchiwizowane elementy to artykuły oznaczone jako przeczytane, niewyróżnione i nieotagowane. Ustawienie „Zachowaj zarchiwizowane artykuły” jest skuteczne tylko na tym urządzeniu. Starsze zarchiwizowane artykuły zostaną usunięte z tego urządzenia. Usunięcia artykułów nie można cofnąć.</string>
|
||||
<string name="initial_open_app">Aplikacja po kliknięciu linku</string>
|
||||
<string name="additional_info_desc">Dodatkowe informacje obejmują opcje konfiguracji dla każdego kanału, takie jak zezwolenie na powiadomienia, analiza pełnej zawartości itp. Jeśli zamierzasz używać wyeksportowanego pliku OPML z innymi czytnikami, wybierz opcję „Wyklucz”.</string>
|
||||
<string name="submit_bug_report">zgłoś błąd na GitHub</string>
|
||||
<string name="unexpected_error_msg">Aplikacja napotkała nieoczekiwany błąd i musiała zostać zamknięta.\n\nAby szybko zidentyfikować i rozwiązać problem, zapoznaj się z poniższym śladem stosu błędów %1$s.</string>
|
||||
<string name="pull_to_switch_article">Przeciągnij, aby zmienić artykuł</string>
|
||||
<string name="mark_above_as_read">Oznacz powyższe jako przeczytane</string>
|
||||
<string name="mark_below_as_read">Oznacz poniższe jako przeczytane</string>
|
||||
<string name="toolbars">Paski narzędzi</string>
|
||||
<string name="open_link_something_wrong">Ustawienie „Otwórz link” zostało zignorowane, ponieważ coś poszło nie tak.</string>
|
||||
<string name="native_component">Komponent natywny</string>
|
||||
<string name="content_renderer">Renderer treści</string>
|
||||
<string name="read_aloud">Czytać na głos</string>
|
||||
<string name="only_available_on_webview">Dostępne tylko w WebView</string>
|
||||
<string name="use_bionic_reading">Użyj czytania bionicznego</string>
|
||||
<string name="bionic_reading_tips">Czym jest czytanie bioniczne?</string>
|
||||
<string name="browse_bionic_reading_tips">Dowiedz się więcej na <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="mark_as_read_on_scroll">Oznacz jako przeczytane podczas przewijania</string>
|
||||
<string name="become_a_sponsor">Zostań sponsorem</string>
|
||||
<string name="sponsor_desc">Tworzymy i utrzymujemy tę darmową aplikację typu open source poza godzinami pracy. Jeśli Ci się podoba, rozważ wsparcie nas małą darowizną! ☕️</string>
|
||||
<string name="troubleshooting_desc">Raport o błędach, narzędzia debugowania</string>
|
||||
<string name="toggle_starred">Przełącz wyróżnione</string>
|
||||
</resources>
|
@ -317,4 +317,8 @@
|
||||
<string name="about">Sobre</string>
|
||||
<string name="bionic_reading_tips">O que é Bionic Reading?</string>
|
||||
<string name="browse_bionic_reading_tips">Saiba mais em <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="toolbars">Barras de Ferramentas</string>
|
||||
<string name="mark_as_read_on_scroll">Marcar como lido ao rolar a página</string>
|
||||
<string name="become_a_sponsor">Torne-se um patrocinador</string>
|
||||
<string name="sponsor_desc">Nós contruímos e mantemos este aplicativo gratuito e de código aberto em nossas horas vagas. Se você gosta, considere nos apoiar com uma pequena doação! ☕️</string>
|
||||
</resources>
|
@ -317,4 +317,8 @@
|
||||
<string name="about">Sobre</string>
|
||||
<string name="bionic_reading_tips">O que é Bionic Reading?</string>
|
||||
<string name="browse_bionic_reading_tips">Saiba mais em <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="mark_as_read_on_scroll">Marcar como lido na rolagem</string>
|
||||
<string name="toolbars">Barras de ferramentas</string>
|
||||
<string name="become_a_sponsor">Torne-se um patrocinador</string>
|
||||
<string name="sponsor_desc">Construímos e mantemos este aplicativo gratuito e de código aberto fora do horário comercial. Se você gostou, considere nos apoiar com uma pequena doação! ☕️</string>
|
||||
</resources>
|
@ -320,4 +320,8 @@
|
||||
<string name="read_aloud">Читать вслух</string>
|
||||
<string name="native_component">Нативный компонент</string>
|
||||
<string name="browse_bionic_reading_tips">Узнайте больше на <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="toolbars">Панели инструментов</string>
|
||||
<string name="mark_as_read_on_scroll">Отметить как прочитанное при прокрутке</string>
|
||||
<string name="become_a_sponsor">Стать спонсором</string>
|
||||
<string name="sponsor_desc">Мы создаем и поддерживаем это бесплатное приложение с открытым исходным кодом в свободное время. Если оно вам нравится, пожалуйста, поддержите нас небольшим пожертвованием! ☕️</string>
|
||||
</resources>
|
@ -317,4 +317,8 @@
|
||||
<string name="about">Informácie</string>
|
||||
<string name="content_renderer">Zobrazovač obsahu</string>
|
||||
<string name="only_available_on_webview">Dostupné len vo WebView</string>
|
||||
<string name="mark_as_read_on_scroll">Označiť ako prečítané pri posúvaní</string>
|
||||
<string name="toolbars">Panely nástrojov</string>
|
||||
<string name="sponsor_desc">Túto bezplatnú aplikáciu s otvoreným zdrojovým kódom vytvárame a udržiavame v mimopracovnom čase. Ak sa vám páči, zvážte, či nás podporíte malým darom! ☕️</string>
|
||||
<string name="become_a_sponsor">Staňte sa sponzorom</string>
|
||||
</resources>
|
@ -111,7 +111,7 @@
|
||||
<string name="every_15_minutes">Сваких 15 минута</string>
|
||||
<string name="interaction">Интеракција</string>
|
||||
<string name="check_failure">Провера ажурирања није успела</string>
|
||||
<string name="align_start">Поравнајте почетак</string>
|
||||
<string name="align_start">Поравнај почетак</string>
|
||||
<string name="keep_archived_articles">Чување архивираних чланака</string>
|
||||
<string name="selected">Изабрано</string>
|
||||
<string name="icons">Иконице</string>
|
||||
@ -160,7 +160,7 @@
|
||||
<string name="sponsor">Спонзор</string>
|
||||
<string name="for_3_days">3 дана</string>
|
||||
<string name="deny">Одбиј</string>
|
||||
<string name="align_end">Поравнајте крај</string>
|
||||
<string name="align_end">Поравнај крај</string>
|
||||
<string name="block_list">Блок листа</string>
|
||||
<string name="specific_browser_name">Претраживач: %1$s</string>
|
||||
<string name="specific_browser">Присили одређени претраживач</string>
|
||||
@ -317,4 +317,8 @@
|
||||
<string name="use_bionic_reading">Користите бионичко читање</string>
|
||||
<string name="about">О нама</string>
|
||||
<string name="browse_bionic_reading_tips">Сазнајте више на <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="mark_as_read_on_scroll">Означи као прочитано при превлачењу</string>
|
||||
<string name="become_a_sponsor">Постани спонзор</string>
|
||||
<string name="sponsor_desc">Ми правимо и одржавамо ову бесплатну апликацију отвореног кода ван радног времена. Ако уживате, размислите о томе да нас подржите малом донацијом! ☕</string>
|
||||
<string name="toolbars">Траке са алаткама</string>
|
||||
</resources>
|
2
app/src/main/res/values-ta/strings.xml
Normal file
2
app/src/main/res/values-ta/strings.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
@ -320,4 +320,8 @@
|
||||
<string name="only_available_on_webview">Доступно тільки у WebView</string>
|
||||
<string name="about">Про застосунок</string>
|
||||
<string name="bionic_reading_tips">Що таке біонічне зчитування?</string>
|
||||
<string name="mark_as_read_on_scroll">Позначити як прочитане при прокручуванні</string>
|
||||
<string name="become_a_sponsor">Стати спонсором</string>
|
||||
<string name="sponsor_desc">Ми створюємо і підтримуємо цей безплатний застосунок з відкритим вихідним кодом у вільний від роботи час. Якщо він вам подобається, будь ласка, підтримайте нас невеликою пожертвою! ☕️</string>
|
||||
<string name="toolbars">Панелі інструментів</string>
|
||||
</resources>
|
@ -312,4 +312,7 @@
|
||||
<string name="bionic_reading_tips">什么是 Bionic Reading?</string>
|
||||
<string name="browse_bionic_reading_tips">在 <i><u>bionic-reading.com</u></i>.了解更多信息。</string>
|
||||
<string name="mark_as_read_on_scroll">滚动时标记为已读</string>
|
||||
<string name="become_a_sponsor">成为赞助者</string>
|
||||
<string name="sponsor_desc">我们在休息时间构建并维护这个自由开源应用。如果你觉得应用还不错,请考虑用小额捐赠支持哦我们!☕️</string>
|
||||
<string name="toolbars">工具栏</string>
|
||||
</resources>
|
@ -311,4 +311,8 @@
|
||||
<string name="about">關於</string>
|
||||
<string name="bionic_reading_tips">什麼是仿生閱讀?</string>
|
||||
<string name="browse_bionic_reading_tips">了解更多資訊,請造訪 <i><u>bionic-reading.com</u></i>。</string>
|
||||
<string name="toolbars">工具列</string>
|
||||
<string name="mark_as_read_on_scroll">捲動時標記為已讀</string>
|
||||
<string name="become_a_sponsor">成為贊助者</string>
|
||||
<string name="sponsor_desc">我們在空閒時間建立並維護這個免費的開源應用程式。如果您喜歡它,請考慮給予我們一點小額捐款支持!☕️</string>
|
||||
</resources>
|
@ -392,6 +392,7 @@
|
||||
<string name="server_url">Server URL</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="client_certificate">Client certificate (optional)</string>
|
||||
<string name="connection">Connection</string>
|
||||
<string name="system_default">System</string>
|
||||
<string name="initial_open_app">App when link is clicked</string>
|
||||
@ -458,4 +459,8 @@
|
||||
<string name="bionic_reading_domain" translatable="false">bionic-reading.com</string>
|
||||
<string name="bionic_reading_link" translatable="false">https://bionic-reading.com</string>
|
||||
<string name="mark_as_read_on_scroll">Mark as read on scroll</string>
|
||||
<string name="hide_empty_groups">Hide empty groups</string>
|
||||
<string name="become_a_sponsor">Become a sponsor</string>
|
||||
<string name="sponsor_desc">We build and upkeep this free, open-source app in our off-hours. If you enjoy it, please consider supporting us with a small donation! ☕️</string>
|
||||
<string name="toolbars">Toolbars</string>
|
||||
</resources>
|
||||
|
@ -10,6 +10,7 @@ import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.junit.MockitoJUnitRunner
|
||||
import org.mockito.kotlin.mock
|
||||
import rust.nostr.sdk.Client
|
||||
|
||||
internal const val enclosureUrlString1: String = "https://example.com/enclosure.jpg"
|
||||
internal const val enclosureUrlString2: String = "https://github.blog/wp-content/uploads/2024/03/github_copilot_header.png"
|
||||
@ -50,6 +51,9 @@ class RssHelperTest {
|
||||
@Mock
|
||||
private lateinit var mockOkHttpClient: OkHttpClient
|
||||
|
||||
@Mock
|
||||
private lateinit var mockNostrClient: Client
|
||||
|
||||
private lateinit var rssHelper: RssHelper
|
||||
|
||||
@Before
|
||||
@ -57,7 +61,8 @@ class RssHelperTest {
|
||||
mockContext = mock<Context> { }
|
||||
mockIODispatcher = mock<CoroutineDispatcher> {}
|
||||
mockOkHttpClient = mock<OkHttpClient> {}
|
||||
rssHelper = RssHelper(mockContext, mockIODispatcher, mockOkHttpClient)
|
||||
mockNostrClient = mock<Client> { }
|
||||
rssHelper = RssHelper(mockContext, mockIODispatcher, mockOkHttpClient, mockNostrClient)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
8
fastlane/metadata/android/ar/changelogs/26.txt
Normal file
8
fastlane/metadata/android/ar/changelogs/26.txt
Normal file
@ -0,0 +1,8 @@
|
||||
## 0.11.0
|
||||
|
||||
تغييرات ملحوظة:
|
||||
1. تقديم قارئ مقالة جديد يعتمد على WebView، مع دعم Bionic Reading
|
||||
2. وضع علامة تلقائية على العناصر على أنها مقروءة في التمرير
|
||||
3. انقر فوق شريط التطبيق العلوي للعودة إلى أعلى الصفحة
|
||||
4. إضافة فواصل إلى أشرطة الأدوات في صفحة القراءة
|
||||
5. إضافة زر لطي/توسيع مجموعات التغذية
|
6
fastlane/metadata/android/ar/changelogs/27.txt
Normal file
6
fastlane/metadata/android/ar/changelogs/27.txt
Normal file
@ -0,0 +1,6 @@
|
||||
## 0.11.1
|
||||
|
||||
تغييرات ملحوظة:
|
||||
1. دعم الخط المخصص لقارئ WebView
|
||||
2. إضافة أنماط مرئية جديدة إلى أشرطة الأدوات في صفحة التدفق وصفحة القراءة
|
||||
3. تحسينات وإصلاحات واجهة المستخدم
|
@ -7,8 +7,8 @@
|
||||
5. Подобряване на плъзгане за звезда/премахване на звезда, плъзгане до непрочетено и добавяне на конфигурация (#594, @JunkFood02)
|
||||
6. Използвайте системния локал, за да форматирате дисплея на часа по подразбиране. (#617, @JunkFood02)
|
||||
7. Преминете към внедряване на androidx edge to edge (#690, @Moderpach)
|
||||
8. Добавете програма за преглед на изображения към страницата за четене (#578, #545, @JunkFood02, @nvllz)
|
||||
9. Добавете жестове за плъзгане нагоре и надолу по страницата за четене, за да превключвате статии (#589, @JunkFood02)
|
||||
8. Добавяне на програма за преглед на изображения към страницата за четене (#578, #545, @JunkFood02, @nvllz)
|
||||
9. Добавяне на жестове за плъзгане нагоре и надолу по страницата за четене, за да превключвате статии (#589, @JunkFood02)
|
||||
10. Добавяне на дейност за докладване на срив, за да се справят с неуловени изключения (#576, @JunkFood02)
|
||||
11. Добавете контекстно меню при продължително натискане за елементи в страницата на потока (#613, @JunkFood02)
|
||||
12. Добавяне на многократно предпочитание за височина на реда за страница за четене (#620, @JunkFood02)
|
||||
|
@ -1,50 +0,0 @@
|
||||
## 0.10.1
|
||||
|
||||
1. Коригирайте стила на текстовата икона за емисия (#726, @Ashinch)
|
||||
2. Коригиране на проблем, при който ранните потребители не могат да стартират приложението след импортиране на предпочитания за приложението (#718, @Ashinch)
|
||||
3. Коригирайте някои случаи, причиняващи грешки при импортиране на OPML (#735, @Ashinch)
|
||||
4. Коригирайте някои анимации за навигация на прозорци (#717, @JunkFood02)
|
||||
5. Коригирайте някои проблеми със стила за RTL езици (#732, #733, @JunkFood02)
|
||||
6. Актуализации на превода (Благодаря на всички сътрудници)
|
||||
|
||||
### Бележки
|
||||
|
||||
**Пълен регистър на промените:** [0.10.0...0.10.1](https://github.com/Ashinch/ReadYou/compare/0.10.0...0.10.1)
|
||||
|
||||
Това е корекция на грешки. В случай, че сте го пропуснали, тук е регистърът на промените за версия 0.10.0:
|
||||
|
||||
## 0.10.0
|
||||
|
||||
1. Увеличете зависимостите на Material Design 3, компилирайте sdk и версията на gradle (#502, @JunkFood02)
|
||||
2. Поддържайте инструмента за избор на език в приложението за Android 13 (#541, #571, @JunkFood02)
|
||||
3. Поддръжка за добавяне на емисии чрез системен лист за споделяне (#618, @JunkFood02)
|
||||
4. Поддръжка за определяне на състава на споделеното съдържание (#660, @Ashinch)
|
||||
5. Подобряване на плъзгане за звезда/премахване на звезда, плъзгане до непрочетено и добавяне на конфигурация (#594, @JunkFood02)
|
||||
6. Използвайте системния локал, за да форматирате дисплея на часа по подразбиране. (#617, @JunkFood02)
|
||||
7. Преминете към внедряване на androidx edge to edge (#690, @Moderpach)
|
||||
8. Добавете програма за преглед на изображения към страницата за четене (#578, #545, @JunkFood02, @nvllz)
|
||||
9. Добавете жестове за плъзгане нагоре и надолу по страницата за четене, за да превключвате статии (#589, @JunkFood02)
|
||||
10. Добавяне на дейност за докладване на срив, за да се справят с неуловени изключения (#576, @JunkFood02)
|
||||
11. Добавете контекстно меню при продължително натискане за елементи в страницата на потока (#613, @JunkFood02)
|
||||
12. Добавяне на многократно предпочитание за височина на реда за страница за четене (#620, @JunkFood02)
|
||||
13. Добавете страница със списък с лицензи и подобрете страницата относно (#664, @Ashinch)
|
||||
14. Добавете страница за отстраняване на неизправности и инструменти за импортиране/експортиране на предпочитания за приложения (#672, #710, @Ashinch)
|
||||
15. Коригирайте искането за разрешение за известяване при стартиране на Android 13 (@JunkFood02)
|
||||
16. Коригиране на сблъсък, когато датата на публикуване и актуализираната дата са празни (@JunkFood02)
|
||||
17. Сега заменете датата на публикуване на статия с текущия час, ако е бъдеща дата (#638, @Ashinch)
|
||||
18. Сега, когато почиствате канал или група, статиите със звезда ще бъдат игнорирани (#652, @Ashinch)
|
||||
19. Сега автоматично рестартирайте приложението след зареждане на външни шрифтове (#667, @Ashinch)
|
||||
20. Сега първо намерете тага `<enclosure>` като миниатюра на статията (#681, @Ashinch)
|
||||
21. Вече по подразбиране сортирайте категориите по азбучен ред по време на синхронизиране в Google Reader (#700, @mbestavros)
|
||||
22. Нов контейнер за изображение (#712, @JunkFood02)
|
||||
23. Още подобрения на потребителския интерфейс и корекции на грешки (@Ashinch, @JunkFood02)
|
||||
24. Актуализации на преводите (благодарим на всички, които помогнаха)
|
||||
|
||||
### Бележки
|
||||
|
||||
1. За да се поддържа експортирането на елементи за настройка, някои от елементите за настройка са били нулирани по подразбиране и може да се наложи да ги нулирате.
|
||||
2. Спонсорският канал buymeacoffee.com в момента не е достъпен и дарените преди това пари са възстановени по сметките на дарителите.
|
||||
3. Търсим други канали за спонсорство, в момента наличните са AFDIAN. Ако искате да подкрепите развитието на ReadYou, можете да го спонсорирате чрез [AFDIAN](https://afdian.net/a/ashinch).
|
||||
4. Благодарим на нашите спонсори: @User_3072e, @User_223be, @User_3c5c7(Simon), @User_97ebe, @Lowae, @User_28b9f, @User_yuHC, @nullqwertyuiop, @openisgood, @User_vWca, @qgmzmy, @User_97ee1
|
||||
|
||||
**Пълен регистър на промените:** [0.9.12...0.10.0](https://github.com/Ashinch/ReadYou/compare/0.9.12...0.10.0)
|
8
fastlane/metadata/android/bg/changelogs/26.txt
Normal file
8
fastlane/metadata/android/bg/changelogs/26.txt
Normal file
@ -0,0 +1,8 @@
|
||||
## 0.11.0
|
||||
|
||||
Забележителни промени:
|
||||
1. Въвеждане на нов четец на статии, базиран на WebView, с поддръжка на Bionic Reading
|
||||
2. Автоматично маркиране на елементи като прочетени при превъртане
|
||||
3. Щракнете върху горната лента на приложението, за да се върнете в началото на страницата
|
||||
4. Добавяне на разделители към лентите с инструменти в страницата за четене
|
||||
5. Добавяне на бутон за сгъване/разгъване на групите от емисии
|
@ -3,14 +3,14 @@
|
||||
<b>Характеристики:</b>
|
||||
|
||||
* Абонирайте се за връзки към канали
|
||||
* Импортиране от OPML
|
||||
* Внасяне от OPML
|
||||
* Синхронизиране на статии
|
||||
* Известие за актуализиране на статия
|
||||
* Анализирайте пълното съдържание
|
||||
* Анализ на пълно съдържание
|
||||
* Филтриране на непрочетени и означени със звезда
|
||||
* Групиране на емисии
|
||||
* Локализация
|
||||
* Експортиране като OPML
|
||||
* Изнасяне като OPML
|
||||
* Търсене на статии
|
||||
|
||||
Има още…
|
||||
|
8
fastlane/metadata/android/cs-CZ/changelogs/26.txt
Normal file
8
fastlane/metadata/android/cs-CZ/changelogs/26.txt
Normal file
@ -0,0 +1,8 @@
|
||||
## 0.11.0
|
||||
|
||||
Významné změny:
|
||||
1. Uvedení nové čtečky článků založené na WebView s podporou Bionic Reading.
|
||||
2. Automatické označování položek jako přečtených při posouvání
|
||||
3. Klepnutím na horní lištu aplikace se vrátíte na začátek stránky
|
||||
4. Přidané rozdělovače na panely nástrojů na stránce pro čtení
|
||||
5. Přidané tlačítko pro sbalení/rozbalení skupin kanálů
|
6
fastlane/metadata/android/cs-CZ/changelogs/27.txt
Normal file
6
fastlane/metadata/android/cs-CZ/changelogs/27.txt
Normal file
@ -0,0 +1,6 @@
|
||||
## 0.11.1
|
||||
|
||||
Významné změny:
|
||||
1. Podpora vlastního písma pro čtečku WebView
|
||||
2. Přidání nových vizuálních stylů na lišty nástrojů na stránce toku a stránce čtení
|
||||
3. Vylepšení a opravy uživatelského rozhraní
|
6
fastlane/metadata/android/en-US/changelogs/27.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/27.txt
Normal file
@ -0,0 +1,6 @@
|
||||
## 0.11.1
|
||||
|
||||
Notable changes:
|
||||
1. Custom font support for WebView reader
|
||||
2. Add new visual styles to toolbars in flow page and reading page
|
||||
3. UI improvements & fixes
|
8
fastlane/metadata/android/es/changelogs/26.txt
Normal file
8
fastlane/metadata/android/es/changelogs/26.txt
Normal file
@ -0,0 +1,8 @@
|
||||
## 0.11.0
|
||||
|
||||
Cambios notables:
|
||||
1. Introducir un nuevo lector de artículos basado en WebView, con soporte para Bionic Reading.
|
||||
2. Marcación automática de artículos como leídos al desplazarse
|
||||
3. Haga clic en la barra superior de la aplicación para volver a la parte superior de la página
|
||||
4. Añadir separadores a las barras de herramientas en la página de lectura
|
||||
5. Añadir un botón para contraer/expandir los grupos de noticias
|
6
fastlane/metadata/android/es/changelogs/27.txt
Normal file
6
fastlane/metadata/android/es/changelogs/27.txt
Normal file
@ -0,0 +1,6 @@
|
||||
## 0.11.1
|
||||
|
||||
Cambios notables:
|
||||
1. Compatibilidad con fuentes personalizadas para el lector WebView
|
||||
2. Agregue nuevos estilos visuales a las barras de herramientas en la página de flujo y la página de lectura
|
||||
3. Mejoras y correcciones de la interfaz de usuario
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user