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 |
@ -31,11 +31,11 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "me.ash.reader"
|
applicationId = "me.ash.reader"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 33
|
||||||
versionCode = 26
|
versionCode = 27
|
||||||
versionName = "0.11.0"
|
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"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@ -176,6 +176,12 @@ dependencies {
|
|||||||
implementation(libs.activity.compose)
|
implementation(libs.activity.compose)
|
||||||
implementation(libs.appcompat)
|
implementation(libs.appcompat)
|
||||||
|
|
||||||
|
// Markdown
|
||||||
|
implementation(libs.jetbrains.markdown)
|
||||||
|
|
||||||
|
// Nostr
|
||||||
|
implementation(libs.rust.nostr)
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.junit.ext)
|
androidTestImplementation(libs.junit.ext)
|
||||||
|
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
@ -37,6 +37,14 @@
|
|||||||
# Provider API
|
# Provider API
|
||||||
-keep class me.ash.reader.** { *; }
|
-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
|
# https://github.com/flutter/flutter/issues/127388
|
||||||
-dontwarn org.kxml2.io.KXml**
|
-dontwarn org.kxml2.io.KXml**
|
||||||
|
|
||||||
|
@ -4,10 +4,8 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="28" />
|
android:maxSdkVersion="28" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@ -21,7 +19,6 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".infrastructure.android.AndroidApp"
|
android:name=".infrastructure.android.AndroidApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/read_you"
|
android:label="@string/read_you"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
@ -5,11 +5,13 @@ class FeverSecurityKey private constructor() : SecurityKey() {
|
|||||||
var serverUrl: String? = null
|
var serverUrl: String? = null
|
||||||
var username: String? = null
|
var username: String? = null
|
||||||
var password: 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.serverUrl = serverUrl
|
||||||
this.username = username
|
this.username = username
|
||||||
this.password = password
|
this.password = password
|
||||||
|
this.clientCertificateAlias = clientCertificateAlias
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(value: String? = DESUtils.empty) : this() {
|
constructor(value: String? = DESUtils.empty) : this() {
|
||||||
@ -17,6 +19,7 @@ class FeverSecurityKey private constructor() : SecurityKey() {
|
|||||||
serverUrl = it.serverUrl
|
serverUrl = it.serverUrl
|
||||||
username = it.username
|
username = it.username
|
||||||
password = it.password
|
password = it.password
|
||||||
|
clientCertificateAlias = it.clientCertificateAlias
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,13 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
|
|||||||
var serverUrl: String? = null
|
var serverUrl: String? = null
|
||||||
var username: String? = null
|
var username: String? = null
|
||||||
var password: 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.serverUrl = serverUrl
|
||||||
this.username = username
|
this.username = username
|
||||||
this.password = password
|
this.password = password
|
||||||
|
this.clientCertificateAlias = clientCertificateAlias
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(value: String? = DESUtils.empty) : this() {
|
constructor(value: String? = DESUtils.empty) : this() {
|
||||||
@ -17,6 +19,7 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
|
|||||||
serverUrl = it.serverUrl
|
serverUrl = it.serverUrl
|
||||||
username = it.username
|
username = it.username
|
||||||
password = it.password
|
password = it.password
|
||||||
|
clientCertificateAlias = it.clientCertificateAlias
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,13 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
|
|||||||
var serverUrl: String? = null
|
var serverUrl: String? = null
|
||||||
var username: String? = null
|
var username: String? = null
|
||||||
var password: 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.serverUrl = serverUrl
|
||||||
this.username = username
|
this.username = username
|
||||||
this.password = password
|
this.password = password
|
||||||
|
this.clientCertificateAlias = clientCertificateAlias
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(value: String? = DESUtils.empty) : this() {
|
constructor(value: String? = DESUtils.empty) : this() {
|
||||||
@ -17,6 +19,7 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
|
|||||||
serverUrl = it.serverUrl
|
serverUrl = it.serverUrl
|
||||||
username = it.username
|
username = it.username
|
||||||
password = it.password
|
password = it.password
|
||||||
|
clientCertificateAlias = it.clientCertificateAlias
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ import androidx.paging.PagingSource
|
|||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.rometools.rome.feed.synd.SyndFeed
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.supervisorScope
|
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.account.Account
|
||||||
import me.ash.reader.domain.model.article.ArticleWithFeed
|
import me.ash.reader.domain.model.article.ArticleWithFeed
|
||||||
import me.ash.reader.domain.model.feed.Feed
|
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.android.NotificationHelper
|
||||||
import me.ash.reader.infrastructure.preference.KeepArchivedPreference
|
import me.ash.reader.infrastructure.preference.KeepArchivedPreference
|
||||||
import me.ash.reader.infrastructure.preference.SyncIntervalPreference
|
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.RssHelper
|
||||||
|
import me.ash.reader.infrastructure.rss.SyndFeedDelegate
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
import me.ash.reader.ui.ext.decodeHTML
|
import me.ash.reader.ui.ext.decodeHTML
|
||||||
|
import me.ash.reader.ui.ext.isNostrUri
|
||||||
import me.ash.reader.ui.ext.spacerDollar
|
import me.ash.reader.ui.ext.spacerDollar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@ -59,19 +65,26 @@ abstract class AbstractRssRepository(
|
|||||||
open suspend fun clearAuthorization() {}
|
open suspend fun clearAuthorization() {}
|
||||||
|
|
||||||
open suspend fun subscribe(
|
open suspend fun subscribe(
|
||||||
feedLink: String, searchedFeed: SyndFeed, groupId: String,
|
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
|
||||||
isNotification: Boolean, isFullContent: Boolean
|
isNotification: Boolean, isFullContent: Boolean
|
||||||
) {
|
) {
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
val feed = Feed(
|
val feed = Feed(
|
||||||
id = accountId.spacerDollar(UUID.randomUUID().toString()),
|
id = accountId.spacerDollar(UUID.randomUUID().toString()),
|
||||||
name = searchedFeed.title.decodeHTML()!!,
|
name = with(searchedFeed.title){ if (this.isNostrUri()) this else this.decodeHTML()!!},
|
||||||
url = feedLink,
|
url = feedLink,
|
||||||
groupId = groupId,
|
groupId = groupId,
|
||||||
accountId = accountId,
|
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)
|
feedDao.insert(feed)
|
||||||
articleDao.insertList(articles.map {
|
articleDao.insertList(articles.map {
|
||||||
it.copy(feedId = feed.id)
|
it.copy(feedId = feed.id)
|
||||||
@ -101,21 +114,18 @@ abstract class AbstractRssRepository(
|
|||||||
val preTime = System.currentTimeMillis()
|
val preTime = System.currentTimeMillis()
|
||||||
val preDate = Date(preTime)
|
val preDate = Date(preTime)
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
feedDao.queryAll(accountId)
|
val semaphore = Semaphore(16)
|
||||||
.chunked(16)
|
feedDao.queryAll(accountId).mapIndexed { _, feed ->
|
||||||
.forEach {
|
async(Dispatchers.IO) {
|
||||||
it.map { feed -> async { syncFeed(feed, preDate) } }
|
semaphore.withPermit {
|
||||||
.awaitAll()
|
val feedWithArticle = syncFeed(feed, preDate)
|
||||||
.forEach {
|
val newArticles = articleDao.insertListIfNotExist(feedWithArticle.articles)
|
||||||
if (it.feed.isNotification) {
|
if (feedWithArticle.feed.isNotification) {
|
||||||
notificationHelper.notify(it.apply {
|
notificationHelper.notify(feedWithArticle.copy(articles = newArticles))
|
||||||
articles = articleDao.insertListIfNotExist(it.articles)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
articleDao.insertListIfNotExist(it.articles)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
|
||||||
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
||||||
accountDao.queryById(accountId)?.let { account ->
|
accountDao.queryById(accountId)?.let { account ->
|
||||||
@ -177,17 +187,29 @@ abstract class AbstractRssRepository(
|
|||||||
|
|
||||||
private suspend fun syncFeed(feed: Feed, preDate: Date = Date()): FeedWithArticle {
|
private suspend fun syncFeed(feed: Feed, preDate: Date = Date()): FeedWithArticle {
|
||||||
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
||||||
val articles = rssHelper.queryRssXml(feed, "", preDate)
|
if (feed.url.isNostrUri()) {
|
||||||
if (feed.icon == null) {
|
val syncedFeed = rssHelper.syncNostrFeed(feed, "", preDate)
|
||||||
val iconLink = rssHelper.queryRssIconLink(feed.url)
|
return FeedWithArticle(
|
||||||
if (iconLink != null) {
|
feed = syncedFeed.feed
|
||||||
rssHelper.saveRssIcon(feedDao, feed, iconLink)
|
.apply { isNotification = feed.isNotification && syncedFeed.articles.isNotEmpty() },
|
||||||
}
|
articles = syncedFeed.articles
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return FeedWithArticle(
|
else {
|
||||||
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
|
val articles = rssHelper.queryRssXml(feed, "", preDate)
|
||||||
articles = articles
|
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() {
|
suspend fun clearKeepArchivedArticles() {
|
||||||
@ -200,39 +222,29 @@ abstract class AbstractRssRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cancelSync() {
|
fun cancelSync() {
|
||||||
workManager.cancelAllWork()
|
SyncWorker.cancelPeriodicWork(workManager)
|
||||||
|
SyncWorker.cancelOneTimeWork(workManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doSyncOneTime() {
|
fun doSyncOneTime() {
|
||||||
workManager.cancelAllWork()
|
|
||||||
SyncWorker.enqueueOneTimeWork(workManager)
|
SyncWorker.enqueueOneTimeWork(workManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun doSync(isOnStart: Boolean = false) {
|
suspend fun initSync() {
|
||||||
workManager.cancelAllWork()
|
|
||||||
accountDao.queryById(context.currentAccountId)?.let {
|
accountDao.queryById(context.currentAccountId)?.let {
|
||||||
if (isOnStart) {
|
val syncOnStart = it.syncOnStart.value
|
||||||
if (it.syncOnStart.value) {
|
if (syncOnStart) {
|
||||||
SyncWorker.enqueueOneTimeWork(workManager)
|
doSyncOneTime()
|
||||||
}
|
}
|
||||||
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
|
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
|
||||||
SyncWorker.enqueuePeriodicWork(
|
SyncWorker.enqueuePeriodicWork(
|
||||||
workManager = workManager,
|
workManager = workManager,
|
||||||
syncInterval = it.syncInterval,
|
syncInterval = it.syncInterval,
|
||||||
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
|
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
|
||||||
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
|
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
SyncWorker.enqueueOneTimeWork(workManager)
|
SyncWorker.cancelPeriodicWork(workManager)
|
||||||
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
|
|
||||||
SyncWorker.enqueuePeriodicWork(
|
|
||||||
workManager = workManager,
|
|
||||||
syncInterval = it.syncInterval,
|
|
||||||
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
|
|
||||||
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import android.util.Log
|
|||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.rometools.rome.feed.synd.SyndFeed
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.supervisorScope
|
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.di.MainDispatcher
|
||||||
import me.ash.reader.infrastructure.exception.FeverAPIException
|
import me.ash.reader.infrastructure.exception.FeverAPIException
|
||||||
import me.ash.reader.infrastructure.html.Readability
|
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.RssHelper
|
||||||
import me.ash.reader.infrastructure.rss.provider.fever.FeverAPI
|
import me.ash.reader.infrastructure.rss.provider.fever.FeverAPI
|
||||||
import me.ash.reader.infrastructure.rss.provider.fever.FeverDTO
|
import me.ash.reader.infrastructure.rss.provider.fever.FeverDTO
|
||||||
@ -70,11 +70,13 @@ class FeverRssService @Inject constructor(
|
|||||||
private suspend fun getFeverAPI() =
|
private suspend fun getFeverAPI() =
|
||||||
FeverSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
FeverSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
||||||
FeverAPI.getInstance(
|
FeverAPI.getInstance(
|
||||||
|
context = context,
|
||||||
serverUrl = serverUrl!!,
|
serverUrl = serverUrl!!,
|
||||||
username = username!!,
|
username = username!!,
|
||||||
password = password!!,
|
password = password!!,
|
||||||
httpUsername = null,
|
httpUsername = null,
|
||||||
httpPassword = null,
|
httpPassword = null,
|
||||||
|
clientCertificateAlias = clientCertificateAlias,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +88,7 @@ class FeverRssService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun subscribe(
|
override suspend fun subscribe(
|
||||||
feedLink: String, searchedFeed: SyndFeed, groupId: String,
|
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
|
||||||
isNotification: Boolean, isFullContent: Boolean,
|
isNotification: Boolean, isFullContent: Boolean,
|
||||||
) {
|
) {
|
||||||
throw FeverAPIException("Unsupported")
|
throw FeverAPIException("Unsupported")
|
||||||
|
@ -5,7 +5,6 @@ import android.util.Log
|
|||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.rometools.rome.feed.synd.SyndFeed
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.supervisorScope
|
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.IODispatcher
|
||||||
import me.ash.reader.infrastructure.di.MainDispatcher
|
import me.ash.reader.infrastructure.di.MainDispatcher
|
||||||
import me.ash.reader.infrastructure.html.Readability
|
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.RssHelper
|
||||||
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI
|
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI
|
||||||
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofCategoryIdToStreamId
|
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofCategoryIdToStreamId
|
||||||
@ -72,11 +72,13 @@ class GoogleReaderRssService @Inject constructor(
|
|||||||
private suspend fun getGoogleReaderAPI() =
|
private suspend fun getGoogleReaderAPI() =
|
||||||
GoogleReaderSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
GoogleReaderSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
||||||
GoogleReaderAPI.getInstance(
|
GoogleReaderAPI.getInstance(
|
||||||
|
context = context,
|
||||||
serverUrl = serverUrl!!,
|
serverUrl = serverUrl!!,
|
||||||
username = username!!,
|
username = username!!,
|
||||||
password = password!!,
|
password = password!!,
|
||||||
httpUsername = null,
|
httpUsername = null,
|
||||||
httpPassword = null,
|
httpPassword = null,
|
||||||
|
clientCertificateAlias = clientCertificateAlias,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +99,7 @@ class GoogleReaderRssService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun subscribe(
|
override suspend fun subscribe(
|
||||||
feedLink: String, searchedFeed: SyndFeed, groupId: String,
|
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
|
||||||
isNotification: Boolean, isFullContent: Boolean,
|
isNotification: Boolean, isFullContent: Boolean,
|
||||||
) {
|
) {
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
|
@ -33,15 +33,25 @@ class SyncWorker @AssistedInject constructor(
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val IS_SYNCING = "isSyncing"
|
private const val IS_SYNCING = "isSyncing"
|
||||||
const val WORK_NAME = "ReadYou"
|
private const val WORK_NAME_PERIODIC = "ReadYou"
|
||||||
lateinit var uuid: UUID
|
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(
|
fun enqueueOneTimeWork(
|
||||||
workManager: WorkManager,
|
workManager: WorkManager,
|
||||||
) {
|
) {
|
||||||
workManager.enqueue(OneTimeWorkRequestBuilder<SyncWorker>()
|
workManager.enqueueUniqueWork(
|
||||||
.addTag(WORK_NAME)
|
WORK_NAME_ONETIME,
|
||||||
.build()
|
ExistingWorkPolicy.KEEP,
|
||||||
|
OneTimeWorkRequestBuilder<SyncWorker>().addTag(WORK_TAG).build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,15 +62,16 @@ class SyncWorker @AssistedInject constructor(
|
|||||||
syncOnlyOnWiFi: SyncOnlyOnWiFiPreference,
|
syncOnlyOnWiFi: SyncOnlyOnWiFiPreference,
|
||||||
) {
|
) {
|
||||||
workManager.enqueueUniquePeriodicWork(
|
workManager.enqueueUniquePeriodicWork(
|
||||||
WORK_NAME,
|
WORK_NAME_PERIODIC,
|
||||||
ExistingPeriodicWorkPolicy.REPLACE,
|
ExistingPeriodicWorkPolicy.UPDATE,
|
||||||
PeriodicWorkRequestBuilder<SyncWorker>(syncInterval.value, TimeUnit.MINUTES)
|
PeriodicWorkRequestBuilder<SyncWorker>(syncInterval.value, TimeUnit.MINUTES)
|
||||||
.setConstraints(Constraints.Builder()
|
.setConstraints(
|
||||||
.setRequiresCharging(syncOnlyWhenCharging.value)
|
Constraints.Builder()
|
||||||
.setRequiredNetworkType(if (syncOnlyOnWiFi.value) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
.setRequiresCharging(syncOnlyWhenCharging.value)
|
||||||
.build()
|
.setRequiredNetworkType(if (syncOnlyOnWiFi.value) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
)
|
)
|
||||||
.addTag(WORK_NAME)
|
.addTag(WORK_TAG)
|
||||||
.setInitialDelay(syncInterval.value, TimeUnit.MINUTES)
|
.setInitialDelay(syncInterval.value, TimeUnit.MINUTES)
|
||||||
.build()
|
.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.getLatestApk
|
||||||
import me.ash.reader.ui.ext.isGitHub
|
import me.ash.reader.ui.ext.isGitHub
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import rust.nostr.sdk.Client
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,6 +59,9 @@ class AndroidApp : Application(), Configuration.Provider {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var rssHelper: RssHelper
|
lateinit var rssHelper: RssHelper
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var nostrClient: Client
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var notificationHelper: NotificationHelper
|
lateinit var notificationHelper: NotificationHelper
|
||||||
|
|
||||||
@ -133,7 +137,7 @@ class AndroidApp : Application(), Configuration.Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun workerInit() {
|
private suspend fun workerInit() {
|
||||||
rssService.get().doSync(isOnStart = true)
|
rssService.get().initSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkUpdate() {
|
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
|
package me.ash.reader.infrastructure.di
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.security.KeyChain
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@ -31,15 +33,18 @@ import okhttp3.Cache
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.platform.Platform
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.Socket
|
||||||
import java.security.KeyManagementException
|
import java.security.KeyManagementException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.Principal
|
||||||
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import javax.net.ssl.HostnameVerifier
|
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.X509KeyManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,6 +59,7 @@ object OkHttpClientModule {
|
|||||||
fun provideOkHttpClient(
|
fun provideOkHttpClient(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
): OkHttpClient = cachingHttpClient(
|
): OkHttpClient = cachingHttpClient(
|
||||||
|
context = context,
|
||||||
cacheDirectory = context.cacheDir.resolve("http")
|
cacheDirectory = context.cacheDir.resolve("http")
|
||||||
).newBuilder()
|
).newBuilder()
|
||||||
.addNetworkInterceptor(UserAgentInterceptor)
|
.addNetworkInterceptor(UserAgentInterceptor)
|
||||||
@ -61,11 +67,13 @@ object OkHttpClientModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cachingHttpClient(
|
fun cachingHttpClient(
|
||||||
|
context: Context,
|
||||||
cacheDirectory: File? = null,
|
cacheDirectory: File? = null,
|
||||||
cacheSize: Long = 10L * 1024L * 1024L,
|
cacheSize: Long = 10L * 1024L * 1024L,
|
||||||
trustAllCerts: Boolean = true,
|
trustAllCerts: Boolean = true,
|
||||||
connectTimeoutSecs: Long = 30L,
|
connectTimeoutSecs: Long = 30L,
|
||||||
readTimeoutSecs: Long = 30L,
|
readTimeoutSecs: Long = 30L,
|
||||||
|
clientCertificateAlias: String? = null,
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
|
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
|
||||||
|
|
||||||
@ -78,31 +86,75 @@ fun cachingHttpClient(
|
|||||||
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
|
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
|
||||||
.followRedirects(true)
|
.followRedirects(true)
|
||||||
|
|
||||||
if (trustAllCerts) {
|
if (!clientCertificateAlias.isNullOrBlank() || trustAllCerts) {
|
||||||
builder.trustAllCerts()
|
builder.setupSsl(context, clientCertificateAlias, trustAllCerts)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun OkHttpClient.Builder.trustAllCerts() {
|
fun OkHttpClient.Builder.setupSsl(
|
||||||
|
context: Context,
|
||||||
|
clientCertificateAlias: String?,
|
||||||
|
trustAllCerts: Boolean
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
val trustManager = object : X509TrustManager {
|
val clientKeyManager = clientCertificateAlias?.let { clientAlias ->
|
||||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
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")
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
|
sslContext.init(arrayOf(clientKeyManager), arrayOf(trustManager), null)
|
||||||
val sslSocketFactory = sslContext.socketFactory
|
val sslSocketFactory = sslContext.socketFactory
|
||||||
|
|
||||||
sslSocketFactory(sslSocketFactory, trustManager)
|
sslSocketFactory(sslSocketFactory, trustManager)
|
||||||
.hostnameVerifier(HostnameVerifier { _, _ -> true })
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
// ignore
|
// ignore
|
||||||
} catch (e: KeyManagementException) {
|
} catch (e: KeyManagementException) {
|
||||||
|
@ -15,12 +15,8 @@ val LocalFlowTopBarTonalElevation =
|
|||||||
compositionLocalOf<FlowTopBarTonalElevationPreference> { FlowTopBarTonalElevationPreference.default }
|
compositionLocalOf<FlowTopBarTonalElevationPreference> { FlowTopBarTonalElevationPreference.default }
|
||||||
|
|
||||||
sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||||
object Level0 : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
|
object None : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
|
||||||
object Level1 : FlowTopBarTonalElevationPreference(ElevationTokens.Level1)
|
object Elevated : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
|
||||||
object Level2 : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
|
|
||||||
object Level3 : FlowTopBarTonalElevationPreference(ElevationTokens.Level3)
|
|
||||||
object Level4 : FlowTopBarTonalElevationPreference(ElevationTokens.Level4)
|
|
||||||
object Level5 : FlowTopBarTonalElevationPreference(ElevationTokens.Level5)
|
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@ -30,27 +26,19 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
|||||||
|
|
||||||
fun toDesc(context: Context): String =
|
fun toDesc(context: Context): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
None -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
Elevated -> "Level 2 (${ElevationTokens.Level2}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)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val default = Level0
|
val default = Elevated
|
||||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
val values = listOf(None, Elevated)
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences) =
|
fun fromPreferences(preferences: Preferences) =
|
||||||
when (preferences[DataStoreKey.keys[flowTopBarTonalElevation]?.key as Preferences.Key<Int>]) {
|
when (preferences[DataStoreKey.keys[flowTopBarTonalElevation]?.key as Preferences.Key<Int>]) {
|
||||||
ElevationTokens.Level0 -> Level0
|
ElevationTokens.Level0 -> None
|
||||||
ElevationTokens.Level1 -> Level1
|
ElevationTokens.Level2 -> Elevated
|
||||||
ElevationTokens.Level2 -> Level2
|
|
||||||
ElevationTokens.Level3 -> Level3
|
|
||||||
ElevationTokens.Level4 -> Level4
|
|
||||||
ElevationTokens.Level5 -> Level5
|
|
||||||
else -> default
|
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),
|
readingRenderer = ReadingRendererPreference.fromPreferences(this),
|
||||||
readingBionicReading = ReadingBionicReadingPreference.fromPreferences(this),
|
readingBionicReading = ReadingBionicReadingPreference.fromPreferences(this),
|
||||||
readingTheme = ReadingThemePreference.fromPreferences(this),
|
readingTheme = ReadingThemePreference.fromPreferences(this),
|
||||||
readingDarkTheme = ReadingDarkThemePreference.fromPreferences(this),
|
|
||||||
readingPageTonalElevation = ReadingPageTonalElevationPreference.fromPreferences(this),
|
readingPageTonalElevation = ReadingPageTonalElevationPreference.fromPreferences(this),
|
||||||
readingAutoHideToolbar = ReadingAutoHideToolbarPreference.fromPreferences(this),
|
readingAutoHideToolbar = ReadingAutoHideToolbarPreference.fromPreferences(this),
|
||||||
readingTextFontSize = ReadingTextFontSizePreference.fromPreferences(this),
|
readingTextFontSize = ReadingTextFontSizePreference.fromPreferences(this),
|
||||||
@ -82,6 +81,7 @@ fun Preferences.toSettings(): Settings {
|
|||||||
swipeStartAction = SwipeStartActionPreference.fromPreferences(this),
|
swipeStartAction = SwipeStartActionPreference.fromPreferences(this),
|
||||||
swipeEndAction = SwipeEndActionPreference.fromPreferences(this),
|
swipeEndAction = SwipeEndActionPreference.fromPreferences(this),
|
||||||
markAsReadOnScroll = MarkAsReadOnScrollPreference.fromPreferences(this),
|
markAsReadOnScroll = MarkAsReadOnScrollPreference.fromPreferences(this),
|
||||||
|
hideEmptyGroups = HideEmptyGroupsPreference.fromPreferences(this),
|
||||||
pullToSwitchArticle = PullToSwitchArticlePreference.fromPreference(this),
|
pullToSwitchArticle = PullToSwitchArticlePreference.fromPreference(this),
|
||||||
openLink = OpenLinkPreference.fromPreferences(this),
|
openLink = OpenLinkPreference.fromPreferences(this),
|
||||||
openLinkSpecificBrowser = OpenLinkSpecificBrowserPreference.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 }
|
compositionLocalOf<ReadingPageTonalElevationPreference> { ReadingPageTonalElevationPreference.default }
|
||||||
|
|
||||||
sealed class ReadingPageTonalElevationPreference(val value: Int) : Preference() {
|
sealed class ReadingPageTonalElevationPreference(val value: Int) : Preference() {
|
||||||
object Level0 : ReadingPageTonalElevationPreference(ElevationTokens.Level0)
|
data object Outlined : ReadingPageTonalElevationPreference(ElevationTokens.Level0)
|
||||||
object Level1 : ReadingPageTonalElevationPreference(ElevationTokens.Level1)
|
data object Elevated : ReadingPageTonalElevationPreference(ElevationTokens.Level2)
|
||||||
object Level2 : ReadingPageTonalElevationPreference(ElevationTokens.Level2)
|
|
||||||
object Level3 : ReadingPageTonalElevationPreference(ElevationTokens.Level3)
|
|
||||||
object Level4 : ReadingPageTonalElevationPreference(ElevationTokens.Level4)
|
|
||||||
object Level5 : ReadingPageTonalElevationPreference(ElevationTokens.Level5)
|
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
context.dataStore.put(DataStoreKey.readingPageTonalElevation, value)
|
context.dataStore.put(readingPageTonalElevation, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toDesc(context: Context): String =
|
fun toDesc(context: Context): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
Outlined -> "${ElevationTokens.Level0}dp"
|
||||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
Elevated -> "${ElevationTokens.Level2}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)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val default = Level0
|
val default = Outlined
|
||||||
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
|
val values = listOf(Outlined, Elevated)
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences) =
|
fun fromPreferences(preferences: Preferences) =
|
||||||
when (preferences[DataStoreKey.keys[readingPageTonalElevation]?.key as Preferences.Key<Int>]) {
|
when (preferences[DataStoreKey.keys[readingPageTonalElevation]?.key as Preferences.Key<Int>]) {
|
||||||
ElevationTokens.Level0 -> Level0
|
ElevationTokens.Level0 -> Outlined
|
||||||
ElevationTokens.Level1 -> Level1
|
ElevationTokens.Level2 -> Elevated
|
||||||
ElevationTokens.Level2 -> Level2
|
|
||||||
ElevationTokens.Level3 -> Level3
|
|
||||||
ElevationTokens.Level4 -> Level4
|
|
||||||
ElevationTokens.Level5 -> Level5
|
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,6 @@ data class Settings(
|
|||||||
val readingRenderer: ReadingRendererPreference = ReadingRendererPreference.default,
|
val readingRenderer: ReadingRendererPreference = ReadingRendererPreference.default,
|
||||||
val readingBionicReading: ReadingBionicReadingPreference = ReadingBionicReadingPreference.default,
|
val readingBionicReading: ReadingBionicReadingPreference = ReadingBionicReadingPreference.default,
|
||||||
val readingTheme: ReadingThemePreference = ReadingThemePreference.default,
|
val readingTheme: ReadingThemePreference = ReadingThemePreference.default,
|
||||||
val readingDarkTheme: ReadingDarkThemePreference = ReadingDarkThemePreference.default,
|
|
||||||
val readingPageTonalElevation: ReadingPageTonalElevationPreference = ReadingPageTonalElevationPreference.default,
|
val readingPageTonalElevation: ReadingPageTonalElevationPreference = ReadingPageTonalElevationPreference.default,
|
||||||
val readingAutoHideToolbar: ReadingAutoHideToolbarPreference = ReadingAutoHideToolbarPreference.default,
|
val readingAutoHideToolbar: ReadingAutoHideToolbarPreference = ReadingAutoHideToolbarPreference.default,
|
||||||
val readingTextFontSize: Int = ReadingTextFontSizePreference.default,
|
val readingTextFontSize: Int = ReadingTextFontSizePreference.default,
|
||||||
@ -80,6 +79,7 @@ data class Settings(
|
|||||||
val swipeStartAction: SwipeStartActionPreference = SwipeStartActionPreference.default,
|
val swipeStartAction: SwipeStartActionPreference = SwipeStartActionPreference.default,
|
||||||
val swipeEndAction: SwipeEndActionPreference = SwipeEndActionPreference.default,
|
val swipeEndAction: SwipeEndActionPreference = SwipeEndActionPreference.default,
|
||||||
val markAsReadOnScroll: MarkAsReadOnScrollPreference = MarkAsReadOnScrollPreference.default,
|
val markAsReadOnScroll: MarkAsReadOnScrollPreference = MarkAsReadOnScrollPreference.default,
|
||||||
|
val hideEmptyGroups: HideEmptyGroupsPreference = HideEmptyGroupsPreference.default,
|
||||||
val pullToSwitchArticle: PullToSwitchArticlePreference = PullToSwitchArticlePreference.default,
|
val pullToSwitchArticle: PullToSwitchArticlePreference = PullToSwitchArticlePreference.default,
|
||||||
val openLink: OpenLinkPreference = OpenLinkPreference.default,
|
val openLink: OpenLinkPreference = OpenLinkPreference.default,
|
||||||
val openLinkSpecificBrowser: OpenLinkSpecificBrowserPreference = OpenLinkSpecificBrowserPreference.default,
|
val openLinkSpecificBrowser: OpenLinkSpecificBrowserPreference = OpenLinkSpecificBrowserPreference.default,
|
||||||
@ -146,7 +146,6 @@ fun SettingsProvider(
|
|||||||
LocalReadingRenderer provides settings.readingRenderer,
|
LocalReadingRenderer provides settings.readingRenderer,
|
||||||
LocalReadingBionicReading provides settings.readingBionicReading,
|
LocalReadingBionicReading provides settings.readingBionicReading,
|
||||||
LocalReadingTheme provides settings.readingTheme,
|
LocalReadingTheme provides settings.readingTheme,
|
||||||
LocalReadingDarkTheme provides settings.readingDarkTheme,
|
|
||||||
LocalReadingPageTonalElevation provides settings.readingPageTonalElevation,
|
LocalReadingPageTonalElevation provides settings.readingPageTonalElevation,
|
||||||
LocalReadingAutoHideToolbar provides settings.readingAutoHideToolbar,
|
LocalReadingAutoHideToolbar provides settings.readingAutoHideToolbar,
|
||||||
LocalReadingTextFontSize provides settings.readingTextFontSize,
|
LocalReadingTextFontSize provides settings.readingTextFontSize,
|
||||||
@ -172,6 +171,7 @@ fun SettingsProvider(
|
|||||||
LocalArticleListSwipeStartAction provides settings.swipeStartAction,
|
LocalArticleListSwipeStartAction provides settings.swipeStartAction,
|
||||||
LocalArticleListSwipeEndAction provides settings.swipeEndAction,
|
LocalArticleListSwipeEndAction provides settings.swipeEndAction,
|
||||||
LocalMarkAsReadOnScroll provides settings.markAsReadOnScroll,
|
LocalMarkAsReadOnScroll provides settings.markAsReadOnScroll,
|
||||||
|
LocalHideEmptyGroups provides settings.hideEmptyGroups,
|
||||||
LocalPullToSwitchArticle provides settings.pullToSwitchArticle,
|
LocalPullToSwitchArticle provides settings.pullToSwitchArticle,
|
||||||
LocalOpenLink provides settings.openLink,
|
LocalOpenLink provides settings.openLink,
|
||||||
LocalOpenLinkSpecificBrowser provides settings.openLinkSpecificBrowser,
|
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.domain.model.group.GroupWithFeed
|
||||||
import me.ash.reader.infrastructure.di.IODispatcher
|
import me.ash.reader.infrastructure.di.IODispatcher
|
||||||
import me.ash.reader.ui.ext.extractDomain
|
import me.ash.reader.ui.ext.extractDomain
|
||||||
|
import me.ash.reader.ui.ext.isNostrUri
|
||||||
import me.ash.reader.ui.ext.spacerDollar
|
import me.ash.reader.ui.ext.spacerDollar
|
||||||
|
import rust.nostr.sdk.Client
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -48,7 +50,21 @@ class OPMLDataSource @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} 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(
|
Feed(
|
||||||
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
|
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
|
||||||
name = outline.extractName(),
|
name = outline.extractName(),
|
||||||
@ -58,7 +74,8 @@ class OPMLDataSource @Inject constructor(
|
|||||||
isNotification = outline.extractPresetNotification(),
|
isNotification = outline.extractPresetNotification(),
|
||||||
isFullContent = outline.extractPresetFullContent(),
|
isFullContent = outline.extractPresetFullContent(),
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
groupWithFeedList.addFeedToDefault(feedToAdd)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var groupId = defaultGroup.id
|
var groupId = defaultGroup.id
|
||||||
@ -74,7 +91,21 @@ class OPMLDataSource @Inject constructor(
|
|||||||
}
|
}
|
||||||
for (subOutline in outline.subElements) {
|
for (subOutline in outline.subElements) {
|
||||||
if (subOutline != null && subOutline.attributes != null) {
|
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(
|
Feed(
|
||||||
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
|
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
|
||||||
name = subOutline.extractName(),
|
name = subOutline.extractName(),
|
||||||
@ -84,7 +115,8 @@ class OPMLDataSource @Inject constructor(
|
|||||||
isNotification = subOutline.extractPresetNotification(),
|
isNotification = subOutline.extractPresetNotification(),
|
||||||
isFullContent = subOutline.extractPresetFullContent(),
|
isFullContent = subOutline.extractPresetFullContent(),
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
groupWithFeedList.addFeed(feedToAdd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package me.ash.reader.infrastructure.rss
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.rometools.rome.feed.synd.SyndEntry
|
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.feed.synd.SyndImageImpl
|
||||||
import com.rometools.rome.io.SyndFeedInput
|
import com.rometools.rome.io.SyndFeedInput
|
||||||
import com.rometools.rome.io.XmlReader
|
import com.rometools.rome.io.XmlReader
|
||||||
@ -12,18 +11,30 @@ import kotlinx.coroutines.CoroutineDispatcher
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.ash.reader.domain.model.article.Article
|
import me.ash.reader.domain.model.article.Article
|
||||||
import me.ash.reader.domain.model.feed.Feed
|
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.domain.repository.FeedDao
|
||||||
import me.ash.reader.infrastructure.di.IODispatcher
|
import me.ash.reader.infrastructure.di.IODispatcher
|
||||||
import me.ash.reader.infrastructure.html.Readability
|
import me.ash.reader.infrastructure.html.Readability
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
import me.ash.reader.ui.ext.decodeHTML
|
import me.ash.reader.ui.ext.decodeHTML
|
||||||
import me.ash.reader.ui.ext.extractDomain
|
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.isFuture
|
||||||
|
import me.ash.reader.ui.ext.isNostrUri
|
||||||
import me.ash.reader.ui.ext.spacerDollar
|
import me.ash.reader.ui.ext.spacerDollar
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.executeAsync
|
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.io.InputStream
|
||||||
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -39,15 +50,24 @@ class RssHelper @Inject constructor(
|
|||||||
@IODispatcher
|
@IODispatcher
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
|
private val nostrClient: Client
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
suspend fun searchFeed(feedLink: String): SyndFeed {
|
suspend fun searchFeed(feedLink: String): FetchedFeed? {
|
||||||
return withContext(ioDispatcher) {
|
return withContext(ioDispatcher) {
|
||||||
SyndFeedInput().build(XmlReader(inputStream(okHttpClient, feedLink))).also {
|
if(feedLink.isNostrUri()) {
|
||||||
it.icon = SyndImageImpl()
|
NostrFeed.fetchFeedFrom(feedLink, nostrClient)
|
||||||
it.icon.link = queryRssIconLink(feedLink)
|
}
|
||||||
it.icon.url = it.icon.link
|
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? {
|
fun findThumbnail(syndEntry: SyndEntry): String? {
|
||||||
if (syndEntry.enclosures?.firstOrNull()?.url != null) {
|
if (syndEntry.enclosures?.firstOrNull()?.url != null) {
|
||||||
return syndEntry.enclosures.first().url
|
return syndEntry.enclosures.first().url
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
package me.ash.reader.infrastructure.rss.provider
|
package me.ash.reader.infrastructure.rss.provider
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import me.ash.reader.infrastructure.di.UserAgentInterceptor
|
import me.ash.reader.infrastructure.di.UserAgentInterceptor
|
||||||
import me.ash.reader.infrastructure.di.cachingHttpClient
|
import me.ash.reader.infrastructure.di.cachingHttpClient
|
||||||
import okhttp3.OkHttpClient
|
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()
|
.newBuilder()
|
||||||
.addNetworkInterceptor(UserAgentInterceptor)
|
.addNetworkInterceptor(UserAgentInterceptor)
|
||||||
.build()
|
.build()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package me.ash.reader.infrastructure.rss.provider.fever
|
package me.ash.reader.infrastructure.rss.provider.fever
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import me.ash.reader.infrastructure.exception.FeverAPIException
|
import me.ash.reader.infrastructure.exception.FeverAPIException
|
||||||
import me.ash.reader.infrastructure.rss.provider.ProviderAPI
|
import me.ash.reader.infrastructure.rss.provider.ProviderAPI
|
||||||
import me.ash.reader.ui.ext.encodeBase64
|
import me.ash.reader.ui.ext.encodeBase64
|
||||||
@ -10,11 +11,13 @@ import okhttp3.executeAsync
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class FeverAPI private constructor(
|
class FeverAPI private constructor(
|
||||||
|
context: Context,
|
||||||
private val serverUrl: String,
|
private val serverUrl: String,
|
||||||
private val apiKey: String,
|
private val apiKey: String,
|
||||||
private val httpUsername: String? = null,
|
private val httpUsername: String? = null,
|
||||||
private val httpPassword: String? = null,
|
private val httpPassword: String? = null,
|
||||||
) : ProviderAPI() {
|
clientCertificateAlias: String? = null,
|
||||||
|
) : ProviderAPI(context, clientCertificateAlias) {
|
||||||
|
|
||||||
private suspend inline fun <reified T> postRequest(query: String?): T {
|
private suspend inline fun <reified T> postRequest(query: String?): T {
|
||||||
val response = client.newCall(
|
val response = client.newCall(
|
||||||
@ -104,14 +107,16 @@ class FeverAPI private constructor(
|
|||||||
private val instances: ConcurrentHashMap<String, FeverAPI> = ConcurrentHashMap()
|
private val instances: ConcurrentHashMap<String, FeverAPI> = ConcurrentHashMap()
|
||||||
|
|
||||||
fun getInstance(
|
fun getInstance(
|
||||||
|
context: Context,
|
||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
httpUsername: String? = null,
|
httpUsername: String? = null,
|
||||||
httpPassword: String? = null,
|
httpPassword: String? = null,
|
||||||
|
clientCertificateAlias: String? = null,
|
||||||
): FeverAPI = "$username:$password".md5().run {
|
): FeverAPI = "$username:$password".md5().run {
|
||||||
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword") {
|
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword$clientCertificateAlias") {
|
||||||
FeverAPI(serverUrl, this, httpUsername, httpPassword)
|
FeverAPI(context, serverUrl, this, httpUsername, httpPassword, clientCertificateAlias)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package me.ash.reader.infrastructure.rss.provider.greader
|
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.di.USER_AGENT_STRING
|
||||||
import me.ash.reader.infrastructure.exception.GoogleReaderAPIException
|
import me.ash.reader.infrastructure.exception.GoogleReaderAPIException
|
||||||
import me.ash.reader.infrastructure.exception.RetryException
|
import me.ash.reader.infrastructure.exception.RetryException
|
||||||
@ -10,12 +11,14 @@ import okhttp3.executeAsync
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class GoogleReaderAPI private constructor(
|
class GoogleReaderAPI private constructor(
|
||||||
|
context: Context,
|
||||||
private val serverUrl: String,
|
private val serverUrl: String,
|
||||||
private val username: String,
|
private val username: String,
|
||||||
private val password: String,
|
private val password: String,
|
||||||
private val httpUsername: String? = null,
|
private val httpUsername: String? = null,
|
||||||
private val httpPassword: String? = null,
|
private val httpPassword: String? = null,
|
||||||
) : ProviderAPI() {
|
clientCertificateAlias: String? = null,
|
||||||
|
) : ProviderAPI(context, clientCertificateAlias) {
|
||||||
|
|
||||||
enum class Stream(val tag: String) {
|
enum class Stream(val tag: String) {
|
||||||
ALL_ITEMS("user/-/state/com.google/reading-list"),
|
ALL_ITEMS("user/-/state/com.google/reading-list"),
|
||||||
@ -350,13 +353,15 @@ class GoogleReaderAPI private constructor(
|
|||||||
private val instances: ConcurrentHashMap<String, GoogleReaderAPI> = ConcurrentHashMap()
|
private val instances: ConcurrentHashMap<String, GoogleReaderAPI> = ConcurrentHashMap()
|
||||||
|
|
||||||
fun getInstance(
|
fun getInstance(
|
||||||
|
context: Context,
|
||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
httpUsername: String? = null,
|
httpUsername: String? = null,
|
||||||
httpPassword: String? = null,
|
httpPassword: String? = null,
|
||||||
): GoogleReaderAPI = instances.getOrPut("$serverUrl$username$password$httpUsername$httpPassword") {
|
clientCertificateAlias: String? = null
|
||||||
GoogleReaderAPI(serverUrl, username, password, httpUsername, httpPassword)
|
): GoogleReaderAPI = instances.getOrPut("$serverUrl$username$password$httpUsername$httpPassword$clientCertificateAlias") {
|
||||||
|
GoogleReaderAPI(context, serverUrl, username, password, httpUsername, httpPassword, clientCertificateAlias)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearInstance() {
|
fun clearInstance() {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package me.ash.reader.ui.component.base
|
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.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -22,6 +24,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
@ -46,6 +49,7 @@ fun RYOutlineTextField(
|
|||||||
errorMessage: String = "",
|
errorMessage: String = "",
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
@ -59,7 +63,11 @@ fun RYOutlineTextField(
|
|||||||
}
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = if (onClick != null) {
|
||||||
|
Modifier.focusProperties { canFocus = false }
|
||||||
|
} else {
|
||||||
|
Modifier.focusRequester(focusRequester)
|
||||||
|
},
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
focusedContainerColor = Color.Transparent
|
focusedContainerColor = Color.Transparent
|
||||||
@ -115,5 +123,18 @@ fun RYOutlineTextField(
|
|||||||
},
|
},
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
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.LocalOpenLink
|
||||||
import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser
|
import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
|
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.LocalReadingImageHorizontalPadding
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingImageRoundedCorners
|
import me.ash.reader.infrastructure.preference.LocalReadingImageRoundedCorners
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
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.LocalReadingTextHorizontalPadding
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingTextLetterSpacing
|
import me.ash.reader.infrastructure.preference.LocalReadingTextLetterSpacing
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingTextLineHeight
|
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.openURL
|
||||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||||
import me.ash.reader.ui.theme.palette.alwaysLight
|
import me.ash.reader.ui.theme.palette.alwaysLight
|
||||||
@ -55,6 +58,7 @@ fun RYWebView(
|
|||||||
val linkTextColor: Int = MaterialTheme.colorScheme.primary.toArgb()
|
val linkTextColor: Int = MaterialTheme.colorScheme.primary.toArgb()
|
||||||
val subheadBold: Boolean = LocalReadingSubheadBold.current.value
|
val subheadBold: Boolean = LocalReadingSubheadBold.current.value
|
||||||
val subheadUpperCase: Boolean = LocalReadingSubheadUpperCase.current.value
|
val subheadUpperCase: Boolean = LocalReadingSubheadUpperCase.current.value
|
||||||
|
val readingFonts = LocalReadingFonts.current
|
||||||
val fontSize: Int = LocalReadingTextFontSize.current
|
val fontSize: Int = LocalReadingTextFontSize.current
|
||||||
val letterSpacing: Float = LocalReadingTextLetterSpacing.current
|
val letterSpacing: Float = LocalReadingTextLetterSpacing.current
|
||||||
val lineHeight: Float = LocalReadingTextLineHeight.current
|
val lineHeight: Float = LocalReadingTextLineHeight.current
|
||||||
@ -69,6 +73,7 @@ fun RYWebView(
|
|||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
WebViewLayout.get(
|
WebViewLayout.get(
|
||||||
context = context,
|
context = context,
|
||||||
|
readingFontsPreference = readingFonts,
|
||||||
webViewClient = WebViewClient(
|
webViewClient = WebViewClient(
|
||||||
context = context,
|
context = context,
|
||||||
refererDomain = refererDomain,
|
refererDomain = refererDomain,
|
||||||
@ -81,6 +86,11 @@ fun RYWebView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val fontPath =
|
||||||
|
if (readingFonts is ReadingFontsPreference.External) ExternalFonts.FontType.ReadingFont.toPath(
|
||||||
|
context
|
||||||
|
) else null
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
factory = { webView },
|
factory = { webView },
|
||||||
@ -95,6 +105,7 @@ fun RYWebView(
|
|||||||
WebViewHtml.HTML.format(
|
WebViewHtml.HTML.format(
|
||||||
WebViewStyle.get(
|
WebViewStyle.get(
|
||||||
fontSize = fontSize,
|
fontSize = fontSize,
|
||||||
|
fontPath = fontPath,
|
||||||
lineHeight = lineHeight,
|
lineHeight = lineHeight,
|
||||||
letterSpacing = letterSpacing,
|
letterSpacing = letterSpacing,
|
||||||
textMargin = textMargin,
|
textMargin = textMargin,
|
||||||
|
@ -65,7 +65,8 @@ class WebViewClient(
|
|||||||
var imgs = document.getElementsByTagName("img");
|
var imgs = document.getElementsByTagName("img");
|
||||||
for(var i = 0; i < imgs.length; i++){
|
for(var i = 0; i < imgs.length; i++){
|
||||||
imgs[i].pos = i;
|
imgs[i].pos = i;
|
||||||
imgs[i].onclick = function() {
|
imgs[i].onclick = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
window.${JavaScriptInterface.NAME}.onImgTagClick(this.src, this.alt);
|
window.${JavaScriptInterface.NAME}.onImgTagClick(this.src, this.alt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,15 @@ import android.content.Context
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import me.ash.reader.infrastructure.preference.BasicFontsPreference
|
||||||
|
import me.ash.reader.infrastructure.preference.ReadingFontsPreference
|
||||||
|
|
||||||
object WebViewLayout {
|
object WebViewLayout {
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
fun get(
|
fun get(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
readingFontsPreference: ReadingFontsPreference,
|
||||||
webViewClient: WebViewClient,
|
webViewClient: WebViewClient,
|
||||||
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
|
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
|
||||||
) = WebView(context).apply {
|
) = WebView(context).apply {
|
||||||
@ -20,6 +23,20 @@ object WebViewLayout {
|
|||||||
isVerticalScrollBarEnabled = true
|
isVerticalScrollBarEnabled = true
|
||||||
setBackgroundColor(Color.TRANSPARENT)
|
setBackgroundColor(Color.TRANSPARENT)
|
||||||
with(this.settings) {
|
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
|
domStorageEnabled = true
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
addJavascriptInterface(object : JavaScriptInterface {
|
addJavascriptInterface(object : JavaScriptInterface {
|
||||||
|
@ -4,8 +4,24 @@ object WebViewStyle {
|
|||||||
|
|
||||||
private fun argbToCssColor(argb: Int): String = String.format("#%06X", 0xFFFFFF and argb)
|
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(
|
fun get(
|
||||||
fontSize: Int,
|
fontSize: Int,
|
||||||
|
fontPath: String? = null,
|
||||||
lineHeight: Float,
|
lineHeight: Float,
|
||||||
letterSpacing: Float,
|
letterSpacing: Float,
|
||||||
textMargin: Int,
|
textMargin: Int,
|
||||||
@ -24,21 +40,22 @@ object WebViewStyle {
|
|||||||
selectionTextColor: Int,
|
selectionTextColor: Int,
|
||||||
selectionBgColor: Int,
|
selectionBgColor: Int,
|
||||||
): String = """
|
): String = """
|
||||||
|
${applyFontFace(fontPath)}
|
||||||
:root {
|
:root {
|
||||||
/* --font-family: Inter; */
|
${applyFontFamily(fontPath)}
|
||||||
--font-size: ${fontSize}px;
|
--font-size: ${fontSize}px;
|
||||||
--line-height: ${lineHeight * 1.5f};
|
--line-height: ${lineHeight * 1.5f};
|
||||||
--letter-spacing: ${letterSpacing}px;
|
--letter-spacing: ${letterSpacing}px;
|
||||||
--text-margin: ${textMargin}px;
|
--text-margin: ${textMargin}px;
|
||||||
--text-color: ${argbToCssColor(textColor)};
|
--text-color: ${argbToCssColor(textColor)};
|
||||||
--text-bold: ${if(textBold) "600" else "normal"};
|
--text-bold: ${if (textBold) "600" else "normal"};
|
||||||
--text-align: ${textAlign};
|
--text-align: ${textAlign};
|
||||||
--bold-text-color: ${argbToCssColor(boldTextColor)};
|
--bold-text-color: ${argbToCssColor(boldTextColor)};
|
||||||
--link-text-color: ${argbToCssColor(linkTextColor)};
|
--link-text-color: ${argbToCssColor(linkTextColor)};
|
||||||
--selection-text-color: ${argbToCssColor(selectionTextColor)};
|
--selection-text-color: ${argbToCssColor(selectionTextColor)};
|
||||||
--selection-bg-color: ${argbToCssColor(selectionBgColor)};
|
--selection-bg-color: ${argbToCssColor(selectionBgColor)};
|
||||||
--subhead-bold: ${if(subheadBold) "600" else "normal"};
|
--subhead-bold: ${if (subheadBold) "600" else "normal"};
|
||||||
--subhead-upper-case: ${if(subheadUpperCase) "uppercase" else "none"};
|
--subhead-upper-case: ${if (subheadUpperCase) "uppercase" else "none"};
|
||||||
--img-margin: ${imgMargin}px;
|
--img-margin: ${imgMargin}px;
|
||||||
--img-border-radius: ${imgBorderRadius}px;
|
--img-border-radius: ${imgBorderRadius}px;
|
||||||
--content-padding;
|
--content-padding;
|
||||||
@ -312,7 +329,7 @@ figure {
|
|||||||
text-align: var(--text-align) !important;
|
text-align: var(--text-align) !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
opacity: 0.8 !important;
|
opacity: 0.8 !important;
|
||||||
font-size: 0.8em !important;
|
font-size: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure * {
|
figure * {
|
||||||
@ -323,7 +340,7 @@ figure p,
|
|||||||
caption,
|
caption,
|
||||||
figcaption {
|
figcaption {
|
||||||
opacity: 0.8 !important;
|
opacity: 0.8 !important;
|
||||||
font-size: 0.8em !important;
|
font-size: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
@ -136,7 +136,6 @@ data class DataStoreKey<T>(
|
|||||||
// Reading page
|
// Reading page
|
||||||
const val readingRenderer = "readingRender"
|
const val readingRenderer = "readingRender"
|
||||||
const val readingBionicReading = "readingBionicReading"
|
const val readingBionicReading = "readingBionicReading"
|
||||||
const val readingDarkTheme = "readingDarkTheme"
|
|
||||||
const val readingPageTonalElevation = "readingPageTonalElevation"
|
const val readingPageTonalElevation = "readingPageTonalElevation"
|
||||||
const val readingTextFontSize = "readingTextFontSize"
|
const val readingTextFontSize = "readingTextFontSize"
|
||||||
const val readingTextLineHeight = "readingTextLineHeight"
|
const val readingTextLineHeight = "readingTextLineHeight"
|
||||||
@ -163,6 +162,7 @@ data class DataStoreKey<T>(
|
|||||||
const val swipeStartAction = "swipeStartAction"
|
const val swipeStartAction = "swipeStartAction"
|
||||||
const val swipeEndAction = "swipeEndAction"
|
const val swipeEndAction = "swipeEndAction"
|
||||||
const val markAsReadOnScroll = "markAsReadOnScroll"
|
const val markAsReadOnScroll = "markAsReadOnScroll"
|
||||||
|
const val hideEmptyGroups = "hideEmptyGroups"
|
||||||
const val pullToSwitchArticle = "pullToSwitchArticle"
|
const val pullToSwitchArticle = "pullToSwitchArticle"
|
||||||
const val openLink = "openLink"
|
const val openLink = "openLink"
|
||||||
const val openLinkAppSpecificBrowser = "openLinkAppSpecificBrowser"
|
const val openLinkAppSpecificBrowser = "openLinkAppSpecificBrowser"
|
||||||
@ -212,7 +212,6 @@ data class DataStoreKey<T>(
|
|||||||
// Reading page
|
// Reading page
|
||||||
readingRenderer to DataStoreKey(intPreferencesKey(readingRenderer), Int::class.java),
|
readingRenderer to DataStoreKey(intPreferencesKey(readingRenderer), Int::class.java),
|
||||||
readingBionicReading to DataStoreKey(booleanPreferencesKey(readingBionicReading), Boolean::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),
|
readingPageTonalElevation to DataStoreKey(intPreferencesKey(readingPageTonalElevation), Int::class.java),
|
||||||
readingTextFontSize to DataStoreKey(intPreferencesKey(readingTextFontSize), Int::class.java),
|
readingTextFontSize to DataStoreKey(intPreferencesKey(readingTextFontSize), Int::class.java),
|
||||||
readingTextLineHeight to DataStoreKey(floatPreferencesKey(readingTextLineHeight), Float::class.java),
|
readingTextLineHeight to DataStoreKey(floatPreferencesKey(readingTextLineHeight), Float::class.java),
|
||||||
@ -241,6 +240,7 @@ data class DataStoreKey<T>(
|
|||||||
booleanPreferencesKey(markAsReadOnScroll),
|
booleanPreferencesKey(markAsReadOnScroll),
|
||||||
Boolean::class.java
|
Boolean::class.java
|
||||||
),
|
),
|
||||||
|
hideEmptyGroups to DataStoreKey(booleanPreferencesKey(hideEmptyGroups), Boolean::class.java),
|
||||||
pullToSwitchArticle to DataStoreKey(booleanPreferencesKey(pullToSwitchArticle), Boolean::class.java),
|
pullToSwitchArticle to DataStoreKey(booleanPreferencesKey(pullToSwitchArticle), Boolean::class.java),
|
||||||
openLink to DataStoreKey(intPreferencesKey(openLink), Int::class.java),
|
openLink to DataStoreKey(intPreferencesKey(openLink), Int::class.java),
|
||||||
openLinkAppSpecificBrowser to DataStoreKey(stringPreferencesKey(openLinkAppSpecificBrowser), String::class.java),
|
openLinkAppSpecificBrowser to DataStoreKey(stringPreferencesKey(openLinkAppSpecificBrowser), String::class.java),
|
||||||
|
@ -8,88 +8,38 @@
|
|||||||
|
|
||||||
package me.ash.reader.ui.ext
|
package me.ash.reader.ui.ext
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.navigation.NamedNavArgument
|
import androidx.navigation.NamedNavArgument
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavDeepLink
|
import androidx.navigation.NavDeepLink
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import me.ash.reader.ui.motion.EmphasizedAccelerate
|
|
||||||
import me.ash.reader.ui.motion.EmphasizedDecelerate
|
|
||||||
import me.ash.reader.ui.motion.materialSharedAxisXIn
|
import me.ash.reader.ui.motion.materialSharedAxisXIn
|
||||||
import me.ash.reader.ui.motion.materialSharedAxisXOut
|
import me.ash.reader.ui.motion.materialSharedAxisXOut
|
||||||
|
|
||||||
private const val INITIAL_OFFSET_FACTOR = 0.10f
|
private const val INITIAL_OFFSET_FACTOR = 0.10f
|
||||||
private const val INITIAL_SCALE_FACTOR = 0.8f
|
|
||||||
|
|
||||||
|
|
||||||
fun NavGraphBuilder.animatedComposable(
|
fun NavGraphBuilder.animatedComposable(
|
||||||
route: String,
|
route: String,
|
||||||
arguments: List<NamedNavArgument> = emptyList(),
|
arguments: List<NamedNavArgument> = emptyList(),
|
||||||
deepLinks: List<NavDeepLink> = emptyList(),
|
deepLinks: List<NavDeepLink> = emptyList(),
|
||||||
usePredictiveBack: Boolean = Build.VERSION.SDK_INT >= 34,
|
content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
|
||||||
content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit,
|
) = composable(
|
||||||
) {
|
|
||||||
if (usePredictiveBack) {
|
|
||||||
animatedComposablePredictiveBack(route, arguments, deepLinks, content)
|
|
||||||
} else {
|
|
||||||
animatedComposableLegacy(route, arguments, deepLinks, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavGraphBuilder.animatedComposablePredictiveBack(
|
|
||||||
route: String,
|
|
||||||
arguments: List<NamedNavArgument> = emptyList(),
|
|
||||||
deepLinks: List<NavDeepLink> = emptyList(),
|
|
||||||
content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit,
|
|
||||||
) =
|
|
||||||
composable(
|
|
||||||
route = route,
|
route = route,
|
||||||
arguments = arguments,
|
arguments = arguments,
|
||||||
deepLinks = deepLinks,
|
deepLinks = deepLinks,
|
||||||
enterTransition = { materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
enterTransition = {
|
||||||
|
materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() })
|
||||||
|
},
|
||||||
exitTransition = {
|
exitTransition = {
|
||||||
materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
|
materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
|
||||||
},
|
},
|
||||||
popEnterTransition = {
|
popEnterTransition = {
|
||||||
scaleIn(
|
materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
|
||||||
animationSpec = tween(durationMillis = 350, easing = EmphasizedDecelerate),
|
|
||||||
initialScale = INITIAL_SCALE_FACTOR,
|
|
||||||
) + materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
|
|
||||||
},
|
},
|
||||||
popExitTransition = {
|
popExitTransition = {
|
||||||
materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) +
|
materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() })
|
||||||
scaleOut(
|
|
||||||
targetScale = INITIAL_SCALE_FACTOR,
|
|
||||||
animationSpec = tween(durationMillis = 350, easing = EmphasizedAccelerate),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
content = content,
|
content = content
|
||||||
)
|
|
||||||
|
|
||||||
fun NavGraphBuilder.animatedComposableLegacy(
|
|
||||||
route: String,
|
|
||||||
arguments: List<NamedNavArgument> = emptyList(),
|
|
||||||
deepLinks: List<NavDeepLink> = emptyList(),
|
|
||||||
content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
|
|
||||||
) = composable(
|
|
||||||
route = route,
|
|
||||||
arguments = arguments,
|
|
||||||
deepLinks = deepLinks,
|
|
||||||
enterTransition = {
|
|
||||||
materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() })
|
|
||||||
},
|
|
||||||
exitTransition = {
|
|
||||||
materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
|
|
||||||
},
|
|
||||||
popEnterTransition = {
|
|
||||||
materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() })
|
|
||||||
},
|
|
||||||
popExitTransition = {
|
|
||||||
materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() })
|
|
||||||
},
|
|
||||||
content = content
|
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,9 @@ package me.ash.reader.ui.ext
|
|||||||
|
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.util.Base64
|
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.math.BigInteger
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.text.Bidi
|
import java.text.Bidi
|
||||||
@ -18,7 +21,7 @@ fun String.formatUrl(): String {
|
|||||||
if (this.startsWith("//")) {
|
if (this.startsWith("//")) {
|
||||||
return "https:$this"
|
return "https:$this"
|
||||||
}
|
}
|
||||||
val regex = Regex("^(https?|ftp|file).*")
|
val regex = Regex("^(https?|ftp|file|nostr).*")
|
||||||
return if (!regex.matches(this)) {
|
return if (!regex.matches(this)) {
|
||||||
"https://$this"
|
"https://$this"
|
||||||
} else {
|
} else {
|
||||||
@ -61,3 +64,16 @@ fun String?.extractDomain(): String? {
|
|||||||
val domainMatchResult = domainRegex.find(this)
|
val domainMatchResult = domainRegex.find(this)
|
||||||
return domainMatchResult?.value
|
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 kotlinx.coroutines.flow.collectLatest
|
||||||
import me.ash.reader.domain.model.general.Filter
|
import me.ash.reader.domain.model.general.Filter
|
||||||
import me.ash.reader.infrastructure.preference.LocalDarkTheme
|
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.animatedComposable
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
import me.ash.reader.ui.ext.findActivity
|
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.feeds.FeedsPageStylePage
|
||||||
import me.ash.reader.ui.page.settings.color.flow.FlowPageStylePage
|
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.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.ReadingImagePage
|
||||||
import me.ash.reader.ui.page.settings.color.reading.ReadingStylePage
|
import me.ash.reader.ui.page.settings.color.reading.ReadingStylePage
|
||||||
import me.ash.reader.ui.page.settings.color.reading.ReadingTextPage
|
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(
|
AppTheme(
|
||||||
useDarkTheme = if (isReadingPage) LocalReadingDarkTheme.current.isDarkTheme()
|
useDarkTheme = LocalDarkTheme.current.isDarkTheme()
|
||||||
else LocalDarkTheme.current.isDarkTheme()
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
@ -210,9 +201,6 @@ fun HomeEntry(
|
|||||||
animatedComposable(route = RouteName.READING_BIONIC_READING) {
|
animatedComposable(route = RouteName.READING_BIONIC_READING) {
|
||||||
BionicReadingPage(navController)
|
BionicReadingPage(navController)
|
||||||
}
|
}
|
||||||
animatedComposable(route = RouteName.READING_DARK_THEME) {
|
|
||||||
ReadingDarkThemePage(navController)
|
|
||||||
}
|
|
||||||
animatedComposable(route = RouteName.READING_PAGE_TITLE) {
|
animatedComposable(route = RouteName.READING_PAGE_TITLE) {
|
||||||
ReadingTitlePage(navController)
|
ReadingTitlePage(navController)
|
||||||
}
|
}
|
||||||
|
@ -46,11 +46,11 @@ class HomeViewModel @Inject constructor(
|
|||||||
private val _filterUiState = MutableStateFlow(FilterState())
|
private val _filterUiState = MutableStateFlow(FilterState())
|
||||||
val filterUiState = _filterUiState.asStateFlow()
|
val filterUiState = _filterUiState.asStateFlow()
|
||||||
|
|
||||||
val syncWorkLiveData = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_NAME)
|
val syncWorkLiveData = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_TAG)
|
||||||
|
|
||||||
fun sync() {
|
fun sync() {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
rssService.get().doSync()
|
rssService.get().doSyncOneTime()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ fun FeedItem(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.clip(if (isEnded()) ShapeBottom32 else RectangleShape)
|
.clip(if (isEnded()) ShapeBottom32 else RectangleShape)
|
||||||
.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
onClick()
|
onClick()
|
||||||
@ -88,9 +88,7 @@ fun FeedItem(
|
|||||||
}
|
}
|
||||||
if ((feed.important ?: 0) != 0) {
|
if ((feed.important ?: 0) != 0) {
|
||||||
Badge(
|
Badge(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
alpha = badgeAlpha
|
|
||||||
),
|
|
||||||
contentColor = MaterialTheme.colorScheme.outline,
|
contentColor = MaterialTheme.colorScheme.outline,
|
||||||
content = {
|
content = {
|
||||||
Text(
|
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.LocalFeedsGroupListExpand
|
||||||
import me.ash.reader.infrastructure.preference.LocalFeedsGroupListTonalElevation
|
import me.ash.reader.infrastructure.preference.LocalFeedsGroupListTonalElevation
|
||||||
import me.ash.reader.infrastructure.preference.LocalFeedsTopBarTonalElevation
|
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.LocalNewVersionNumber
|
||||||
import me.ash.reader.infrastructure.preference.LocalSkipVersionNumber
|
import me.ash.reader.infrastructure.preference.LocalSkipVersionNumber
|
||||||
import me.ash.reader.ui.component.FilterBar
|
import me.ash.reader.ui.component.FilterBar
|
||||||
@ -198,16 +199,21 @@ fun FeedsPage(
|
|||||||
feedsViewModel.fetchAccount()
|
feedsViewModel.fetchAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hideEmptyGroups = LocalHideEmptyGroups.current.value
|
||||||
|
|
||||||
LaunchedEffect(filterUiState, isSyncing) {
|
LaunchedEffect(filterUiState, isSyncing) {
|
||||||
snapshotFlow { filterUiState }.collect {
|
snapshotFlow { filterUiState }.collect {
|
||||||
feedsViewModel.pullFeeds(it)
|
feedsViewModel.pullFeeds(it, hideEmptyGroups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackHandler(true) {
|
||||||
|
context.findActivity()?.moveTaskToBack(false)
|
||||||
|
}
|
||||||
|
|
||||||
RYScaffold(
|
RYScaffold(
|
||||||
topBarTonalElevation = topBarTonalElevation.value.dp,
|
topBarTonalElevation = topBarTonalElevation.value.dp,
|
||||||
containerTonalElevation = groupListTonalElevation.value.dp,
|
// containerTonalElevation = groupListTonalElevation.value.dp,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier.clickable(
|
modifier = Modifier.clickable(
|
||||||
|
@ -49,7 +49,7 @@ class FeedsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun pullFeeds(filterState: FilterState) {
|
fun pullFeeds(filterState: FilterState, hideEmptyGroups: Boolean) {
|
||||||
val isStarred = filterState.filter.isStarred()
|
val isStarred = filterState.filter.isStarred()
|
||||||
val isUnread = filterState.filter.isUnread()
|
val isUnread = filterState.filter.isUnread()
|
||||||
_feedsUiState.update {
|
_feedsUiState.update {
|
||||||
@ -77,7 +77,7 @@ class FeedsViewModel @Inject constructor(
|
|||||||
while (groupIterator.hasNext()) {
|
while (groupIterator.hasNext()) {
|
||||||
val groupWithFeed = groupIterator.next()
|
val groupWithFeed = groupIterator.next()
|
||||||
val groupImportant = importantMap[groupWithFeed.group.id] ?: 0
|
val groupImportant = importantMap[groupWithFeed.group.id] ?: 0
|
||||||
if ((isStarred || isUnread) && groupImportant == 0) {
|
if (hideEmptyGroups && (isStarred || isUnread) && groupImportant == 0) {
|
||||||
groupIterator.remove()
|
groupIterator.remove()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ class FeedsViewModel @Inject constructor(
|
|||||||
val feed = feedIterator.next()
|
val feed = feedIterator.next()
|
||||||
val feedImportant = importantMap[feed.id] ?: 0
|
val feedImportant = importantMap[feed.id] ?: 0
|
||||||
groupWithFeed.group.feeds++
|
groupWithFeed.group.feeds++
|
||||||
if ((isStarred || isUnread) && feedImportant == 0) {
|
if (hideEmptyGroups && (isStarred || isUnread) && feedImportant == 0) {
|
||||||
feedIterator.remove()
|
feedIterator.remove()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ fun GroupItem(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.clip(if (isExpanded() && !roundedBottomCorner()) ShapeTop32 else Shape32)
|
.clip(if (isExpanded() && !roundedBottomCorner()) ShapeTop32 else Shape32)
|
||||||
.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
groupOnClick()
|
groupOnClick()
|
||||||
@ -84,7 +84,7 @@ fun GroupItem(
|
|||||||
.padding(end = 20.dp)
|
.padding(end = 20.dp)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.surfaceTint.copy(alpha = indicatorAlpha))
|
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||||
.clickable { onExpanded() },
|
.clickable { onExpanded() },
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
@ -33,6 +33,8 @@ import me.ash.reader.R
|
|||||||
import me.ash.reader.domain.model.account.Account
|
import me.ash.reader.domain.model.account.Account
|
||||||
import me.ash.reader.ui.component.base.RYDialog
|
import me.ash.reader.ui.component.base.RYDialog
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
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
|
import me.ash.reader.ui.theme.palette.alwaysLight
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@ -75,20 +77,21 @@ fun AccountsTab(
|
|||||||
}
|
}
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
) {
|
) {
|
||||||
|
val selected = account.id == context.currentAccountId
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(52.dp)
|
.size(52.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (account.id == context.currentAccountId) {
|
if (selected) {
|
||||||
MaterialTheme.colorScheme.primaryContainer alwaysLight true
|
LocalFixedColorRoles.current.primaryFixed
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.surfaceDim alwaysLight true
|
MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
AccountTypeIcon(account = account)
|
AccountTypeIcon(account = account, selected = selected)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -125,15 +128,19 @@ fun AccountsTab(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AccountTypeIcon(
|
fun AccountTypeIcon(
|
||||||
account: Account,
|
account: Account,
|
||||||
|
selected: Boolean
|
||||||
) {
|
) {
|
||||||
val icon = account.type.toIcon().takeIf { it is ImageVector }?.let { it as ImageVector }
|
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 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) {
|
if (icon != null) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = account.name,
|
contentDescription = account.name,
|
||||||
tint = MaterialTheme.colorScheme.onSurface alwaysLight true
|
tint = contentColor
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
iconPainter?.let {
|
iconPainter?.let {
|
||||||
@ -141,7 +148,7 @@ fun AccountTypeIcon(
|
|||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
painter = it,
|
painter = it,
|
||||||
contentDescription = account.name,
|
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.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.animation.with
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.CreateNewFolder
|
import androidx.compose.material.icons.outlined.CreateNewFolder
|
||||||
@ -81,7 +80,7 @@ fun SubscribeDialog(
|
|||||||
icon = {
|
icon = {
|
||||||
FeedIcon(
|
FeedIcon(
|
||||||
feedName = subscribeUiState.searchedFeed?.title ?: stringResource(R.string.subscribe),
|
feedName = subscribeUiState.searchedFeed?.title ?: stringResource(R.string.subscribe),
|
||||||
iconUrl = subscribeUiState.searchedFeed?.icon?.url,
|
iconUrl = subscribeUiState.searchedFeed?.getIconUrl(),
|
||||||
placeholderIcon = Icons.Rounded.RssFeed,
|
placeholderIcon = Icons.Rounded.RssFeed,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds.subscribe
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.rometools.rome.feed.synd.SyndFeed
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
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.domain.service.RssService
|
||||||
import me.ash.reader.infrastructure.android.AndroidStringsHelper
|
import me.ash.reader.infrastructure.android.AndroidStringsHelper
|
||||||
import me.ash.reader.infrastructure.di.ApplicationScope
|
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.infrastructure.rss.RssHelper
|
||||||
import me.ash.reader.ui.ext.formatUrl
|
import me.ash.reader.ui.ext.formatUrl
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -59,7 +59,7 @@ class SubscribeViewModel @Inject constructor(
|
|||||||
fun importFromInputStream(inputStream: InputStream) {
|
fun importFromInputStream(inputStream: InputStream) {
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
opmlService.saveToDatabase(inputStream)
|
opmlService.saveToDatabase(inputStream)
|
||||||
rssService.get().doSync()
|
rssService.get().doSyncOneTime()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +245,7 @@ data class SubscribeUiState(
|
|||||||
val errorMessage: String = "",
|
val errorMessage: String = "",
|
||||||
val linkContent: String = "",
|
val linkContent: String = "",
|
||||||
val lockLinkInput: Boolean = false,
|
val lockLinkInput: Boolean = false,
|
||||||
val searchedFeed: SyndFeed? = null,
|
val searchedFeed: FetchedFeed? = null,
|
||||||
val allowNotificationPreset: Boolean = false,
|
val allowNotificationPreset: Boolean = false,
|
||||||
val parseFullContentPreset: Boolean = false,
|
val parseFullContentPreset: Boolean = false,
|
||||||
val selectedGroupId: String = "",
|
val selectedGroupId: String = "",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package me.ash.reader.ui.page.home.flow
|
package me.ash.reader.ui.page.home.flow
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.LargeTopAppBar
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
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.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringResource
|
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 androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.eventFlow
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import androidx.work.WorkInfo
|
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.component.base.RYScaffold
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
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.common.RouteName
|
||||||
import me.ash.reader.ui.page.home.HomeViewModel
|
import me.ash.reader.ui.page.home.HomeViewModel
|
||||||
|
|
||||||
@ -80,10 +93,10 @@ fun FlowPage(
|
|||||||
homeViewModel: HomeViewModel,
|
homeViewModel: HomeViewModel,
|
||||||
) {
|
) {
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
|
|
||||||
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
|
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
|
||||||
val articleListFeedIcon = LocalFlowArticleListFeedIcon.current
|
val articleListFeedIcon = LocalFlowArticleListFeedIcon.current
|
||||||
val articleListDateStickyHeader = LocalFlowArticleListDateStickyHeader.current
|
val articleListDateStickyHeader = LocalFlowArticleListDateStickyHeader.current
|
||||||
|
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
|
||||||
val filterBarStyle = LocalFlowFilterBarStyle.current
|
val filterBarStyle = LocalFlowFilterBarStyle.current
|
||||||
val filterBarFilled = LocalFlowFilterBarFilled.current
|
val filterBarFilled = LocalFlowFilterBarFilled.current
|
||||||
val filterBarPadding = LocalFlowFilterBarPadding.current
|
val filterBarPadding = LocalFlowFilterBarPadding.current
|
||||||
@ -99,6 +112,18 @@ fun FlowPage(
|
|||||||
val listState =
|
val listState =
|
||||||
if (pagingItems.itemCount > 0) flowUiState.listState else rememberLazyListState()
|
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) {
|
if (markAsReadOnScroll) {
|
||||||
LaunchedEffect(listState.isScrollInProgress) {
|
LaunchedEffect(listState.isScrollInProgress) {
|
||||||
if (!listState.isScrollInProgress) {
|
if (!listState.isScrollInProgress) {
|
||||||
@ -127,7 +152,7 @@ fun FlowPage(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
var markAsRead by remember { mutableStateOf(false) }
|
var markAsRead by remember { mutableStateOf(false) }
|
||||||
var onSearch by remember { mutableStateOf(false) }
|
var onSearch by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val owner = LocalLifecycleOwner.current
|
val owner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
@ -148,6 +173,13 @@ fun FlowPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(owner) {
|
DisposableEffect(owner) {
|
||||||
|
scope.launch {
|
||||||
|
owner.lifecycle.eventFlow.collect {
|
||||||
|
if (it == Lifecycle.Event.ON_PAUSE) {
|
||||||
|
flowViewModel.commitDiff()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
|
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
|
||||||
workInfoList.let {
|
workInfoList.let {
|
||||||
isSyncing = it.any { workInfo -> workInfo.state == WorkInfo.State.RUNNING }
|
isSyncing = it.any { workInfo -> workInfo.state == WorkInfo.State.RUNNING }
|
||||||
@ -206,15 +238,10 @@ fun FlowPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(onSearch) {
|
LaunchedEffect(onSearch) {
|
||||||
snapshotFlow { onSearch }.collect {
|
if (!onSearch) {
|
||||||
if (it) {
|
keyboardController?.hide()
|
||||||
delay(100) // ???
|
if (homeUiState.searchContent.isNotBlank()) {
|
||||||
focusRequester.requestFocus()
|
homeViewModel.inputSearchContent("")
|
||||||
} else {
|
|
||||||
keyboardController?.hide()
|
|
||||||
if (homeUiState.searchContent.isNotBlank()) {
|
|
||||||
homeViewModel.inputSearchContent("")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,7 +259,6 @@ fun FlowPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
RYScaffold(
|
RYScaffold(
|
||||||
topBarTonalElevation = topBarTonalElevation.value.dp,
|
|
||||||
containerTonalElevation = articleListTonalElevation.value.dp,
|
containerTonalElevation = articleListTonalElevation.value.dp,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@ -247,7 +273,20 @@ fun FlowPage(
|
|||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
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 = {
|
navigationIcon = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
@ -298,12 +337,14 @@ fun FlowPage(
|
|||||||
flowUiState.listState.animateScrollToItem(0)
|
flowUiState.listState.animateScrollToItem(0)
|
||||||
}
|
}
|
||||||
onSearch = !onSearch
|
onSearch = !onSearch
|
||||||
|
if (onSearch) {
|
||||||
|
delay(100)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, colors = TopAppBarDefaults.topAppBarColors(
|
}, colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
containerColor = topBarContainerColor
|
||||||
topBarTonalElevation.value.dp
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -320,11 +361,7 @@ fun FlowPage(
|
|||||||
item {
|
item {
|
||||||
DisplayText(
|
DisplayText(
|
||||||
modifier = Modifier.padding(start = if (articleListFeedIcon.value) 30.dp else 0.dp),
|
modifier = Modifier.padding(start = if (articleListFeedIcon.value) 30.dp else 0.dp),
|
||||||
text = when {
|
text = titleText,
|
||||||
filterUiState.group != null -> filterUiState.group.name
|
|
||||||
filterUiState.feed != null -> filterUiState.feed.name
|
|
||||||
else -> filterUiState.filter.toName()
|
|
||||||
},
|
|
||||||
desc = "",
|
desc = "",
|
||||||
)
|
)
|
||||||
RYExtensibleVisibility(visible = markAsRead) {
|
RYExtensibleVisibility(visible = markAsRead) {
|
||||||
@ -387,7 +424,6 @@ fun FlowPage(
|
|||||||
articleListTonalElevation = articleListTonalElevation.value,
|
articleListTonalElevation = articleListTonalElevation.value,
|
||||||
isSwipeEnabled = { listState.isScrollInProgress },
|
isSwipeEnabled = { listState.isScrollInProgress },
|
||||||
onClick = {
|
onClick = {
|
||||||
onSearch = false
|
|
||||||
navController.navigate("${RouteName.READING}/${it.article.id}") {
|
navController.navigate("${RouteName.READING}/${it.article.id}") {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ class FlowViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun sync() {
|
fun sync() {
|
||||||
applicationScope.launch(ioDispatcher) {
|
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 android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.VisibilityThreshold
|
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@ -33,15 +30,14 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
|
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.infrastructure.preference.ReadingRendererPreference
|
||||||
import me.ash.reader.ui.component.base.CanBeDisabledIconButton
|
import me.ash.reader.ui.component.base.CanBeDisabledIconButton
|
||||||
import me.ash.reader.ui.component.base.RYExtensibleVisibility
|
|
||||||
import me.ash.reader.ui.component.webview.BionicReadingIcon
|
import me.ash.reader.ui.component.webview.BionicReadingIcon
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -60,6 +56,7 @@ fun BottomBar(
|
|||||||
onReadAloud: () -> Unit = {},
|
onReadAloud: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val tonalElevation = LocalReadingPageTonalElevation.current
|
val tonalElevation = LocalReadingPageTonalElevation.current
|
||||||
|
val isOutlined = tonalElevation == ReadingPageTonalElevationPreference.Outlined
|
||||||
val renderer = LocalReadingRenderer.current
|
val renderer = LocalReadingRenderer.current
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@ -70,16 +67,20 @@ fun BottomBar(
|
|||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isShow,
|
visible = isShow,
|
||||||
enter = expandVertically(),
|
enter = expandVertically(expandFrom = Alignment.Top),
|
||||||
exit = shrinkVertically()
|
exit = shrinkVertically(shrinkTowards = Alignment.Top)
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
Column {
|
Column {
|
||||||
HorizontalDivider(
|
if (isOutlined) {
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
HorizontalDivider(
|
||||||
thickness = 0.5f.dp
|
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
)
|
thickness = 0.5f.dp
|
||||||
Surface() {
|
)
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.run { if (isOutlined) surface else surfaceContainer }
|
||||||
|
) {
|
||||||
// TODO: Component styles await refactoring
|
// TODO: Component styles await refactoring
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -5,9 +5,7 @@ import androidx.compose.animation.AnimatedContent
|
|||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
@ -16,9 +14,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -28,11 +24,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.isSpecified
|
import androidx.compose.ui.unit.isSpecified
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@ -40,7 +34,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
|
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
|
||||||
@ -123,7 +116,7 @@ fun ReadingPage(
|
|||||||
TopBar(
|
TopBar(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
isShow = isShowToolBar,
|
isShow = isShowToolBar,
|
||||||
showDivider = showTopDivider,
|
isScrolled = showTopDivider,
|
||||||
title = readerState.title,
|
title = readerState.title,
|
||||||
link = readerState.link,
|
link = readerState.link,
|
||||||
onClick = { bringToTop = true },
|
onClick = { bringToTop = true },
|
||||||
@ -194,7 +187,7 @@ fun ReadingPage(
|
|||||||
|
|
||||||
|
|
||||||
showTopDivider = snapshotFlow {
|
showTopDivider = snapshotFlow {
|
||||||
scrollState.value != 0 || listState.firstVisibleItemIndex != 0
|
scrollState.value >= 120 || listState.firstVisibleItemIndex != 0
|
||||||
}.collectAsStateValue(initial = false)
|
}.collectAsStateValue(initial = false)
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
@ -22,6 +22,7 @@ import me.ash.reader.infrastructure.di.ApplicationScope
|
|||||||
import me.ash.reader.infrastructure.di.IODispatcher
|
import me.ash.reader.infrastructure.di.IODispatcher
|
||||||
import me.ash.reader.infrastructure.rss.RssHelper
|
import me.ash.reader.infrastructure.rss.RssHelper
|
||||||
import me.ash.reader.infrastructure.storage.AndroidImageDownloader
|
import me.ash.reader.infrastructure.storage.AndroidImageDownloader
|
||||||
|
import me.ash.reader.ui.ext.isNostrUri
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -47,6 +48,8 @@ class ReadingViewModel @Inject constructor(
|
|||||||
private val currentFeed: Feed?
|
private val currentFeed: Feed?
|
||||||
get() = readingUiState.value.articleWithFeed?.feed
|
get() = readingUiState.value.articleWithFeed?.feed
|
||||||
|
|
||||||
|
private var initialArticleItems: List<ArticleFlowItem> = emptyList()
|
||||||
|
|
||||||
fun initData(articleId: String) {
|
fun initData(articleId: String) {
|
||||||
setLoading()
|
setLoading()
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch(ioDispatcher) {
|
||||||
@ -96,10 +99,14 @@ class ReadingViewModel @Inject constructor(
|
|||||||
private suspend fun internalRenderFullContent() {
|
private suspend fun internalRenderFullContent() {
|
||||||
setLoading()
|
setLoading()
|
||||||
runCatching {
|
runCatching {
|
||||||
rssHelper.parseFullContent(
|
if (currentArticle?.link?.isNostrUri() == true) {
|
||||||
currentArticle?.link ?: "",
|
currentArticle?.fullContent.toString()
|
||||||
currentArticle?.title ?: ""
|
} else {
|
||||||
)
|
rssHelper.parseFullContent(
|
||||||
|
currentArticle?.link ?: "",
|
||||||
|
currentArticle?.title ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
}.onSuccess { content ->
|
}.onSuccess { content ->
|
||||||
_readerState.update { it.copy(content = ReaderState.FullContent(content = content)) }
|
_readerState.update { it.copy(content = ReaderState.FullContent(content = content)) }
|
||||||
}.onFailure { th ->
|
}.onFailure { th ->
|
||||||
@ -146,7 +153,11 @@ class ReadingViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun prefetchArticleId(pagingItems: ItemSnapshotList<ArticleFlowItem>) {
|
fun prefetchArticleId(pagingItems: ItemSnapshotList<ArticleFlowItem>) {
|
||||||
val items = pagingItems.items
|
if (initialArticleItems.isEmpty()) {
|
||||||
|
initialArticleItems = pagingItems.items
|
||||||
|
}
|
||||||
|
|
||||||
|
val items = initialArticleItems
|
||||||
val currentId = currentArticle?.id
|
val currentId = currentArticle?.id
|
||||||
val index = items.indexOfFirst { item ->
|
val index = items.indexOfFirst { item ->
|
||||||
item is ArticleFlowItem.Article && item.articleWithFeed.article.id == currentId
|
item is ArticleFlowItem.Article && item.articleWithFeed.article.id == currentId
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package me.ash.reader.ui.page.home.reading
|
package me.ash.reader.ui.page.home.reading
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
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.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.clickable
|
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.height
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Palette
|
import androidx.compose.material.icons.outlined.Palette
|
||||||
import androidx.compose.material.icons.outlined.Share
|
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.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||||
import me.ash.reader.infrastructure.preference.LocalSharedContent
|
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.component.base.FeedbackIconButton
|
||||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
|
||||||
import me.ash.reader.ui.page.common.RouteName
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@ -48,7 +52,7 @@ import me.ash.reader.ui.page.common.RouteName
|
|||||||
fun TopBar(
|
fun TopBar(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
isShow: Boolean,
|
isShow: Boolean,
|
||||||
showDivider: Boolean = false,
|
isScrolled: Boolean = false,
|
||||||
title: String? = "",
|
title: String? = "",
|
||||||
link: String? = "",
|
link: String? = "",
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
@ -56,6 +60,13 @@ fun TopBar(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedContent = LocalSharedContent.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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -63,29 +74,30 @@ fun TopBar(
|
|||||||
.zIndex(1f),
|
.zIndex(1f),
|
||||||
contentAlignment = Alignment.TopCenter
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
Column(modifier = if (onClick == null) Modifier else Modifier.clickable(
|
Column(
|
||||||
onClick = onClick,
|
modifier = Modifier.drawBehind { drawRect(containerColor) }
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Surface(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(
|
.height(
|
||||||
WindowInsets.statusBars
|
WindowInsets.statusBars
|
||||||
.asPaddingValues()
|
.asPaddingValues()
|
||||||
.calculateTopPadding()
|
.calculateTopPadding()
|
||||||
)
|
),
|
||||||
) {}
|
)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isShow,
|
visible = isShow,
|
||||||
enter = expandVertically(expandFrom = Alignment.Top),
|
enter = expandVertically(expandFrom = Alignment.Bottom),
|
||||||
exit = shrinkVertically(shrinkTowards = Alignment.Top)
|
exit = shrinkVertically(shrinkTowards = Alignment.Bottom)
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {},
|
title = {},
|
||||||
modifier = Modifier,
|
modifier = if (onClick == null) Modifier else Modifier.clickable(
|
||||||
|
onClick = onClick,
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
),
|
||||||
windowInsets = WindowInsets(0.dp),
|
windowInsets = WindowInsets(0.dp),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
@ -95,7 +107,8 @@ fun TopBar(
|
|||||||
) {
|
) {
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
}, actions = {
|
},
|
||||||
|
actions = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
modifier = Modifier.size(22.dp),
|
modifier = Modifier.size(22.dp),
|
||||||
imageVector = Icons.Outlined.Palette,
|
imageVector = Icons.Outlined.Palette,
|
||||||
@ -114,10 +127,11 @@ fun TopBar(
|
|||||||
) {
|
) {
|
||||||
sharedContent.share(context, title, link)
|
sharedContent.share(context, title, link)
|
||||||
}
|
}
|
||||||
}, colors = TopAppBarDefaults.topAppBarColors()
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showDivider) {
|
if (isOutlined && isScrolled) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
thickness = 0.5f.dp
|
thickness = 0.5f.dp
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package me.ash.reader.ui.page.settings.accounts.addition
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@ -55,6 +57,7 @@ fun AddFeverAccountDialog(
|
|||||||
var feverServerUrl by rememberSaveable { mutableStateOf("") }
|
var feverServerUrl by rememberSaveable { mutableStateOf("") }
|
||||||
var feverUsername by rememberSaveable { mutableStateOf("") }
|
var feverUsername by rememberSaveable { mutableStateOf("") }
|
||||||
var feverPassword by rememberSaveable { mutableStateOf("") }
|
var feverPassword by rememberSaveable { mutableStateOf("") }
|
||||||
|
var feverClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
RYDialog(
|
RYDialog(
|
||||||
modifier = Modifier.padding(horizontal = 44.dp),
|
modifier = Modifier.padding(horizontal = 44.dp),
|
||||||
@ -121,6 +124,19 @@ fun AddFeverAccountDialog(
|
|||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
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 = {
|
confirmButton = {
|
||||||
@ -138,6 +154,7 @@ fun AddFeverAccountDialog(
|
|||||||
serverUrl = feverServerUrl,
|
serverUrl = feverServerUrl,
|
||||||
username = feverUsername,
|
username = feverUsername,
|
||||||
password = feverPassword,
|
password = feverPassword,
|
||||||
|
clientCertificateAlias = feverClientCertificateAlias,
|
||||||
).toString(),
|
).toString(),
|
||||||
)) { account, exception ->
|
)) { account, exception ->
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package me.ash.reader.ui.page.settings.accounts.addition
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@ -55,6 +57,7 @@ fun AddFreshRSSAccountDialog(
|
|||||||
var freshRSSServerUrl by rememberSaveable { mutableStateOf("") }
|
var freshRSSServerUrl by rememberSaveable { mutableStateOf("") }
|
||||||
var freshRSSUsername by rememberSaveable { mutableStateOf("") }
|
var freshRSSUsername by rememberSaveable { mutableStateOf("") }
|
||||||
var freshRSSPassword by rememberSaveable { mutableStateOf("") }
|
var freshRSSPassword by rememberSaveable { mutableStateOf("") }
|
||||||
|
var freshRSSClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
RYDialog(
|
RYDialog(
|
||||||
modifier = Modifier.padding(horizontal = 44.dp),
|
modifier = Modifier.padding(horizontal = 44.dp),
|
||||||
@ -122,6 +125,19 @@ fun AddFreshRSSAccountDialog(
|
|||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
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 = {
|
confirmButton = {
|
||||||
@ -142,6 +158,7 @@ fun AddFreshRSSAccountDialog(
|
|||||||
serverUrl = freshRSSServerUrl,
|
serverUrl = freshRSSServerUrl,
|
||||||
username = freshRSSUsername,
|
username = freshRSSUsername,
|
||||||
password = freshRSSPassword,
|
password = freshRSSPassword,
|
||||||
|
clientCertificateAlias = freshRSSClientCertificateAlias,
|
||||||
).toString(),
|
).toString(),
|
||||||
)) { account, exception ->
|
)) { account, exception ->
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package me.ash.reader.ui.page.settings.accounts.addition
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@ -56,6 +58,7 @@ fun AddGoogleReaderAccountDialog(
|
|||||||
var googleReaderServerUrl by rememberSaveable { mutableStateOf("") }
|
var googleReaderServerUrl by rememberSaveable { mutableStateOf("") }
|
||||||
var googleReaderUsername by rememberSaveable { mutableStateOf("") }
|
var googleReaderUsername by rememberSaveable { mutableStateOf("") }
|
||||||
var googleReaderPassword by rememberSaveable { mutableStateOf("") }
|
var googleReaderPassword by rememberSaveable { mutableStateOf("") }
|
||||||
|
var googleReaderClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
RYDialog(
|
RYDialog(
|
||||||
modifier = Modifier.padding(horizontal = 44.dp),
|
modifier = Modifier.padding(horizontal = 44.dp),
|
||||||
@ -123,6 +126,19 @@ fun AddGoogleReaderAccountDialog(
|
|||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
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 = {
|
confirmButton = {
|
||||||
@ -143,6 +159,7 @@ fun AddGoogleReaderAccountDialog(
|
|||||||
serverUrl = googleReaderServerUrl,
|
serverUrl = googleReaderServerUrl,
|
||||||
username = googleReaderUsername,
|
username = googleReaderUsername,
|
||||||
password = googleReaderPassword,
|
password = googleReaderPassword,
|
||||||
|
clientCertificateAlias = googleReaderClientCertificateAlias.takeIf { it.isNotEmpty() },
|
||||||
).toString(),
|
).toString(),
|
||||||
)) { account, exception ->
|
)) { account, exception ->
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
package me.ash.reader.ui.page.settings.accounts.connection
|
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.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.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
@ -17,6 +26,8 @@ fun LazyItemScope.FeverConnection(
|
|||||||
account: Account,
|
account: Account,
|
||||||
viewModel: AccountViewModel = hiltViewModel(),
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val securityKey by remember {
|
val securityKey by remember {
|
||||||
derivedStateOf { FeverSecurityKey(account.securityKey) }
|
derivedStateOf { FeverSecurityKey(account.securityKey) }
|
||||||
}
|
}
|
||||||
@ -56,6 +67,16 @@ fun LazyItemScope.FeverConnection(
|
|||||||
passwordDialogVisible = true
|
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(
|
TextFieldDialog(
|
||||||
visible = serverUrlDialogVisible,
|
visible = serverUrlDialogVisible,
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
package me.ash.reader.ui.page.settings.accounts.connection
|
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.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.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
@ -17,6 +26,8 @@ fun LazyItemScope.FreshRSSConnection(
|
|||||||
account: Account,
|
account: Account,
|
||||||
viewModel: AccountViewModel = hiltViewModel(),
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val securityKey by remember {
|
val securityKey by remember {
|
||||||
derivedStateOf { FreshRSSSecurityKey(account.securityKey) }
|
derivedStateOf { FreshRSSSecurityKey(account.securityKey) }
|
||||||
}
|
}
|
||||||
@ -56,6 +67,16 @@ fun LazyItemScope.FreshRSSConnection(
|
|||||||
passwordDialogVisible = true
|
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(
|
TextFieldDialog(
|
||||||
visible = serverUrlDialogVisible,
|
visible = serverUrlDialogVisible,
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
package me.ash.reader.ui.page.settings.accounts.connection
|
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.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.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
@ -17,6 +26,8 @@ fun LazyItemScope.GoogleReaderConnection(
|
|||||||
account: Account,
|
account: Account,
|
||||||
viewModel: AccountViewModel = hiltViewModel(),
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val securityKey by remember {
|
val securityKey by remember {
|
||||||
derivedStateOf { GoogleReaderSecurityKey(account.securityKey) }
|
derivedStateOf { GoogleReaderSecurityKey(account.securityKey) }
|
||||||
}
|
}
|
||||||
@ -56,6 +67,16 @@ fun LazyItemScope.GoogleReaderConnection(
|
|||||||
passwordDialogVisible = true
|
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(
|
TextFieldDialog(
|
||||||
visible = serverUrlDialogVisible,
|
visible = serverUrlDialogVisible,
|
||||||
|
@ -62,9 +62,7 @@ fun FeedsPagePreview(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateContentSize()
|
.animateContentSize()
|
||||||
.background(
|
.background(
|
||||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
color = MaterialTheme.colorScheme.surface,
|
||||||
groupListTonalElevation.value.dp
|
|
||||||
) onDark MaterialTheme.colorScheme.surface,
|
|
||||||
shape = RoundedCornerShape(24.dp)
|
shape = RoundedCornerShape(24.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -123,7 +123,7 @@ fun FeedsPageStylePage(
|
|||||||
(!groupListExpand).put(context, scope)
|
(!groupListExpand).put(context, scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingItem(
|
/* SettingItem(
|
||||||
title = stringResource(R.string.tonal_elevation),
|
title = stringResource(R.string.tonal_elevation),
|
||||||
desc = "${groupListTonalElevation.value}dp",
|
desc = "${groupListTonalElevation.value}dp",
|
||||||
onClick = {
|
onClick = {
|
||||||
@ -131,7 +131,7 @@ fun FeedsPageStylePage(
|
|||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
Tips(text = stringResource(R.string.tips_group_list_tonal_elevation))
|
Tips(text = stringResource(R.string.tips_group_list_tonal_elevation))
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter Bar
|
// Filter Bar
|
||||||
@ -242,7 +242,7 @@ fun FeedsPageStylePage(
|
|||||||
topBarTonalElevationDialogVisible = false
|
topBarTonalElevationDialogVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioDialog(
|
/* RadioDialog(
|
||||||
visible = groupListTonalElevationDialogVisible,
|
visible = groupListTonalElevationDialogVisible,
|
||||||
title = stringResource(R.string.tonal_elevation),
|
title = stringResource(R.string.tonal_elevation),
|
||||||
options = FeedsGroupListTonalElevationPreference.values.map {
|
options = FeedsGroupListTonalElevationPreference.values.map {
|
||||||
@ -255,5 +255,5 @@ fun FeedsPageStylePage(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
groupListTonalElevationDialogVisible = false
|
groupListTonalElevationDialogVisible = false
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,16 @@ import androidx.compose.material.icons.rounded.DoneAll
|
|||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
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.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.domain.model.article.Article
|
import me.ash.reader.domain.model.article.Article
|
||||||
import me.ash.reader.domain.model.article.ArticleWithFeed
|
import me.ash.reader.domain.model.article.ArticleWithFeed
|
||||||
@ -55,8 +58,19 @@ fun FlowPagePreview(
|
|||||||
shape = RoundedCornerShape(24.dp)
|
shape = RoundedCornerShape(24.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
val preview = generateArticleWithFeedPreview()
|
||||||
|
val feed = preview.feed
|
||||||
|
val article = preview.article
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {},
|
title = {
|
||||||
|
Text(
|
||||||
|
text = feed.name,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
|
||||||
|
)
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
@ -83,10 +97,6 @@ fun FlowPagePreview(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
val preview = generateArticleWithFeedPreview()
|
|
||||||
val feed = preview.feed
|
|
||||||
val article = preview.article
|
|
||||||
|
|
||||||
ArticleItem(
|
ArticleItem(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
feedName = feed.name,
|
feedName = feed.name,
|
||||||
|
@ -115,7 +115,7 @@ fun FlowPageStylePage(
|
|||||||
topBarTonalElevationDialogVisible = true
|
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))
|
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.LocalPullToSwitchArticle
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar
|
import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
|
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.LocalReadingFonts
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||||
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
|
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
|
||||||
@ -76,8 +75,6 @@ fun ReadingStylePage(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val readingTheme = LocalReadingTheme.current
|
val readingTheme = LocalReadingTheme.current
|
||||||
val darkTheme = LocalReadingDarkTheme.current
|
|
||||||
val darkThemeNot = !darkTheme
|
|
||||||
val tonalElevation = LocalReadingPageTonalElevation.current
|
val tonalElevation = LocalReadingPageTonalElevation.current
|
||||||
val fonts = LocalReadingFonts.current
|
val fonts = LocalReadingFonts.current
|
||||||
val autoHideToolbar = LocalReadingAutoHideToolbar.current
|
val autoHideToolbar = LocalReadingAutoHideToolbar.current
|
||||||
@ -89,12 +86,17 @@ fun ReadingStylePage(
|
|||||||
var rendererDialogVisible by remember { mutableStateOf(false) }
|
var rendererDialogVisible by remember { mutableStateOf(false) }
|
||||||
var fontsDialogVisible by remember { mutableStateOf(false) }
|
var fontsDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
val launcher =
|
||||||
uri?.let {
|
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||||
ExternalFonts(context, it, ExternalFonts.FontType.ReadingFont).copyToInternalStorage()
|
uri?.let {
|
||||||
ReadingFontsPreference.External.put(context, scope)
|
ExternalFonts(
|
||||||
} ?: context.showToast("Cannot get activity result with launcher")
|
context,
|
||||||
}
|
it,
|
||||||
|
ExternalFonts.FontType.ReadingFont
|
||||||
|
).copyToInternalStorage()
|
||||||
|
ReadingFontsPreference.External.put(context, scope)
|
||||||
|
} ?: context.showToast("Cannot get activity result with launcher")
|
||||||
|
}
|
||||||
|
|
||||||
RYScaffold(
|
RYScaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
@ -115,7 +117,8 @@ fun ReadingStylePage(
|
|||||||
|
|
||||||
// Preview
|
// Preview
|
||||||
item {
|
item {
|
||||||
Row(modifier = Modifier.horizontalScroll(rememberScrollState())
|
Row(
|
||||||
|
modifier = Modifier.horizontalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(24.dp))
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
ReadingThemePreference.values.map {
|
ReadingThemePreference.values.map {
|
||||||
@ -187,22 +190,6 @@ fun ReadingStylePage(
|
|||||||
desc = fonts.toDesc(context),
|
desc = fonts.toDesc(context),
|
||||||
onClick = { fontsDialogVisible = true },
|
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(
|
SettingItem(
|
||||||
title = stringResource(R.string.auto_hide_toolbars),
|
title = stringResource(R.string.auto_hide_toolbars),
|
||||||
onClick = {
|
onClick = {
|
||||||
@ -224,6 +211,10 @@ fun ReadingStylePage(
|
|||||||
onClick = { pullToSwitchArticle.toggle(context, scope) }) {
|
onClick = { pullToSwitchArticle.toggle(context, scope) }) {
|
||||||
RYSwitch(activated = pullToSwitchArticle.value)
|
RYSwitch(activated = pullToSwitchArticle.value)
|
||||||
}
|
}
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.toolbars)
|
||||||
|
)
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.tonal_elevation),
|
title = stringResource(R.string.tonal_elevation),
|
||||||
desc = "${tonalElevation.value}dp",
|
desc = "${tonalElevation.value}dp",
|
||||||
@ -231,6 +222,7 @@ fun ReadingStylePage(
|
|||||||
tonalElevationDialogVisible = true
|
tonalElevationDialogVisible = true
|
||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +283,7 @@ fun ReadingStylePage(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/* RadioDialog(
|
RadioDialog(
|
||||||
visible = tonalElevationDialogVisible,
|
visible = tonalElevationDialogVisible,
|
||||||
title = stringResource(R.string.tonal_elevation),
|
title = stringResource(R.string.tonal_elevation),
|
||||||
options = ReadingPageTonalElevationPreference.values.map {
|
options = ReadingPageTonalElevationPreference.values.map {
|
||||||
@ -304,7 +296,7 @@ fun ReadingStylePage(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
tonalElevationDialogVisible = false
|
tonalElevationDialogVisible = false
|
||||||
}*/
|
}
|
||||||
|
|
||||||
RadioDialog(
|
RadioDialog(
|
||||||
visible = rendererDialogVisible,
|
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.InitialPagePreference
|
||||||
import me.ash.reader.infrastructure.preference.LocalArticleListSwipeEndAction
|
import me.ash.reader.infrastructure.preference.LocalArticleListSwipeEndAction
|
||||||
import me.ash.reader.infrastructure.preference.LocalArticleListSwipeStartAction
|
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.LocalInitialFilter
|
||||||
import me.ash.reader.infrastructure.preference.LocalInitialPage
|
import me.ash.reader.infrastructure.preference.LocalInitialPage
|
||||||
import me.ash.reader.infrastructure.preference.LocalMarkAsReadOnScroll
|
import me.ash.reader.infrastructure.preference.LocalMarkAsReadOnScroll
|
||||||
@ -58,6 +59,7 @@ fun InteractionPage(
|
|||||||
val swipeToStartAction = LocalArticleListSwipeStartAction.current
|
val swipeToStartAction = LocalArticleListSwipeStartAction.current
|
||||||
val swipeToEndAction = LocalArticleListSwipeEndAction.current
|
val swipeToEndAction = LocalArticleListSwipeEndAction.current
|
||||||
val markAsReadOnScroll = LocalMarkAsReadOnScroll.current
|
val markAsReadOnScroll = LocalMarkAsReadOnScroll.current
|
||||||
|
val hideEmptyGroups = LocalHideEmptyGroups.current
|
||||||
val pullToSwitchArticle = LocalPullToSwitchArticle.current
|
val pullToSwitchArticle = LocalPullToSwitchArticle.current
|
||||||
val openLink = LocalOpenLink.current
|
val openLink = LocalOpenLink.current
|
||||||
val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current
|
val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current
|
||||||
@ -112,6 +114,22 @@ fun InteractionPage(
|
|||||||
) {}
|
) {}
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
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(
|
Subtitle(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
text = stringResource(R.string.article_list),
|
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,
|
targetValue = pressAMP,
|
||||||
animationSpec = tween()
|
animationSpec = tween()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var showSponsorDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
currentVersion = context.getCurrentVersion().toString()
|
currentVersion = context.getCurrentVersion().toString()
|
||||||
@ -210,7 +212,7 @@ fun TipsAndSupportPage(
|
|||||||
) {
|
) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
view.playSoundEffect(SoundEffectConstants.CLICK)
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
context.showToast(context.getString(R.string.coming_soon))
|
showSponsorDialog = true
|
||||||
})
|
})
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
@ -250,6 +252,9 @@ fun TipsAndSupportPage(
|
|||||||
)
|
)
|
||||||
|
|
||||||
UpdateDialog()
|
UpdateDialog()
|
||||||
|
if (showSponsorDialog) {
|
||||||
|
SponsorDialog { showSponsorDialog = false }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@ -11,6 +11,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import me.ash.reader.infrastructure.preference.LocalBasicFonts
|
import me.ash.reader.infrastructure.preference.LocalBasicFonts
|
||||||
import me.ash.reader.infrastructure.preference.LocalThemeIndex
|
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.LocalTonalPalettes
|
||||||
import me.ash.reader.ui.theme.palette.TonalPalettes
|
import me.ash.reader.ui.theme.palette.TonalPalettes
|
||||||
import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
|
import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
|
||||||
@ -59,15 +61,21 @@ fun AppTheme(
|
|||||||
LocalTonalPalettes provides tonalPalettes.apply { Preparing() },
|
LocalTonalPalettes provides tonalPalettes.apply { Preparing() },
|
||||||
LocalTextStyle provides LocalTextStyle.current.applyTextDirection()
|
LocalTextStyle provides LocalTextStyle.current.applyTextDirection()
|
||||||
) {
|
) {
|
||||||
MaterialTheme(
|
val lightColors = dynamicLightColorScheme()
|
||||||
colorScheme =
|
val darkColors = dynamicDarkColorScheme()
|
||||||
if (useDarkTheme) dynamicDarkColorScheme()
|
CompositionLocalProvider(
|
||||||
else dynamicLightColorScheme(),
|
LocalFixedColorRoles provides FixedColorRoles.fromColorSchemes(
|
||||||
typography = LocalBasicFonts.current.asTypography(LocalContext.current)
|
lightColors, darkColors
|
||||||
.applyTextDirection(),
|
)
|
||||||
shapes = Shapes,
|
) {
|
||||||
content = content,
|
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
|
@Composable
|
||||||
fun dynamicDarkColorScheme(): ColorScheme {
|
fun dynamicDarkColorScheme(): ColorScheme {
|
||||||
val palettes = LocalTonalPalettes.current
|
val palettes = LocalTonalPalettes.current
|
||||||
val amoledDarkTheme = LocalAmoledDarkTheme.current
|
val useAmoledDarkTheme = LocalAmoledDarkTheme.current.value
|
||||||
|
|
||||||
return darkColorScheme(
|
return darkColorScheme(
|
||||||
primary = palettes primary 80,
|
primary = palettes primary 80,
|
||||||
@ -70,7 +70,7 @@ fun dynamicDarkColorScheme(): ColorScheme {
|
|||||||
onTertiaryContainer = palettes tertiary 90,
|
onTertiaryContainer = palettes tertiary 90,
|
||||||
background = palettes neutral 10,
|
background = palettes neutral 10,
|
||||||
onBackground = palettes neutral 90,
|
onBackground = palettes neutral 90,
|
||||||
surface = palettes neutral if (amoledDarkTheme.value) 0 else 10,
|
surface = palettes neutral 6,
|
||||||
onSurface = palettes neutral 90,
|
onSurface = palettes neutral 90,
|
||||||
surfaceVariant = palettes neutralVariant 30,
|
surfaceVariant = palettes neutralVariant 30,
|
||||||
onSurfaceVariant = palettes neutralVariant 80,
|
onSurfaceVariant = palettes neutralVariant 80,
|
||||||
@ -86,7 +86,16 @@ fun dynamicDarkColorScheme(): ColorScheme {
|
|||||||
surfaceContainer = palettes neutral 12,
|
surfaceContainer = palettes neutral 12,
|
||||||
surfaceContainerHigh = palettes neutral 17,
|
surfaceContainerHigh = palettes neutral 17,
|
||||||
surfaceContainerHighest = palettes neutral 22,
|
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
|
@Composable
|
||||||
@ -111,7 +120,6 @@ infix fun Color.alwaysLight(isAlways: Boolean): Color {
|
|||||||
colorScheme.error -> colorScheme.onError
|
colorScheme.error -> colorScheme.onError
|
||||||
colorScheme.surface -> colorScheme.onSurface
|
colorScheme.surface -> colorScheme.onSurface
|
||||||
colorScheme.surfaceVariant -> colorScheme.onSurfaceVariant
|
colorScheme.surfaceVariant -> colorScheme.onSurfaceVariant
|
||||||
colorScheme.error -> colorScheme.onError
|
|
||||||
colorScheme.primaryContainer -> colorScheme.onPrimaryContainer
|
colorScheme.primaryContainer -> colorScheme.onPrimaryContainer
|
||||||
colorScheme.secondaryContainer -> colorScheme.onSecondaryContainer
|
colorScheme.secondaryContainer -> colorScheme.onSecondaryContainer
|
||||||
colorScheme.tertiaryContainer -> colorScheme.onTertiaryContainer
|
colorScheme.tertiaryContainer -> colorScheme.onTertiaryContainer
|
||||||
@ -125,7 +133,6 @@ infix fun Color.alwaysLight(isAlways: Boolean): Color {
|
|||||||
colorScheme.onError -> colorScheme.error
|
colorScheme.onError -> colorScheme.error
|
||||||
colorScheme.onSurface -> colorScheme.surface
|
colorScheme.onSurface -> colorScheme.surface
|
||||||
colorScheme.onSurfaceVariant -> colorScheme.surfaceVariant
|
colorScheme.onSurfaceVariant -> colorScheme.surfaceVariant
|
||||||
colorScheme.onError -> colorScheme.error
|
|
||||||
colorScheme.onPrimaryContainer -> colorScheme.primaryContainer
|
colorScheme.onPrimaryContainer -> colorScheme.primaryContainer
|
||||||
colorScheme.onSecondaryContainer -> colorScheme.secondaryContainer
|
colorScheme.onSecondaryContainer -> colorScheme.secondaryContainer
|
||||||
colorScheme.onTertiaryContainer -> colorScheme.tertiaryContainer
|
colorScheme.onTertiaryContainer -> colorScheme.tertiaryContainer
|
||||||
@ -153,7 +160,6 @@ infix fun Color.alwaysDark(isAlways: Boolean): Color {
|
|||||||
colorScheme.error -> colorScheme.onError
|
colorScheme.error -> colorScheme.onError
|
||||||
colorScheme.surface -> colorScheme.onSurface
|
colorScheme.surface -> colorScheme.onSurface
|
||||||
colorScheme.surfaceVariant -> colorScheme.onSurfaceVariant
|
colorScheme.surfaceVariant -> colorScheme.onSurfaceVariant
|
||||||
colorScheme.error -> colorScheme.onError
|
|
||||||
colorScheme.primaryContainer -> colorScheme.onPrimaryContainer
|
colorScheme.primaryContainer -> colorScheme.onPrimaryContainer
|
||||||
colorScheme.secondaryContainer -> colorScheme.onSecondaryContainer
|
colorScheme.secondaryContainer -> colorScheme.onSecondaryContainer
|
||||||
colorScheme.tertiaryContainer -> colorScheme.onTertiaryContainer
|
colorScheme.tertiaryContainer -> colorScheme.onTertiaryContainer
|
||||||
@ -167,7 +173,6 @@ infix fun Color.alwaysDark(isAlways: Boolean): Color {
|
|||||||
colorScheme.onError -> colorScheme.error
|
colorScheme.onError -> colorScheme.error
|
||||||
colorScheme.onSurface -> colorScheme.surface
|
colorScheme.onSurface -> colorScheme.surface
|
||||||
colorScheme.onSurfaceVariant -> colorScheme.surfaceVariant
|
colorScheme.onSurfaceVariant -> colorScheme.surfaceVariant
|
||||||
colorScheme.onError -> colorScheme.error
|
|
||||||
colorScheme.onPrimaryContainer -> colorScheme.primaryContainer
|
colorScheme.onPrimaryContainer -> colorScheme.primaryContainer
|
||||||
colorScheme.onSecondaryContainer -> colorScheme.secondaryContainer
|
colorScheme.onSecondaryContainer -> colorScheme.secondaryContainer
|
||||||
colorScheme.onTertiaryContainer -> colorScheme.tertiaryContainer
|
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) {
|
infix fun neutral(tone: TonalValue): Color = neutral.getOrPut(tone) {
|
||||||
zcamLch(
|
zcamLch(
|
||||||
L = tone.toZcamLightness(),
|
L = tone.toZcamLightness(),
|
||||||
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 12.0,
|
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 8.0,
|
||||||
h = hue,
|
h = hue,
|
||||||
).clampToRgb().toColor()
|
).clampToRgb().toColor()
|
||||||
}
|
}
|
||||||
|
@ -314,4 +314,8 @@
|
|||||||
<string name="browse_bionic_reading_tips">قرا عن القراية البايونيك ع <i><u>bionic-reading.com</u></i>.</string>
|
<string name="browse_bionic_reading_tips">قرا عن القراية البايونيك ع <i><u>bionic-reading.com</u></i>.</string>
|
||||||
<string name="use_bionic_reading">ستعمل القراية البايونيك</string>
|
<string name="use_bionic_reading">ستعمل القراية البايونيك</string>
|
||||||
<string name="bionic_reading_tips">شو القراية البايونيك؟</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>
|
</resources>
|
@ -327,4 +327,7 @@
|
|||||||
<string name="bionic_reading_tips">ما هي القراءة الحيوية؟?</string>
|
<string name="bionic_reading_tips">ما هي القراءة الحيوية؟?</string>
|
||||||
<string name="browse_bionic_reading_tips">تعرف على المزيد على <i><u>bionic-reading.com</u></i>.</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="mark_as_read_on_scroll">وضع علامة مقروء على التمرير</string>
|
||||||
|
<string name="become_a_sponsor">كن داعما</string>
|
||||||
|
<string name="sponsor_desc">نحن نبني ونحافظ على هذا التطبيق المجاني والمفتوح المصدر في غير ساعات العمل لدينا. إذا كنت تستمتع به، فيرجى التفكير في دعمنا بتبرع صغير! ☕️</string>
|
||||||
|
<string name="toolbars">شريط الأدوات</string>
|
||||||
</resources>
|
</resources>
|
@ -312,4 +312,8 @@
|
|||||||
<string name="only_available_on_webview">Предлага се само в WebView</string>
|
<string name="only_available_on_webview">Предлага се само в WebView</string>
|
||||||
<string name="native_component">Собствен компонент</string>
|
<string name="native_component">Собствен компонент</string>
|
||||||
<string name="use_bionic_reading">Използвай на бионично четене</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>
|
</resources>
|
@ -317,4 +317,8 @@
|
|||||||
<string name="about">Informace</string>
|
<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="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="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>
|
</resources>
|
@ -314,4 +314,8 @@
|
|||||||
<string name="about">Über</string>
|
<string name="about">Über</string>
|
||||||
<string name="bionic_reading_tips">Was ist bionisches Lesen?</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="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>
|
</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="use_bionic_reading">Utiliza la lectura biónica</string>
|
||||||
<string name="about">Acerca de</string>
|
<string name="about">Acerca de</string>
|
||||||
<string name="mark_as_read_on_scroll">Marcar como leído al desplazarse</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>
|
</resources>
|
@ -26,4 +26,145 @@
|
|||||||
<string name="confirm">Kinnita</string>
|
<string name="confirm">Kinnita</string>
|
||||||
<string name="cancel">Katkesta</string>
|
<string name="cancel">Katkesta</string>
|
||||||
<string name="allow">Luba</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>
|
</resources>
|
@ -315,4 +315,8 @@
|
|||||||
<string name="only_available_on_webview">Hanya tersedia di WebView</string>
|
<string name="only_available_on_webview">Hanya tersedia di WebView</string>
|
||||||
<string name="use_bionic_reading">Gunakan Bionic Reading</string>
|
<string name="use_bionic_reading">Gunakan Bionic Reading</string>
|
||||||
<string name="bionic_reading_tips">Apa itu 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>
|
</resources>
|
@ -318,4 +318,7 @@
|
|||||||
<string name="native_component">Componente nativo</string>
|
<string name="native_component">Componente nativo</string>
|
||||||
<string name="only_available_on_webview">Disponibile esclusivamente in WebView</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="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>
|
</resources>
|
@ -216,4 +216,30 @@
|
|||||||
<string name="always_ask">תמיד לשאול</string>
|
<string name="always_ask">תמיד לשאול</string>
|
||||||
<string name="unfold_more">הרחבה של הכול</string>
|
<string name="unfold_more">הרחבה של הכול</string>
|
||||||
<string name="unfold_less">צמצום של הכול</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>
|
</resources>
|
@ -116,4 +116,10 @@
|
|||||||
<string name="dark_theme">ഡാർക്ക് തീം</string>
|
<string name="dark_theme">ഡാർക്ക് തീം</string>
|
||||||
<string name="use_device_theme">ഉപകരണത്തിന്റെ തീം ഉപയോഗിക്കുക</string>
|
<string name="use_device_theme">ഉപകരണത്തിന്റെ തീം ഉപയോഗിക്കുക</string>
|
||||||
<string name="skip_this_version">ഈ പതിപ്പ് ഒഴിവാക്കുക</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>
|
</resources>
|
@ -30,7 +30,7 @@
|
|||||||
<string name="defaults">Domyślne</string>
|
<string name="defaults">Domyślne</string>
|
||||||
<string name="unknown">Nieznane</string>
|
<string name="unknown">Nieznane</string>
|
||||||
<string name="back">Wstecz</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="settings">Ustawienia</string>
|
||||||
<string name="refresh">Odśwież</string>
|
<string name="refresh">Odśwież</string>
|
||||||
<string name="search">Wyszukaj</string>
|
<string name="search">Wyszukaj</string>
|
||||||
@ -72,7 +72,7 @@
|
|||||||
<string name="unsubscribe">Odsubskrybuj</string>
|
<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="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">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="group_option_tips">Poniższe zmiany zostaną zastosowane dla wszystkich kanałów w tej grupie</string>
|
||||||
<string name="today">Dziś</string>
|
<string name="today">Dziś</string>
|
||||||
<string name="yesterday">Wczoraj</string>
|
<string name="yesterday">Wczoraj</string>
|
||||||
@ -92,7 +92,7 @@
|
|||||||
<string name="seven_days">7d</string>
|
<string name="seven_days">7d</string>
|
||||||
<string name="close">Zamknij</string>
|
<string name="close">Zamknij</string>
|
||||||
<string name="get_new_updates">Sprawdź aktualizacje</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="in_coding">W trakcie kodowania</string>
|
||||||
<string name="coming_soon">Dostępne wkrótce</string>
|
<string name="coming_soon">Dostępne wkrótce</string>
|
||||||
<string name="accounts">Konta</string>
|
<string name="accounts">Konta</string>
|
||||||
@ -107,117 +107,11 @@
|
|||||||
<string name="tips_and_support">Porady i wsparcie</string>
|
<string name="tips_and_support">Porady i wsparcie</string>
|
||||||
<string name="tips_and_support_desc">O aplikacji i licencjach open source</string>
|
<string name="tips_and_support_desc">O aplikacji i licencjach open source</string>
|
||||||
<string name="welcome">Witamy</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 name="browse_tos_tips">Zapoznaj się z <i><u>Warunkami Świadczenia Usługi oraz Polityką Prywatności</u></i>
|
||||||
</string>
|
</string>
|
||||||
<string name="terms_of_service">Warunki świadczenia usługi</string>
|
<string name="terms_of_service">Warunki świadczenia usługi</string>
|
||||||
<string name="tos_content">
|
<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>
|
||||||
<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="agree">Zgadzam się</string>
|
<string name="agree">Zgadzam się</string>
|
||||||
<string name="wallpaper_colors">Kolory z tapety</string>
|
<string name="wallpaper_colors">Kolory z tapety</string>
|
||||||
<string name="no_palettes">Brak palet</string>
|
<string name="no_palettes">Brak palet</string>
|
||||||
@ -232,7 +126,7 @@
|
|||||||
<string name="on">Włączono</string>
|
<string name="on">Włączono</string>
|
||||||
<string name="off">Wyłączono</string>
|
<string name="off">Wyłączono</string>
|
||||||
<string name="other">Inne</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="tonal_elevation">Odcień</string>
|
||||||
<string name="reading_fonts">Czcionka artykułu</string>
|
<string name="reading_fonts">Czcionka artykułu</string>
|
||||||
<string name="basic_fonts">Podstawowe czcionki</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="update_link">https://api.github.com/repos/Ashinch/ReadYou/releases/latest</string>
|
||||||
<string name="change_log">Lista zmian</string>
|
<string name="change_log">Lista zmian</string>
|
||||||
<string name="update">Aktualizuj</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="checking_updates">Sprawdzam aktualizacje…</string>
|
||||||
<string name="is_latest_version">Masz najnowszą wersję aplikacji</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="download_failure">Nie udało się pobrać aktualizacji</string>
|
||||||
<string name="rate_limit">Limit częstotliwości żądań</string>
|
<string name="rate_limit">Limit częstotliwości żądań</string>
|
||||||
<string name="help">Pomoc</string>
|
<string name="help">Pomoc</string>
|
||||||
@ -277,12 +171,12 @@
|
|||||||
<string name="filter_bar">Pasek filtrów</string>
|
<string name="filter_bar">Pasek filtrów</string>
|
||||||
<string name="icons">Ikony</string>
|
<string name="icons">Ikony</string>
|
||||||
<string name="icons_and_labels">Ikony z tekstem</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="icons_and_label_only_selected">Ikony z tekstem (tylko aktywne)</string>
|
||||||
<string name="tips_top_bar_tonal_elevation">Ten odcień jest dostępny tylko podczas przewijania.</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">Ten odcień jest dostępny tylko w jasnym motywie.</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">Ten odcień 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="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="text">Tekst</string>
|
||||||
<string name="font_size">Rozmiar czcionki</string>
|
<string name="font_size">Rozmiar czcionki</string>
|
||||||
<string name="letter_spacing">Odstępy między znakami</string>
|
<string name="letter_spacing">Odstępy między znakami</string>
|
||||||
@ -295,8 +189,8 @@
|
|||||||
<string name="images">Obrazy</string>
|
<string name="images">Obrazy</string>
|
||||||
<string name="rounded_corners">Zaokrąglone rogi</string>
|
<string name="rounded_corners">Zaokrąglone rogi</string>
|
||||||
<string name="videos">Filmy</string>
|
<string name="videos">Filmy</string>
|
||||||
<string name="align_start">Do lewej</string>
|
<string name="align_start">Wyrównaj początek</string>
|
||||||
<string name="align_end">Do prawej</string>
|
<string name="align_end">Wyrównaj koniec</string>
|
||||||
<string name="center_text">Do środka</string>
|
<string name="center_text">Do środka</string>
|
||||||
<string name="justify">Wyjustuj</string>
|
<string name="justify">Wyjustuj</string>
|
||||||
<string name="external_fonts">Zewnętrzne czcionki</string>
|
<string name="external_fonts">Zewnętrzne czcionki</string>
|
||||||
@ -330,7 +224,7 @@
|
|||||||
<string name="for_1_month">1 miesiąc</string>
|
<string name="for_1_month">1 miesiąc</string>
|
||||||
<string name="local">Konto lokalne</string>
|
<string name="local">Konto lokalne</string>
|
||||||
<string name="services">Usługi</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="self_hosted">Własny serwer</string>
|
||||||
<string name="more">Więcej</string>
|
<string name="more">Więcej</string>
|
||||||
<string name="add_accounts">Dodaj konta</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="delete_account_toast">Konto zostało usunięte</string>
|
||||||
<string name="clear_all_articles">Usuń wszystkie artykuły</string>
|
<string name="clear_all_articles">Usuń wszystkie artykuły</string>
|
||||||
<string name="block_list">Lista zablokowanych</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="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="local_desc">Na tym urządzeniu</string>
|
||||||
<string name="switch_account">Przełącz</string>
|
<string name="switch_account">Przełącz</string>
|
||||||
<string name="add">Dodaj</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="empty">Pusty</string>
|
||||||
<string name="username">Nazwa użytkownika</string>
|
<string name="username">Nazwa użytkownika</string>
|
||||||
<string name="password">Hasło</string>
|
<string name="password">Hasło</string>
|
||||||
@ -396,4 +290,36 @@
|
|||||||
<string name="unfold_more">Rozwiń wszystkie</string>
|
<string name="unfold_more">Rozwiń wszystkie</string>
|
||||||
<string name="unfold_less">Zwiń wszystkie</string>
|
<string name="unfold_less">Zwiń wszystkie</string>
|
||||||
<string name="about">O aplikacji</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>
|
</resources>
|
@ -317,4 +317,8 @@
|
|||||||
<string name="about">Sobre</string>
|
<string name="about">Sobre</string>
|
||||||
<string name="bionic_reading_tips">O que é Bionic Reading?</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="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>
|
</resources>
|
@ -317,4 +317,8 @@
|
|||||||
<string name="about">Sobre</string>
|
<string name="about">Sobre</string>
|
||||||
<string name="bionic_reading_tips">O que é Bionic Reading?</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="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>
|
</resources>
|
@ -320,4 +320,8 @@
|
|||||||
<string name="read_aloud">Читать вслух</string>
|
<string name="read_aloud">Читать вслух</string>
|
||||||
<string name="native_component">Нативный компонент</string>
|
<string name="native_component">Нативный компонент</string>
|
||||||
<string name="browse_bionic_reading_tips">Узнайте больше на <i><u>bionic-reading.com</u></i>.</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>
|
</resources>
|
@ -317,4 +317,8 @@
|
|||||||
<string name="about">Informácie</string>
|
<string name="about">Informácie</string>
|
||||||
<string name="content_renderer">Zobrazovač obsahu</string>
|
<string name="content_renderer">Zobrazovač obsahu</string>
|
||||||
<string name="only_available_on_webview">Dostupné len vo WebView</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>
|
</resources>
|
@ -111,7 +111,7 @@
|
|||||||
<string name="every_15_minutes">Сваких 15 минута</string>
|
<string name="every_15_minutes">Сваких 15 минута</string>
|
||||||
<string name="interaction">Интеракција</string>
|
<string name="interaction">Интеракција</string>
|
||||||
<string name="check_failure">Провера ажурирања није успела</string>
|
<string name="check_failure">Провера ажурирања није успела</string>
|
||||||
<string name="align_start">Поравнајте почетак</string>
|
<string name="align_start">Поравнај почетак</string>
|
||||||
<string name="keep_archived_articles">Чување архивираних чланака</string>
|
<string name="keep_archived_articles">Чување архивираних чланака</string>
|
||||||
<string name="selected">Изабрано</string>
|
<string name="selected">Изабрано</string>
|
||||||
<string name="icons">Иконице</string>
|
<string name="icons">Иконице</string>
|
||||||
@ -160,7 +160,7 @@
|
|||||||
<string name="sponsor">Спонзор</string>
|
<string name="sponsor">Спонзор</string>
|
||||||
<string name="for_3_days">3 дана</string>
|
<string name="for_3_days">3 дана</string>
|
||||||
<string name="deny">Одбиј</string>
|
<string name="deny">Одбиј</string>
|
||||||
<string name="align_end">Поравнајте крај</string>
|
<string name="align_end">Поравнај крај</string>
|
||||||
<string name="block_list">Блок листа</string>
|
<string name="block_list">Блок листа</string>
|
||||||
<string name="specific_browser_name">Претраживач: %1$s</string>
|
<string name="specific_browser_name">Претраживач: %1$s</string>
|
||||||
<string name="specific_browser">Присили одређени претраживач</string>
|
<string name="specific_browser">Присили одређени претраживач</string>
|
||||||
@ -317,4 +317,8 @@
|
|||||||
<string name="use_bionic_reading">Користите бионичко читање</string>
|
<string name="use_bionic_reading">Користите бионичко читање</string>
|
||||||
<string name="about">О нама</string>
|
<string name="about">О нама</string>
|
||||||
<string name="browse_bionic_reading_tips">Сазнајте више на <i><u>bionic-reading.com</u></i>.</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>
|
</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="only_available_on_webview">Доступно тільки у WebView</string>
|
||||||
<string name="about">Про застосунок</string>
|
<string name="about">Про застосунок</string>
|
||||||
<string name="bionic_reading_tips">Що таке біонічне зчитування?</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>
|
</resources>
|
@ -312,4 +312,7 @@
|
|||||||
<string name="bionic_reading_tips">什么是 Bionic Reading?</string>
|
<string name="bionic_reading_tips">什么是 Bionic Reading?</string>
|
||||||
<string name="browse_bionic_reading_tips">在 <i><u>bionic-reading.com</u></i>.了解更多信息。</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="mark_as_read_on_scroll">滚动时标记为已读</string>
|
||||||
|
<string name="become_a_sponsor">成为赞助者</string>
|
||||||
|
<string name="sponsor_desc">我们在休息时间构建并维护这个自由开源应用。如果你觉得应用还不错,请考虑用小额捐赠支持哦我们!☕️</string>
|
||||||
|
<string name="toolbars">工具栏</string>
|
||||||
</resources>
|
</resources>
|
@ -311,4 +311,8 @@
|
|||||||
<string name="about">關於</string>
|
<string name="about">關於</string>
|
||||||
<string name="bionic_reading_tips">什麼是仿生閱讀?</string>
|
<string name="bionic_reading_tips">什麼是仿生閱讀?</string>
|
||||||
<string name="browse_bionic_reading_tips">了解更多資訊,請造訪 <i><u>bionic-reading.com</u></i>。</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>
|
</resources>
|
@ -392,6 +392,7 @@
|
|||||||
<string name="server_url">Server URL</string>
|
<string name="server_url">Server URL</string>
|
||||||
<string name="username">Username</string>
|
<string name="username">Username</string>
|
||||||
<string name="password">Password</string>
|
<string name="password">Password</string>
|
||||||
|
<string name="client_certificate">Client certificate (optional)</string>
|
||||||
<string name="connection">Connection</string>
|
<string name="connection">Connection</string>
|
||||||
<string name="system_default">System</string>
|
<string name="system_default">System</string>
|
||||||
<string name="initial_open_app">App when link is clicked</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_domain" translatable="false">bionic-reading.com</string>
|
||||||
<string name="bionic_reading_link" translatable="false">https://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="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>
|
</resources>
|
||||||
|
@ -10,6 +10,7 @@ import org.junit.runner.RunWith
|
|||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.junit.MockitoJUnitRunner
|
import org.mockito.junit.MockitoJUnitRunner
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
|
import rust.nostr.sdk.Client
|
||||||
|
|
||||||
internal const val enclosureUrlString1: String = "https://example.com/enclosure.jpg"
|
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"
|
internal const val enclosureUrlString2: String = "https://github.blog/wp-content/uploads/2024/03/github_copilot_header.png"
|
||||||
@ -50,6 +51,9 @@ class RssHelperTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var mockOkHttpClient: OkHttpClient
|
private lateinit var mockOkHttpClient: OkHttpClient
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var mockNostrClient: Client
|
||||||
|
|
||||||
private lateinit var rssHelper: RssHelper
|
private lateinit var rssHelper: RssHelper
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ -57,7 +61,8 @@ class RssHelperTest {
|
|||||||
mockContext = mock<Context> { }
|
mockContext = mock<Context> { }
|
||||||
mockIODispatcher = mock<CoroutineDispatcher> {}
|
mockIODispatcher = mock<CoroutineDispatcher> {}
|
||||||
mockOkHttpClient = mock<OkHttpClient> {}
|
mockOkHttpClient = mock<OkHttpClient> {}
|
||||||
rssHelper = RssHelper(mockContext, mockIODispatcher, mockOkHttpClient)
|
mockNostrClient = mock<Client> { }
|
||||||
|
rssHelper = RssHelper(mockContext, mockIODispatcher, mockOkHttpClient, mockNostrClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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)
|
5. Подобряване на плъзгане за звезда/премахване на звезда, плъзгане до непрочетено и добавяне на конфигурация (#594, @JunkFood02)
|
||||||
6. Използвайте системния локал, за да форматирате дисплея на часа по подразбиране. (#617, @JunkFood02)
|
6. Използвайте системния локал, за да форматирате дисплея на часа по подразбиране. (#617, @JunkFood02)
|
||||||
7. Преминете към внедряване на androidx edge to edge (#690, @Moderpach)
|
7. Преминете към внедряване на androidx edge to edge (#690, @Moderpach)
|
||||||
8. Добавете програма за преглед на изображения към страницата за четене (#578, #545, @JunkFood02, @nvllz)
|
8. Добавяне на програма за преглед на изображения към страницата за четене (#578, #545, @JunkFood02, @nvllz)
|
||||||
9. Добавете жестове за плъзгане нагоре и надолу по страницата за четене, за да превключвате статии (#589, @JunkFood02)
|
9. Добавяне на жестове за плъзгане нагоре и надолу по страницата за четене, за да превключвате статии (#589, @JunkFood02)
|
||||||
10. Добавяне на дейност за докладване на срив, за да се справят с неуловени изключения (#576, @JunkFood02)
|
10. Добавяне на дейност за докладване на срив, за да се справят с неуловени изключения (#576, @JunkFood02)
|
||||||
11. Добавете контекстно меню при продължително натискане за елементи в страницата на потока (#613, @JunkFood02)
|
11. Добавете контекстно меню при продължително натискане за елементи в страницата на потока (#613, @JunkFood02)
|
||||||
12. Добавяне на многократно предпочитание за височина на реда за страница за четене (#620, @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>
|
<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
|
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