Compare commits

...

28 Commits

Author SHA1 Message Date
drgnchan
74d0c50da7
Scroll back to last seen item(from unread list) (#935)
* refactor: use initial items in unread reading-view-model

* chore: remove code comment
2025-01-24 11:34:21 +08:00
Weblate (bot)
54e022169a
Translated using Weblate (Japanese) (#884)
Currently translated at 36.0% (9 of 25 strings)

Translated using Weblate (Polish)

Currently translated at 64.0% (16 of 25 strings)

Translated using Weblate (Polish)

Currently translated at 52.0% (13 of 25 strings)

Translated using Weblate (Estonian)

Currently translated at 12.0% (3 of 25 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (24 of 24 strings)













Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/es/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/et/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/id/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/ja/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/vi/
Translation: ReadYou/F-Droid and Play Store metadata

Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Kum Hathaway <drqw0lsw6@mozmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Whiteowle <swillowhoe.precise538@passinbox.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: mastoduy <mastoduy@gmail.com>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
2025-01-24 10:55:12 +08:00
Weblate (bot)
09d9f4d55b
Translated using Weblate (Greek) (#908)
Currently translated at 10.0% (34 of 340 strings)

Added translation using Weblate (Greek)

Translated using Weblate (Estonian)

Currently translated at 56.1% (191 of 340 strings)

Translated using Weblate (Hebrew)

Currently translated at 76.4% (260 of 340 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Estonian)

Currently translated at 53.8% (183 of 340 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Malayalam)

Currently translated at 41.1% (140 of 340 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Estonian)

Currently translated at 42.3% (144 of 340 strings)

Translated using Weblate (German)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Estonian)

Currently translated at 35.2% (120 of 340 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Malayalam)

Currently translated at 40.0% (136 of 340 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (340 of 340 strings)

Added translation using Weblate (Tamil)

Translated using Weblate (Slovak)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (340 of 340 strings)

























Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/apc/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/ar/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/bg/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/cs/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/de/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/el/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/es/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/et/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/he/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/id/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/it/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/ml/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/pl/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/pt/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/ru/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/sk/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/sr/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/uk/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/zh_Hant/
Translation: ReadYou/Android strings

Co-authored-by: Akhil Raj <89210430+akhi07rx@users.noreply.github.com>
Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Co-authored-by: Artur V. Neto <arturvanderleineto@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Geovani Amaral <geovani.af4@gmail.com>
Co-authored-by: Jacob <ex.deme@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Maharajan <maha1314@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
Co-authored-by: Pizza Party <paol.m@proton.me>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: hoems222 <hoems222@mailbox.org>
Co-authored-by: nhman <eliezr34@gmail.com>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: Λευτέρης Τ <lefteris592@gmail.com>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-01-24 10:54:52 +08:00
ymcx
1ffdb2f4f7
Make hiding empty groups toggleable (#921) 2025-01-24 10:53:44 +08:00
Angelo Suzuki
39dc3bceee
Support specifying a client certificate for mTLS auth (#940) 2025-01-24 10:47:39 +08:00
KotlinGeekDev
ec05bddba8
Nostr feed support. (#945)
* Add rust nostr library.

* Build FetchedFeed abstractions to be used for representing fetched feeds.

* Replace some occurrences of SyndFeed with FetchedFeed where needed.

* Add a function for checking a Nostr URI. Modify other occurrences in AbstractRssRepository.

* Add validation for Nostr URIs.

* Actually take Nostr URIs into account when searching for feeds.

* Move fetchFeedFrom() to a companion object instead.

* Update Nostr-sdk library and add Proguard rules for the Nostr sdk library.

* Create a singleton DI module for the Nostr Client.

* Add some fixes for summary, and others.

* Include Jetbrains Markdown library.

* Introduce htmlFromMarkdown(), and use it where needed.

* Revert to using the Nostr URI for the article link, for full content support.

* Implement full content support for Nostr articles, by reusing the content, since it's all the same anyway.

* Switch to throwing an exception when no Nostr feed info is found.

* Add feed syncing for Nostr feeds.

* Use a new Nostr client instance for each syncing feed, to avoid race conditions when using the client.

* Change syncNostrFeed return signature. Improve Nostr feed syncing. Use randomUuid for Nostr article Ids.

* Add OPML feed support for Nostr feeds. Try getting metadata first before saving the feed(else it won't modify it later.)

* Just use the author's profile name as feed title when fetching feed.

* Fix RssHelperTest.

* Fix bug caused by non-differentiation between outlines and sub-outlines when handling sub-outlines.

* Re-use original code for importing feeds from OPML(for GH runner happiness, maybe).

* Revert previous change(just uncommenting code).

* Just return feed with empty article list if nothing is found.

* Manage Nostr Client lifecycle when using it during OPML import.

* Undo changes of previous commit.
2025-01-24 10:41:44 +08:00
junkfood
e0cd9acb03
fix: user agent in build config (#914) 2024-12-01 21:01:24 +08:00
junkfood
133f196430
fix(sync): refactor SyncWorker for robust background sync (#909)
* feat(sync): sync feeds concurrently

* fix(sync): refactor `SyncWorker` for robust background sync
2024-11-27 16:10:03 +08:00
junkfood
4caca8db39
feat(sync): sync feeds concurrently (#907) 2024-11-27 16:09:27 +08:00
junkfood
99eca4b109
chore: release v0.11.1 (27) 2024-11-24 17:55:32 +08:00
junkfood
1826d2f948
Merge remote-tracking branch 'weblate/main' 2024-11-24 17:42:28 +08:00
junkfood
94323b84a0
feat(ui): preserve search state in flow page (#906) 2024-11-24 17:38:54 +08:00
junkfood
976678812e
fix(ui): commit diffs before running in background (#901) 2024-11-24 00:46:58 +08:00
junkfood
e55f2c976a
feat(ui): fine-tune colors (#899)
* fix(ui): migrate feed page to surface container

* fix: remove tonal elevation for feed list

* fix(ui): add fixed color roles
2024-11-23 16:47:09 +08:00
junkfood
368ed14801
fix: toolbar elevation 2024-11-22 23:27:23 +08:00
junkfood
3ba1eed774
feat(ui): show title in top bar upon scroll in flow page (#896)
* feat(ui): show title in top bar upon scroll in flow page

* fix(reader): make webview align with reading font family (#882)

* fix(reader): make webview align with reading font family

* feat(reader): external font support in webview

* fix(ui): fine-tune dark color scheme

* feat(ui): top bar elevation preference
2024-11-22 22:34:47 +08:00
junkfood
6e55c12ca8
feat(ui): elevated style reader toolbars (#894)
* feat(ui): toolbar animation

* feat(ui): elevated variant for reader toolbars
2024-11-22 22:34:36 +08:00
junkfood
629f489ee3
refactor(ui): remove reading dark theme (#890) 2024-11-20 22:22:53 +08:00
junkfood
76ec77a502
fix(ui): fine-tune dark color scheme 2024-11-20 22:08:52 +08:00
junkfood
697e94eb88
fix(reader): make webview align with reading font family (#882)
* fix(reader): make webview align with reading font family

* feat(reader): external font support in webview
2024-11-20 15:17:40 +08:00
junkfood
47a911ae0d
fix(reader): figcaption font size 2024-11-20 01:07:39 +08:00
junkfood
a89ccc7fd9
fix(ui): remove tonal elevation preference in reading page 2024-11-18 21:46:22 +08:00
Weblate (bot)
84a04d89ff
Translated using Weblate (Serbian) (#875)
Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (337 of 337 strings)













Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/ar/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/bg/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/cs/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/es/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/id/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/it/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/pt/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/sk/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/sr/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/uk/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/zh_Hans/
Translation: ReadYou/Android strings

Co-authored-by: Cleverson Cândido <optimuspraimu@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-11-16 02:35:08 +08:00
Hosted Weblate
0d514cd00a
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (337 of 337 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (337 of 337 strings)

Co-authored-by: Cleverson Cândido <optimuspraimu@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/ar/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/bg/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/cs/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/es/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/id/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/it/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/pt/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/sk/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/sr/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/uk/
Translate-URL: https://hosted.weblate.org/projects/readyou/android-strings/zh_Hans/
Translation: ReadYou/Android strings
2024-11-15 19:34:40 +01:00
Weblate (bot)
d34fa76f5b
Translated using Weblate (Bulgarian) (#876)
Currently translated at 95.8% (23 of 24 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Bulgarian)

Currently translated at 95.8% (23 of 24 strings)






Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/bg/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/es/
Translate-URL: https://hosted.weblate.org/projects/readyou/f-droid-and-play-store-metadata/sk/
Translation: ReadYou/F-Droid and Play Store metadata

Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: trunars <trunars@gmail.com>
2024-11-16 02:34:36 +08:00
junkfood
f36a24efcd
fix(sync): update enqueued works instead of replacing 2024-11-14 22:31:48 +08:00
junkfood
3ab910582a
fix(webview): intercept default click events for imgs 2024-11-14 22:20:00 +08:00
junkfood
82e06daea4
feat(ui): add sponsor dialog 2024-11-14 16:39:44 +08:00
127 changed files with 2257 additions and 660 deletions

View File

@ -32,10 +32,10 @@ android {
applicationId = "me.ash.reader"
minSdk = 26
targetSdk = 33
versionCode = 26
versionName = "0.11.0"
versionCode = 27
versionName = "0.11.1"
buildConfigField("String", "USER_AGENT_STRING", "\"ReadYou/${'$'}{versionName}(${versionCode})\"")
buildConfigField("String", "USER_AGENT_STRING", "\"ReadYou/${versionName}(${versionCode})\"")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -176,6 +176,12 @@ dependencies {
implementation(libs.activity.compose)
implementation(libs.appcompat)
// Markdown
implementation(libs.jetbrains.markdown)
// Nostr
implementation(libs.rust.nostr)
// Testing
testImplementation(libs.junit)
androidTestImplementation(libs.junit.ext)

View File

@ -37,6 +37,14 @@
# Provider API
-keep class me.ash.reader.** { *; }
# Nostr
-keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; }
-dontwarn java.awt.Component
-dontwarn java.awt.GraphicsEnvironment
-dontwarn java.awt.HeadlessException
-dontwarn java.awt.Window
# https://github.com/flutter/flutter/issues/127388
-dontwarn org.kxml2.io.KXml**

View File

@ -5,11 +5,13 @@ class FeverSecurityKey private constructor() : SecurityKey() {
var serverUrl: String? = null
var username: String? = null
var password: String? = null
var clientCertificateAlias: String? = null
constructor(serverUrl: String?, username: String?, password: String?) : this() {
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
this.serverUrl = serverUrl
this.username = username
this.password = password
this.clientCertificateAlias = clientCertificateAlias
}
constructor(value: String? = DESUtils.empty) : this() {
@ -17,6 +19,7 @@ class FeverSecurityKey private constructor() : SecurityKey() {
serverUrl = it.serverUrl
username = it.username
password = it.password
clientCertificateAlias = it.clientCertificateAlias
}
}
}

View File

@ -5,11 +5,13 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
var serverUrl: String? = null
var username: String? = null
var password: String? = null
var clientCertificateAlias: String? = null
constructor(serverUrl: String?, username: String?, password: String?) : this() {
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
this.serverUrl = serverUrl
this.username = username
this.password = password
this.clientCertificateAlias = clientCertificateAlias
}
constructor(value: String? = DESUtils.empty) : this() {
@ -17,6 +19,7 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
serverUrl = it.serverUrl
username = it.username
password = it.password
clientCertificateAlias = it.clientCertificateAlias
}
}
}

View File

@ -5,11 +5,13 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
var serverUrl: String? = null
var username: String? = null
var password: String? = null
var clientCertificateAlias: String? = null
constructor(serverUrl: String?, username: String?, password: String?) : this() {
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
this.serverUrl = serverUrl
this.username = username
this.password = password
this.clientCertificateAlias = clientCertificateAlias
}
constructor(value: String? = DESUtils.empty) : this() {
@ -17,6 +19,7 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
serverUrl = it.serverUrl
username = it.username
password = it.password
clientCertificateAlias = it.clientCertificateAlias
}
}
}

View File

@ -6,8 +6,8 @@ import androidx.paging.PagingSource
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import com.rometools.rome.feed.synd.SyndFeed
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import me.ash.reader.domain.model.account.Account
import me.ash.reader.domain.model.article.ArticleWithFeed
import me.ash.reader.domain.model.feed.Feed
@ -28,9 +30,13 @@ import me.ash.reader.domain.repository.GroupDao
import me.ash.reader.infrastructure.android.NotificationHelper
import me.ash.reader.infrastructure.preference.KeepArchivedPreference
import me.ash.reader.infrastructure.preference.SyncIntervalPreference
import me.ash.reader.infrastructure.rss.FetchedFeed
import me.ash.reader.infrastructure.rss.NostrFeed
import me.ash.reader.infrastructure.rss.RssHelper
import me.ash.reader.infrastructure.rss.SyndFeedDelegate
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.decodeHTML
import me.ash.reader.ui.ext.isNostrUri
import me.ash.reader.ui.ext.spacerDollar
import java.util.Date
import java.util.UUID
@ -59,19 +65,26 @@ abstract class AbstractRssRepository(
open suspend fun clearAuthorization() {}
open suspend fun subscribe(
feedLink: String, searchedFeed: SyndFeed, groupId: String,
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
isNotification: Boolean, isFullContent: Boolean
) {
val accountId = context.currentAccountId
val feed = Feed(
id = accountId.spacerDollar(UUID.randomUUID().toString()),
name = searchedFeed.title.decodeHTML()!!,
name = with(searchedFeed.title){ if (this.isNostrUri()) this else this.decodeHTML()!!},
url = feedLink,
groupId = groupId,
accountId = accountId,
icon = searchedFeed.icon?.link
icon = searchedFeed.getIconLink()
)
val articles = searchedFeed.entries.map { rssHelper.buildArticleFromSyndEntry(feed, accountId, it) }
val articles = when(searchedFeed) {
is NostrFeed -> searchedFeed.getArticles().map {
rssHelper.buildArticleFromNostrEvent(feed, accountId, it, searchedFeed.getFeedAuthor())
}
is SyndFeedDelegate -> searchedFeed.getArticles().map {
rssHelper.buildArticleFromSyndEntry(feed, accountId, it)
}
}
feedDao.insert(feed)
articleDao.insertList(articles.map {
it.copy(feedId = feed.id)
@ -101,21 +114,18 @@ abstract class AbstractRssRepository(
val preTime = System.currentTimeMillis()
val preDate = Date(preTime)
val accountId = context.currentAccountId
feedDao.queryAll(accountId)
.chunked(16)
.forEach {
it.map { feed -> async { syncFeed(feed, preDate) } }
.awaitAll()
.forEach {
if (it.feed.isNotification) {
notificationHelper.notify(it.apply {
articles = articleDao.insertListIfNotExist(it.articles)
})
} else {
articleDao.insertListIfNotExist(it.articles)
}
val semaphore = Semaphore(16)
feedDao.queryAll(accountId).mapIndexed { _, feed ->
async(Dispatchers.IO) {
semaphore.withPermit {
val feedWithArticle = syncFeed(feed, preDate)
val newArticles = articleDao.insertListIfNotExist(feedWithArticle.articles)
if (feedWithArticle.feed.isNotification) {
notificationHelper.notify(feedWithArticle.copy(articles = newArticles))
}
}
}
}.awaitAll()
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
accountDao.queryById(accountId)?.let { account ->
@ -177,17 +187,29 @@ abstract class AbstractRssRepository(
private suspend fun syncFeed(feed: Feed, preDate: Date = Date()): FeedWithArticle {
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
val articles = rssHelper.queryRssXml(feed, "", preDate)
if (feed.icon == null) {
val iconLink = rssHelper.queryRssIconLink(feed.url)
if (iconLink != null) {
rssHelper.saveRssIcon(feedDao, feed, iconLink)
}
if (feed.url.isNostrUri()) {
val syncedFeed = rssHelper.syncNostrFeed(feed, "", preDate)
return FeedWithArticle(
feed = syncedFeed.feed
.apply { isNotification = feed.isNotification && syncedFeed.articles.isNotEmpty() },
articles = syncedFeed.articles
)
}
return FeedWithArticle(
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
articles = articles
)
else {
val articles = rssHelper.queryRssXml(feed, "", preDate)
if (feed.icon == null) {
val iconLink = rssHelper.queryRssIconLink(feed.url)
if (iconLink != null) {
rssHelper.saveRssIcon(feedDao, feed, iconLink)
}
}
return FeedWithArticle(
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
articles = articles
)
}
}
suspend fun clearKeepArchivedArticles() {
@ -200,39 +222,29 @@ abstract class AbstractRssRepository(
}
fun cancelSync() {
workManager.cancelAllWork()
SyncWorker.cancelPeriodicWork(workManager)
SyncWorker.cancelOneTimeWork(workManager)
}
fun doSyncOneTime() {
workManager.cancelAllWork()
SyncWorker.enqueueOneTimeWork(workManager)
}
suspend fun doSync(isOnStart: Boolean = false) {
workManager.cancelAllWork()
suspend fun initSync() {
accountDao.queryById(context.currentAccountId)?.let {
if (isOnStart) {
if (it.syncOnStart.value) {
SyncWorker.enqueueOneTimeWork(workManager)
}
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
SyncWorker.enqueuePeriodicWork(
workManager = workManager,
syncInterval = it.syncInterval,
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
)
}
val syncOnStart = it.syncOnStart.value
if (syncOnStart) {
doSyncOneTime()
}
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
SyncWorker.enqueuePeriodicWork(
workManager = workManager,
syncInterval = it.syncInterval,
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
)
} else {
SyncWorker.enqueueOneTimeWork(workManager)
if (it.syncInterval.value != SyncIntervalPreference.Manually.value) {
SyncWorker.enqueuePeriodicWork(
workManager = workManager,
syncInterval = it.syncInterval,
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
)
}
SyncWorker.cancelPeriodicWork(workManager)
}
}
}

View File

@ -5,7 +5,6 @@ import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import com.rometools.rome.feed.synd.SyndFeed
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.supervisorScope
@ -27,6 +26,7 @@ import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.di.MainDispatcher
import me.ash.reader.infrastructure.exception.FeverAPIException
import me.ash.reader.infrastructure.html.Readability
import me.ash.reader.infrastructure.rss.FetchedFeed
import me.ash.reader.infrastructure.rss.RssHelper
import me.ash.reader.infrastructure.rss.provider.fever.FeverAPI
import me.ash.reader.infrastructure.rss.provider.fever.FeverDTO
@ -70,11 +70,13 @@ class FeverRssService @Inject constructor(
private suspend fun getFeverAPI() =
FeverSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
FeverAPI.getInstance(
context = context,
serverUrl = serverUrl!!,
username = username!!,
password = password!!,
httpUsername = null,
httpPassword = null,
clientCertificateAlias = clientCertificateAlias,
)
}
@ -86,7 +88,7 @@ class FeverRssService @Inject constructor(
}
override suspend fun subscribe(
feedLink: String, searchedFeed: SyndFeed, groupId: String,
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
isNotification: Boolean, isFullContent: Boolean,
) {
throw FeverAPIException("Unsupported")

View File

@ -5,7 +5,6 @@ import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import com.rometools.rome.feed.synd.SyndFeed
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.supervisorScope
@ -25,6 +24,7 @@ import me.ash.reader.infrastructure.di.DefaultDispatcher
import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.di.MainDispatcher
import me.ash.reader.infrastructure.html.Readability
import me.ash.reader.infrastructure.rss.FetchedFeed
import me.ash.reader.infrastructure.rss.RssHelper
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofCategoryIdToStreamId
@ -72,11 +72,13 @@ class GoogleReaderRssService @Inject constructor(
private suspend fun getGoogleReaderAPI() =
GoogleReaderSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
GoogleReaderAPI.getInstance(
context = context,
serverUrl = serverUrl!!,
username = username!!,
password = password!!,
httpUsername = null,
httpPassword = null,
clientCertificateAlias = clientCertificateAlias,
)
}
@ -97,7 +99,7 @@ class GoogleReaderRssService @Inject constructor(
}
override suspend fun subscribe(
feedLink: String, searchedFeed: SyndFeed, groupId: String,
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
isNotification: Boolean, isFullContent: Boolean,
) {
val accountId = context.currentAccountId

View File

@ -33,15 +33,25 @@ class SyncWorker @AssistedInject constructor(
companion object {
private const val IS_SYNCING = "isSyncing"
const val WORK_NAME = "ReadYou"
lateinit var uuid: UUID
private const val WORK_NAME_PERIODIC = "ReadYou"
private const val WORK_NAME_ONETIME = "SYNC_ONETIME"
const val WORK_TAG = "SYNC_TAG"
fun cancelOneTimeWork(workManager: WorkManager) {
workManager.cancelUniqueWork(WORK_NAME_ONETIME)
}
fun cancelPeriodicWork(workManager: WorkManager) {
workManager.cancelUniqueWork(WORK_NAME_PERIODIC)
}
fun enqueueOneTimeWork(
workManager: WorkManager,
) {
workManager.enqueue(OneTimeWorkRequestBuilder<SyncWorker>()
.addTag(WORK_NAME)
.build()
workManager.enqueueUniqueWork(
WORK_NAME_ONETIME,
ExistingWorkPolicy.KEEP,
OneTimeWorkRequestBuilder<SyncWorker>().addTag(WORK_TAG).build()
)
}
@ -52,15 +62,16 @@ class SyncWorker @AssistedInject constructor(
syncOnlyOnWiFi: SyncOnlyOnWiFiPreference,
) {
workManager.enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
WORK_NAME_PERIODIC,
ExistingPeriodicWorkPolicy.UPDATE,
PeriodicWorkRequestBuilder<SyncWorker>(syncInterval.value, TimeUnit.MINUTES)
.setConstraints(Constraints.Builder()
.setRequiresCharging(syncOnlyWhenCharging.value)
.setRequiredNetworkType(if (syncOnlyOnWiFi.value) NetworkType.UNMETERED else NetworkType.CONNECTED)
.build()
.setConstraints(
Constraints.Builder()
.setRequiresCharging(syncOnlyWhenCharging.value)
.setRequiredNetworkType(if (syncOnlyOnWiFi.value) NetworkType.UNMETERED else NetworkType.CONNECTED)
.build()
)
.addTag(WORK_NAME)
.addTag(WORK_TAG)
.setInitialDelay(syncInterval.value, TimeUnit.MINUTES)
.build()
)

View File

@ -22,6 +22,7 @@ import me.ash.reader.ui.ext.del
import me.ash.reader.ui.ext.getLatestApk
import me.ash.reader.ui.ext.isGitHub
import okhttp3.OkHttpClient
import rust.nostr.sdk.Client
import javax.inject.Inject
/**
@ -58,6 +59,9 @@ class AndroidApp : Application(), Configuration.Provider {
@Inject
lateinit var rssHelper: RssHelper
@Inject
lateinit var nostrClient: Client
@Inject
lateinit var notificationHelper: NotificationHelper
@ -133,7 +137,7 @@ class AndroidApp : Application(), Configuration.Provider {
}
private suspend fun workerInit() {
rssService.get().doSync(isOnStart = true)
rssService.get().initSync()
}
private suspend fun checkUpdate() {

View File

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

View File

@ -20,7 +20,9 @@
package me.ash.reader.infrastructure.di
import android.annotation.SuppressLint
import android.content.Context
import android.security.KeyChain
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -31,15 +33,18 @@ import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.internal.platform.Platform
import java.io.File
import java.net.Socket
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.security.Principal
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509KeyManager
import javax.net.ssl.X509TrustManager
/**
@ -54,6 +59,7 @@ object OkHttpClientModule {
fun provideOkHttpClient(
@ApplicationContext context: Context,
): OkHttpClient = cachingHttpClient(
context = context,
cacheDirectory = context.cacheDir.resolve("http")
).newBuilder()
.addNetworkInterceptor(UserAgentInterceptor)
@ -61,11 +67,13 @@ object OkHttpClientModule {
}
fun cachingHttpClient(
context: Context,
cacheDirectory: File? = null,
cacheSize: Long = 10L * 1024L * 1024L,
trustAllCerts: Boolean = true,
connectTimeoutSecs: Long = 30L,
readTimeoutSecs: Long = 30L,
clientCertificateAlias: String? = null,
): OkHttpClient {
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
@ -78,31 +86,75 @@ fun cachingHttpClient(
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
.followRedirects(true)
if (trustAllCerts) {
builder.trustAllCerts()
if (!clientCertificateAlias.isNullOrBlank() || trustAllCerts) {
builder.setupSsl(context, clientCertificateAlias, trustAllCerts)
}
return builder.build()
}
fun OkHttpClient.Builder.trustAllCerts() {
fun OkHttpClient.Builder.setupSsl(
context: Context,
clientCertificateAlias: String?,
trustAllCerts: Boolean
) {
try {
val trustManager = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
val clientKeyManager = clientCertificateAlias?.let { clientAlias ->
object : X509KeyManager {
override fun getClientAliases(keyType: String?, issuers: Array<Principal>?) =
throw UnsupportedOperationException("getClientAliases")
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
override fun chooseClientAlias(
keyType: Array<String>?,
issuers: Array<Principal>?,
socket: Socket?
) = clientCertificateAlias
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
override fun getServerAliases(keyType: String?, issuers: Array<Principal>?) =
throw UnsupportedOperationException("getServerAliases")
override fun chooseServerAlias(
keyType: String?,
issuers: Array<Principal>?,
socket: Socket?
) = throw UnsupportedOperationException("chooseServerAlias")
override fun getCertificateChain(alias: String?): Array<X509Certificate>? {
return if (alias == clientAlias) KeyChain.getCertificateChain(context, clientAlias) else null
}
override fun getPrivateKey(alias: String?): PrivateKey? {
return if (alias == clientAlias) KeyChain.getPrivateKey(context, clientAlias) else null
}
}
}
val trustManager = if (trustAllCerts) {
hostnameVerifier { _, _ -> true }
@SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?
) = Unit
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?
) = Unit
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
} else {
Platform.get().platformTrustManager()
}
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
sslContext.init(arrayOf(clientKeyManager), arrayOf(trustManager), null)
val sslSocketFactory = sslContext.socketFactory
sslSocketFactory(sslSocketFactory, trustManager)
.hostnameVerifier(HostnameVerifier { _, _ -> true })
} catch (e: NoSuchAlgorithmException) {
// ignore
} catch (e: KeyManagementException) {

View File

@ -15,12 +15,8 @@ val LocalFlowTopBarTonalElevation =
compositionLocalOf<FlowTopBarTonalElevationPreference> { FlowTopBarTonalElevationPreference.default }
sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
object Level0 : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
object Level1 : FlowTopBarTonalElevationPreference(ElevationTokens.Level1)
object Level2 : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
object Level3 : FlowTopBarTonalElevationPreference(ElevationTokens.Level3)
object Level4 : FlowTopBarTonalElevationPreference(ElevationTokens.Level4)
object Level5 : FlowTopBarTonalElevationPreference(ElevationTokens.Level5)
object None : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
object Elevated : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
@ -30,27 +26,19 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
fun toDesc(context: Context): String =
when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
None -> "Level 0 (${ElevationTokens.Level0}dp)"
Elevated -> "Level 2 (${ElevationTokens.Level2}dp)"
}
companion object {
val default = Level0
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
val default = Elevated
val values = listOf(None, Elevated)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKey.keys[flowTopBarTonalElevation]?.key as Preferences.Key<Int>]) {
ElevationTokens.Level0 -> Level0
ElevationTokens.Level1 -> Level1
ElevationTokens.Level2 -> Level2
ElevationTokens.Level3 -> Level3
ElevationTokens.Level4 -> Level4
ElevationTokens.Level5 -> Level5
ElevationTokens.Level0 -> None
ElevationTokens.Level2 -> Elevated
else -> default
}
}

View File

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

View File

@ -56,7 +56,6 @@ fun Preferences.toSettings(): Settings {
readingRenderer = ReadingRendererPreference.fromPreferences(this),
readingBionicReading = ReadingBionicReadingPreference.fromPreferences(this),
readingTheme = ReadingThemePreference.fromPreferences(this),
readingDarkTheme = ReadingDarkThemePreference.fromPreferences(this),
readingPageTonalElevation = ReadingPageTonalElevationPreference.fromPreferences(this),
readingAutoHideToolbar = ReadingAutoHideToolbarPreference.fromPreferences(this),
readingTextFontSize = ReadingTextFontSizePreference.fromPreferences(this),
@ -82,6 +81,7 @@ fun Preferences.toSettings(): Settings {
swipeStartAction = SwipeStartActionPreference.fromPreferences(this),
swipeEndAction = SwipeEndActionPreference.fromPreferences(this),
markAsReadOnScroll = MarkAsReadOnScrollPreference.fromPreferences(this),
hideEmptyGroups = HideEmptyGroupsPreference.fromPreferences(this),
pullToSwitchArticle = PullToSwitchArticlePreference.fromPreference(this),
openLink = OpenLinkPreference.fromPreferences(this),
openLinkSpecificBrowser = OpenLinkSpecificBrowserPreference.fromPreferences(this),

View File

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

View File

@ -15,42 +15,30 @@ val LocalReadingPageTonalElevation =
compositionLocalOf<ReadingPageTonalElevationPreference> { ReadingPageTonalElevationPreference.default }
sealed class ReadingPageTonalElevationPreference(val value: Int) : Preference() {
object Level0 : ReadingPageTonalElevationPreference(ElevationTokens.Level0)
object Level1 : ReadingPageTonalElevationPreference(ElevationTokens.Level1)
object Level2 : ReadingPageTonalElevationPreference(ElevationTokens.Level2)
object Level3 : ReadingPageTonalElevationPreference(ElevationTokens.Level3)
object Level4 : ReadingPageTonalElevationPreference(ElevationTokens.Level4)
object Level5 : ReadingPageTonalElevationPreference(ElevationTokens.Level5)
data object Outlined : ReadingPageTonalElevationPreference(ElevationTokens.Level0)
data object Elevated : ReadingPageTonalElevationPreference(ElevationTokens.Level2)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(DataStoreKey.readingPageTonalElevation, value)
context.dataStore.put(readingPageTonalElevation, value)
}
}
fun toDesc(context: Context): String =
when (this) {
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
Outlined -> "${ElevationTokens.Level0}dp"
Elevated -> "${ElevationTokens.Level2}dp"
}
companion object {
val default = Level0
val values = listOf(Level0, Level1, Level2, Level3, Level4, Level5)
val default = Outlined
val values = listOf(Outlined, Elevated)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKey.keys[readingPageTonalElevation]?.key as Preferences.Key<Int>]) {
ElevationTokens.Level0 -> Level0
ElevationTokens.Level1 -> Level1
ElevationTokens.Level2 -> Level2
ElevationTokens.Level3 -> Level3
ElevationTokens.Level4 -> Level4
ElevationTokens.Level5 -> Level5
ElevationTokens.Level0 -> Outlined
ElevationTokens.Level2 -> Elevated
else -> default
}
}

View File

@ -54,7 +54,6 @@ data class Settings(
val readingRenderer: ReadingRendererPreference = ReadingRendererPreference.default,
val readingBionicReading: ReadingBionicReadingPreference = ReadingBionicReadingPreference.default,
val readingTheme: ReadingThemePreference = ReadingThemePreference.default,
val readingDarkTheme: ReadingDarkThemePreference = ReadingDarkThemePreference.default,
val readingPageTonalElevation: ReadingPageTonalElevationPreference = ReadingPageTonalElevationPreference.default,
val readingAutoHideToolbar: ReadingAutoHideToolbarPreference = ReadingAutoHideToolbarPreference.default,
val readingTextFontSize: Int = ReadingTextFontSizePreference.default,
@ -80,6 +79,7 @@ data class Settings(
val swipeStartAction: SwipeStartActionPreference = SwipeStartActionPreference.default,
val swipeEndAction: SwipeEndActionPreference = SwipeEndActionPreference.default,
val markAsReadOnScroll: MarkAsReadOnScrollPreference = MarkAsReadOnScrollPreference.default,
val hideEmptyGroups: HideEmptyGroupsPreference = HideEmptyGroupsPreference.default,
val pullToSwitchArticle: PullToSwitchArticlePreference = PullToSwitchArticlePreference.default,
val openLink: OpenLinkPreference = OpenLinkPreference.default,
val openLinkSpecificBrowser: OpenLinkSpecificBrowserPreference = OpenLinkSpecificBrowserPreference.default,
@ -146,7 +146,6 @@ fun SettingsProvider(
LocalReadingRenderer provides settings.readingRenderer,
LocalReadingBionicReading provides settings.readingBionicReading,
LocalReadingTheme provides settings.readingTheme,
LocalReadingDarkTheme provides settings.readingDarkTheme,
LocalReadingPageTonalElevation provides settings.readingPageTonalElevation,
LocalReadingAutoHideToolbar provides settings.readingAutoHideToolbar,
LocalReadingTextFontSize provides settings.readingTextFontSize,
@ -172,6 +171,7 @@ fun SettingsProvider(
LocalArticleListSwipeStartAction provides settings.swipeStartAction,
LocalArticleListSwipeEndAction provides settings.swipeEndAction,
LocalMarkAsReadOnScroll provides settings.markAsReadOnScroll,
LocalHideEmptyGroups provides settings.hideEmptyGroups,
LocalPullToSwitchArticle provides settings.pullToSwitchArticle,
LocalOpenLink provides settings.openLink,
LocalOpenLinkSpecificBrowser provides settings.openLinkSpecificBrowser,

View File

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

View File

@ -10,7 +10,9 @@ import me.ash.reader.domain.model.group.Group
import me.ash.reader.domain.model.group.GroupWithFeed
import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.ui.ext.extractDomain
import me.ash.reader.ui.ext.isNostrUri
import me.ash.reader.ui.ext.spacerDollar
import rust.nostr.sdk.Client
import java.io.InputStream
import java.util.*
import javax.inject.Inject
@ -48,7 +50,21 @@ class OPMLDataSource @Inject constructor(
)
}
} else {
groupWithFeedList.addFeedToDefault(
val feedUrl = outline.extractUrl()
val feedToAdd = if (feedUrl?.isNostrUri() == true) {
val feedMetadata = NostrFeed.fetchFeedMetadata(feedUrl, Client())
Feed(
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
name = outline.extractName(),
url = outline.extractUrl() ?: continue,
icon = feedMetadata.imageUrl,
groupId = defaultGroup.id,
accountId = targetAccountId,
isNotification = outline.extractPresetNotification(),
isFullContent = outline.extractPresetFullContent(),
)
}
else {
Feed(
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
name = outline.extractName(),
@ -58,7 +74,8 @@ class OPMLDataSource @Inject constructor(
isNotification = outline.extractPresetNotification(),
isFullContent = outline.extractPresetFullContent(),
)
)
}
groupWithFeedList.addFeedToDefault(feedToAdd)
}
} else {
var groupId = defaultGroup.id
@ -74,7 +91,21 @@ class OPMLDataSource @Inject constructor(
}
for (subOutline in outline.subElements) {
if (subOutline != null && subOutline.attributes != null) {
groupWithFeedList.addFeed(
val feedUrl = outline.extractUrl()
val feedToAdd = if (feedUrl?.isNostrUri() == true) {
val feedMetadata = NostrFeed.fetchFeedMetadata(feedUrl, Client())
Feed(
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
name = subOutline.extractName(),
url = subOutline.extractUrl() ?: continue,
icon = feedMetadata.imageUrl,
groupId = groupId,
accountId = targetAccountId,
isNotification = subOutline.extractPresetNotification(),
isFullContent = subOutline.extractPresetFullContent(),
)
}
else {
Feed(
id = targetAccountId.spacerDollar(UUID.randomUUID().toString()),
name = subOutline.extractName(),
@ -84,7 +115,8 @@ class OPMLDataSource @Inject constructor(
isNotification = subOutline.extractPresetNotification(),
isFullContent = subOutline.extractPresetFullContent(),
)
)
}
groupWithFeedList.addFeed(feedToAdd)
}
}
}

View File

@ -3,7 +3,6 @@ package me.ash.reader.infrastructure.rss
import android.content.Context
import android.util.Log
import com.rometools.rome.feed.synd.SyndEntry
import com.rometools.rome.feed.synd.SyndFeed
import com.rometools.rome.feed.synd.SyndImageImpl
import com.rometools.rome.io.SyndFeedInput
import com.rometools.rome.io.XmlReader
@ -12,18 +11,30 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.ash.reader.domain.model.article.Article
import me.ash.reader.domain.model.feed.Feed
import me.ash.reader.domain.model.feed.FeedWithArticle
import me.ash.reader.domain.repository.FeedDao
import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.html.Readability
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.decodeHTML
import me.ash.reader.ui.ext.extractDomain
import me.ash.reader.ui.ext.htmlFromMarkdown
import me.ash.reader.ui.ext.isFuture
import me.ash.reader.ui.ext.isNostrUri
import me.ash.reader.ui.ext.spacerDollar
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.executeAsync
import rust.nostr.sdk.Alphabet
import rust.nostr.sdk.Client
import rust.nostr.sdk.Coordinate
import rust.nostr.sdk.Event
import rust.nostr.sdk.Kind
import rust.nostr.sdk.KindEnum
import rust.nostr.sdk.SingleLetterTag
import rust.nostr.sdk.TagKind
import java.io.InputStream
import java.time.Instant
import java.util.*
import javax.inject.Inject
@ -39,15 +50,24 @@ class RssHelper @Inject constructor(
@IODispatcher
private val ioDispatcher: CoroutineDispatcher,
private val okHttpClient: OkHttpClient,
private val nostrClient: Client
) {
@Throws(Exception::class)
suspend fun searchFeed(feedLink: String): SyndFeed {
suspend fun searchFeed(feedLink: String): FetchedFeed? {
return withContext(ioDispatcher) {
SyndFeedInput().build(XmlReader(inputStream(okHttpClient, feedLink))).also {
it.icon = SyndImageImpl()
it.icon.link = queryRssIconLink(feedLink)
it.icon.url = it.icon.link
if(feedLink.isNostrUri()) {
NostrFeed.fetchFeedFrom(feedLink, nostrClient)
}
else {
val parsedSyndFeed = SyndFeedInput()
.build(XmlReader(inputStream(okHttpClient, feedLink)))
.also {
it.icon = SyndImageImpl()
it.icon.link = queryRssIconLink(feedLink)
it.icon.url = it.icon.link
}
SyndFeedDelegate(parsedSyndFeed)
}
}
}
@ -126,6 +146,90 @@ class RssHelper @Inject constructor(
)
}
@Throws(Exception::class)
suspend fun syncNostrFeed(
feed: Feed,
latestLink: String?,
preDate: Date = Date()
): FeedWithArticle =
try {
val accountId = context.currentAccountId
Client().use {
val nostrFeed = NostrFeed.fetchFeedFrom(feed.url, it)
val updatedArticles = nostrFeed.getArticles()
.map { buildArticleFromNostrEvent(feed, accountId, it, nostrFeed.getFeedAuthor(), preDate) }
val updatedFeed = feed.copy(
icon = nostrFeed.getIconUrl()
)
return FeedWithArticle(updatedFeed, updatedArticles)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "syncNostrFeedNew[${feed.name}]: ${e.message}")
FeedWithArticle(feed, emptyList())
}
fun buildArticleFromNostrEvent(
feed: Feed,
accountId: Int,
articleEvent: Event,
authorName: String,
// imageUrl: String,
preDate: Date = Date()
): Article {
val articleTitle = articleEvent.tags().find(TagKind.Title)?.content()
val articleImage = articleEvent.tags().find(TagKind.Image)?.content()
val articleSummary = articleEvent.tags().find(TagKind.Summary)?.content()
val timeStamp = articleEvent.tags().find(TagKind.PublishedAt)?.content()?.toLong()
?: Instant.EPOCH.epochSecond
val articleDate = Date.from(Instant.ofEpochSecond(timeStamp)).takeIf { !it.isFuture(preDate) } ?: preDate
val articleNostrAddress =
Coordinate(
Kind.fromEnum(KindEnum.LongFormTextNote),
articleEvent.author(),
articleEvent.tags().find(
TagKind.SingleLetter(
SingleLetterTag.lowercase(Alphabet.D),
),
)?.content().toString(),
).toBech32()
// Highlighter is a service for reading Nostr articles on the web.
//For the external link, we can still give it a value of nostr:<articleAddress>
val externalLink = "nostr:$articleNostrAddress"//""https://highlighter.com/a/$articleNostrAddress"
val articleContent = articleEvent.content()
val parsedContent = htmlFromMarkdown(articleContent)
val actualContent = Readability.parseToText(
parsedContent,
uri = null
)
Log.i(
"RLog",
"Nostr Feed:\n" +
"name: ${feed.name}\n" +
"feedUrl: ${feed.url}\n" +
"url: ${externalLink}\n" +
"title: ${articleTitle}\n" +
"desc: ${articleSummary}\n" +
"content: ${articleContent}\n"
)
return Article(
id = accountId.spacerDollar(UUID.randomUUID().toString()),
accountId = accountId,
feedId = feed.id,
date = articleDate,
title = articleTitle ?: feed.name,
author = authorName,
rawDescription = parsedContent,
shortDescription = articleSummary ?: actualContent.take(110),
fullContent = parsedContent,
img = articleImage,
link = externalLink,
updateAt = articleDate
)
}
fun findThumbnail(syndEntry: SyndEntry): String? {
if (syndEntry.enclosures?.firstOrNull()?.url != null) {
return syndEntry.enclosures.first().url

View File

@ -1,14 +1,18 @@
package me.ash.reader.infrastructure.rss.provider
import android.content.Context
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import me.ash.reader.infrastructure.di.UserAgentInterceptor
import me.ash.reader.infrastructure.di.cachingHttpClient
import okhttp3.OkHttpClient
abstract class ProviderAPI {
abstract class ProviderAPI(context: Context, clientCertificateAlias: String?) {
protected val client: OkHttpClient = cachingHttpClient()
protected val client: OkHttpClient = cachingHttpClient(
context = context,
clientCertificateAlias = clientCertificateAlias,
)
.newBuilder()
.addNetworkInterceptor(UserAgentInterceptor)
.build()

View File

@ -1,5 +1,6 @@
package me.ash.reader.infrastructure.rss.provider.fever
import android.content.Context
import me.ash.reader.infrastructure.exception.FeverAPIException
import me.ash.reader.infrastructure.rss.provider.ProviderAPI
import me.ash.reader.ui.ext.encodeBase64
@ -10,11 +11,13 @@ import okhttp3.executeAsync
import java.util.concurrent.ConcurrentHashMap
class FeverAPI private constructor(
context: Context,
private val serverUrl: String,
private val apiKey: String,
private val httpUsername: String? = null,
private val httpPassword: String? = null,
) : ProviderAPI() {
clientCertificateAlias: String? = null,
) : ProviderAPI(context, clientCertificateAlias) {
private suspend inline fun <reified T> postRequest(query: String?): T {
val response = client.newCall(
@ -104,14 +107,16 @@ class FeverAPI private constructor(
private val instances: ConcurrentHashMap<String, FeverAPI> = ConcurrentHashMap()
fun getInstance(
context: Context,
serverUrl: String,
username: String,
password: String,
httpUsername: String? = null,
httpPassword: String? = null,
clientCertificateAlias: String? = null,
): FeverAPI = "$username:$password".md5().run {
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword") {
FeverAPI(serverUrl, this, httpUsername, httpPassword)
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword$clientCertificateAlias") {
FeverAPI(context, serverUrl, this, httpUsername, httpPassword, clientCertificateAlias)
}
}

View File

@ -1,5 +1,6 @@
package me.ash.reader.infrastructure.rss.provider.greader
import android.content.Context
import me.ash.reader.infrastructure.di.USER_AGENT_STRING
import me.ash.reader.infrastructure.exception.GoogleReaderAPIException
import me.ash.reader.infrastructure.exception.RetryException
@ -10,12 +11,14 @@ import okhttp3.executeAsync
import java.util.concurrent.ConcurrentHashMap
class GoogleReaderAPI private constructor(
context: Context,
private val serverUrl: String,
private val username: String,
private val password: String,
private val httpUsername: String? = null,
private val httpPassword: String? = null,
) : ProviderAPI() {
clientCertificateAlias: String? = null,
) : ProviderAPI(context, clientCertificateAlias) {
enum class Stream(val tag: String) {
ALL_ITEMS("user/-/state/com.google/reading-list"),
@ -350,13 +353,15 @@ class GoogleReaderAPI private constructor(
private val instances: ConcurrentHashMap<String, GoogleReaderAPI> = ConcurrentHashMap()
fun getInstance(
context: Context,
serverUrl: String,
username: String,
password: String,
httpUsername: String? = null,
httpPassword: String? = null,
): GoogleReaderAPI = instances.getOrPut("$serverUrl$username$password$httpUsername$httpPassword") {
GoogleReaderAPI(serverUrl, username, password, httpUsername, httpPassword)
clientCertificateAlias: String? = null
): GoogleReaderAPI = instances.getOrPut("$serverUrl$username$password$httpUsername$httpPassword$clientCertificateAlias") {
GoogleReaderAPI(context, serverUrl, username, password, httpUsername, httpPassword, clientCertificateAlias)
}
fun clearInstance() {

View File

@ -1,5 +1,7 @@
package me.ash.reader.ui.component.base
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@ -22,6 +24,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
@ -46,6 +49,7 @@ fun RYOutlineTextField(
errorMessage: String = "",
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
onClick: (() -> Unit)? = null,
) {
val clipboardManager = LocalClipboardManager.current
val focusRequester = remember { FocusRequester() }
@ -59,7 +63,11 @@ fun RYOutlineTextField(
}
OutlinedTextField(
modifier = Modifier.focusRequester(focusRequester),
modifier = if (onClick != null) {
Modifier.focusProperties { canFocus = false }
} else {
Modifier.focusRequester(focusRequester)
},
colors = TextFieldDefaults.colors(
unfocusedContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent
@ -115,5 +123,18 @@ fun RYOutlineTextField(
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
readOnly = onClick != null,
interactionSource = onClick?.let {
remember { MutableInteractionSource() }
.also { interactionSource ->
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect {
if (it is PressInteraction.Release) {
onClick.invoke()
}
}
}
}
}
)
}

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import me.ash.reader.infrastructure.preference.LocalOpenLink
import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
import me.ash.reader.infrastructure.preference.LocalReadingFonts
import me.ash.reader.infrastructure.preference.LocalReadingImageHorizontalPadding
import me.ash.reader.infrastructure.preference.LocalReadingImageRoundedCorners
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
@ -27,6 +28,8 @@ import me.ash.reader.infrastructure.preference.LocalReadingTextFontSize
import me.ash.reader.infrastructure.preference.LocalReadingTextHorizontalPadding
import me.ash.reader.infrastructure.preference.LocalReadingTextLetterSpacing
import me.ash.reader.infrastructure.preference.LocalReadingTextLineHeight
import me.ash.reader.infrastructure.preference.ReadingFontsPreference
import me.ash.reader.ui.ext.ExternalFonts
import me.ash.reader.ui.ext.openURL
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.theme.palette.alwaysLight
@ -55,6 +58,7 @@ fun RYWebView(
val linkTextColor: Int = MaterialTheme.colorScheme.primary.toArgb()
val subheadBold: Boolean = LocalReadingSubheadBold.current.value
val subheadUpperCase: Boolean = LocalReadingSubheadUpperCase.current.value
val readingFonts = LocalReadingFonts.current
val fontSize: Int = LocalReadingTextFontSize.current
val letterSpacing: Float = LocalReadingTextLetterSpacing.current
val lineHeight: Float = LocalReadingTextLineHeight.current
@ -69,6 +73,7 @@ fun RYWebView(
mutableStateOf(
WebViewLayout.get(
context = context,
readingFontsPreference = readingFonts,
webViewClient = WebViewClient(
context = context,
refererDomain = refererDomain,
@ -81,6 +86,11 @@ fun RYWebView(
)
}
val fontPath =
if (readingFonts is ReadingFontsPreference.External) ExternalFonts.FontType.ReadingFont.toPath(
context
) else null
AndroidView(
modifier = modifier,
factory = { webView },
@ -95,6 +105,7 @@ fun RYWebView(
WebViewHtml.HTML.format(
WebViewStyle.get(
fontSize = fontSize,
fontPath = fontPath,
lineHeight = lineHeight,
letterSpacing = letterSpacing,
textMargin = textMargin,

View File

@ -65,7 +65,8 @@ class WebViewClient(
var imgs = document.getElementsByTagName("img");
for(var i = 0; i < imgs.length; i++){
imgs[i].pos = i;
imgs[i].onclick = function() {
imgs[i].onclick = function(event) {
event.preventDefault();
window.${JavaScriptInterface.NAME}.onImgTagClick(this.src, this.alt);
}
}

View File

@ -5,12 +5,15 @@ import android.content.Context
import android.graphics.Color
import android.webkit.JavascriptInterface
import android.webkit.WebView
import me.ash.reader.infrastructure.preference.BasicFontsPreference
import me.ash.reader.infrastructure.preference.ReadingFontsPreference
object WebViewLayout {
@SuppressLint("SetJavaScriptEnabled")
fun get(
context: Context,
readingFontsPreference: ReadingFontsPreference,
webViewClient: WebViewClient,
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
) = WebView(context).apply {
@ -20,6 +23,20 @@ object WebViewLayout {
isVerticalScrollBarEnabled = true
setBackgroundColor(Color.TRANSPARENT)
with(this.settings) {
standardFontFamily = when (readingFontsPreference) {
ReadingFontsPreference.Cursive -> "cursive"
ReadingFontsPreference.Monospace -> "monospace"
ReadingFontsPreference.SansSerif -> "sans-serif"
ReadingFontsPreference.Serif -> "serif"
ReadingFontsPreference.External -> {
allowFileAccess = true
allowFileAccessFromFileURLs = true
"sans-serif"
}
else -> "sans-serif"
}
domStorageEnabled = true
javaScriptEnabled = true
addJavascriptInterface(object : JavaScriptInterface {

View File

@ -4,8 +4,24 @@ object WebViewStyle {
private fun argbToCssColor(argb: Int): String = String.format("#%06X", 0xFFFFFF and argb)
private fun applyFontFace(
fontPath: String? = null
): String = if (fontPath != null) """
@font-face {
font-family: external;
src: url("file://$fontPath")
}
""".trimIndent() else ""
private fun applyFontFamily(
fontPath: String? = null
): String = if (fontPath != null) """
--font-family: external;
""".trimIndent() else ""
fun get(
fontSize: Int,
fontPath: String? = null,
lineHeight: Float,
letterSpacing: Float,
textMargin: Int,
@ -24,21 +40,22 @@ object WebViewStyle {
selectionTextColor: Int,
selectionBgColor: Int,
): String = """
${applyFontFace(fontPath)}
:root {
/* --font-family: Inter; */
${applyFontFamily(fontPath)}
--font-size: ${fontSize}px;
--line-height: ${lineHeight * 1.5f};
--letter-spacing: ${letterSpacing}px;
--text-margin: ${textMargin}px;
--text-color: ${argbToCssColor(textColor)};
--text-bold: ${if(textBold) "600" else "normal"};
--text-bold: ${if (textBold) "600" else "normal"};
--text-align: ${textAlign};
--bold-text-color: ${argbToCssColor(boldTextColor)};
--link-text-color: ${argbToCssColor(linkTextColor)};
--selection-text-color: ${argbToCssColor(selectionTextColor)};
--selection-bg-color: ${argbToCssColor(selectionBgColor)};
--subhead-bold: ${if(subheadBold) "600" else "normal"};
--subhead-upper-case: ${if(subheadUpperCase) "uppercase" else "none"};
--subhead-bold: ${if (subheadBold) "600" else "normal"};
--subhead-upper-case: ${if (subheadUpperCase) "uppercase" else "none"};
--img-margin: ${imgMargin}px;
--img-border-radius: ${imgBorderRadius}px;
--content-padding;
@ -312,7 +329,7 @@ figure {
text-align: var(--text-align) !important;
margin: 0 !important;
opacity: 0.8 !important;
font-size: 0.8em !important;
font-size: 12px !important;
}
figure * {
@ -323,7 +340,7 @@ figure p,
caption,
figcaption {
opacity: 0.8 !important;
font-size: 0.8em !important;
font-size: 12px !important;
}
hr {

View File

@ -136,7 +136,6 @@ data class DataStoreKey<T>(
// Reading page
const val readingRenderer = "readingRender"
const val readingBionicReading = "readingBionicReading"
const val readingDarkTheme = "readingDarkTheme"
const val readingPageTonalElevation = "readingPageTonalElevation"
const val readingTextFontSize = "readingTextFontSize"
const val readingTextLineHeight = "readingTextLineHeight"
@ -163,6 +162,7 @@ data class DataStoreKey<T>(
const val swipeStartAction = "swipeStartAction"
const val swipeEndAction = "swipeEndAction"
const val markAsReadOnScroll = "markAsReadOnScroll"
const val hideEmptyGroups = "hideEmptyGroups"
const val pullToSwitchArticle = "pullToSwitchArticle"
const val openLink = "openLink"
const val openLinkAppSpecificBrowser = "openLinkAppSpecificBrowser"
@ -212,7 +212,6 @@ data class DataStoreKey<T>(
// Reading page
readingRenderer to DataStoreKey(intPreferencesKey(readingRenderer), Int::class.java),
readingBionicReading to DataStoreKey(booleanPreferencesKey(readingBionicReading), Boolean::class.java),
readingDarkTheme to DataStoreKey(intPreferencesKey(readingDarkTheme), Int::class.java),
readingPageTonalElevation to DataStoreKey(intPreferencesKey(readingPageTonalElevation), Int::class.java),
readingTextFontSize to DataStoreKey(intPreferencesKey(readingTextFontSize), Int::class.java),
readingTextLineHeight to DataStoreKey(floatPreferencesKey(readingTextLineHeight), Float::class.java),
@ -241,6 +240,7 @@ data class DataStoreKey<T>(
booleanPreferencesKey(markAsReadOnScroll),
Boolean::class.java
),
hideEmptyGroups to DataStoreKey(booleanPreferencesKey(hideEmptyGroups), Boolean::class.java),
pullToSwitchArticle to DataStoreKey(booleanPreferencesKey(pullToSwitchArticle), Boolean::class.java),
openLink to DataStoreKey(intPreferencesKey(openLink), Int::class.java),
openLinkAppSpecificBrowser to DataStoreKey(stringPreferencesKey(openLinkAppSpecificBrowser), String::class.java),

View File

@ -2,6 +2,9 @@ package me.ash.reader.ui.ext
import android.text.Html
import android.util.Base64
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.parser.MarkdownParser
import java.math.BigInteger
import java.security.MessageDigest
import java.text.Bidi
@ -18,7 +21,7 @@ fun String.formatUrl(): String {
if (this.startsWith("//")) {
return "https:$this"
}
val regex = Regex("^(https?|ftp|file).*")
val regex = Regex("^(https?|ftp|file|nostr).*")
return if (!regex.matches(this)) {
"https://$this"
} else {
@ -61,3 +64,16 @@ fun String?.extractDomain(): String? {
val domainMatchResult = domainRegex.find(this)
return domainMatchResult?.value
}
private val markDownParser = MarkdownParser(CommonMarkFlavourDescriptor())
fun htmlFromMarkdown(markdown: String): String {
val parsedMarkdown = markDownParser.buildMarkdownTreeFromString(markdown)
val htmlContent = HtmlGenerator(markdown, parsedMarkdown, CommonMarkFlavourDescriptor())
.generateHtml()
return htmlContent
}
const val NOSTR_URI_PREFIX = "nostr:"
fun String.isNostrUri(): Boolean = startsWith(NOSTR_URI_PREFIX) && length > NOSTR_URI_PREFIX.length

View File

@ -20,7 +20,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import me.ash.reader.domain.model.general.Filter
import me.ash.reader.infrastructure.preference.LocalDarkTheme
import me.ash.reader.infrastructure.preference.LocalReadingDarkTheme
import me.ash.reader.ui.ext.animatedComposable
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.findActivity
@ -41,7 +40,6 @@ import me.ash.reader.ui.page.settings.color.DarkThemePage
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
import me.ash.reader.ui.page.settings.color.flow.FlowPageStylePage
import me.ash.reader.ui.page.settings.color.reading.BionicReadingPage
import me.ash.reader.ui.page.settings.color.reading.ReadingDarkThemePage
import me.ash.reader.ui.page.settings.color.reading.ReadingImagePage
import me.ash.reader.ui.page.settings.color.reading.ReadingStylePage
import me.ash.reader.ui.page.settings.color.reading.ReadingTextPage
@ -132,15 +130,8 @@ fun HomeEntry(
}
}
val useDarkTheme = if (isReadingPage) {
LocalReadingDarkTheme.current.isDarkTheme()
} else {
LocalDarkTheme.current.isDarkTheme()
}
AppTheme(
useDarkTheme = if (isReadingPage) LocalReadingDarkTheme.current.isDarkTheme()
else LocalDarkTheme.current.isDarkTheme()
useDarkTheme = LocalDarkTheme.current.isDarkTheme()
) {
NavHost(
@ -210,9 +201,6 @@ fun HomeEntry(
animatedComposable(route = RouteName.READING_BIONIC_READING) {
BionicReadingPage(navController)
}
animatedComposable(route = RouteName.READING_DARK_THEME) {
ReadingDarkThemePage(navController)
}
animatedComposable(route = RouteName.READING_PAGE_TITLE) {
ReadingTitlePage(navController)
}

View File

@ -46,11 +46,11 @@ class HomeViewModel @Inject constructor(
private val _filterUiState = MutableStateFlow(FilterState())
val filterUiState = _filterUiState.asStateFlow()
val syncWorkLiveData = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_NAME)
val syncWorkLiveData = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_TAG)
fun sync() {
applicationScope.launch(ioDispatcher) {
rssService.get().doSync()
rssService.get().doSyncOneTime()
}
}

View File

@ -52,7 +52,7 @@ fun FeedItem(
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(if (isEnded()) ShapeBottom32 else RectangleShape)
.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.combinedClickable(
onClick = {
onClick()
@ -88,9 +88,7 @@ fun FeedItem(
}
if ((feed.important ?: 0) != 0) {
Badge(
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
alpha = badgeAlpha
),
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(

View File

@ -66,6 +66,7 @@ import me.ash.reader.infrastructure.preference.LocalFeedsFilterBarTonalElevation
import me.ash.reader.infrastructure.preference.LocalFeedsGroupListExpand
import me.ash.reader.infrastructure.preference.LocalFeedsGroupListTonalElevation
import me.ash.reader.infrastructure.preference.LocalFeedsTopBarTonalElevation
import me.ash.reader.infrastructure.preference.LocalHideEmptyGroups
import me.ash.reader.infrastructure.preference.LocalNewVersionNumber
import me.ash.reader.infrastructure.preference.LocalSkipVersionNumber
import me.ash.reader.ui.component.FilterBar
@ -198,9 +199,11 @@ fun FeedsPage(
feedsViewModel.fetchAccount()
}
val hideEmptyGroups = LocalHideEmptyGroups.current.value
LaunchedEffect(filterUiState, isSyncing) {
snapshotFlow { filterUiState }.collect {
feedsViewModel.pullFeeds(it)
feedsViewModel.pullFeeds(it, hideEmptyGroups)
}
}
@ -210,7 +213,7 @@ fun FeedsPage(
RYScaffold(
topBarTonalElevation = topBarTonalElevation.value.dp,
containerTonalElevation = groupListTonalElevation.value.dp,
// containerTonalElevation = groupListTonalElevation.value.dp,
topBar = {
TopAppBar(
modifier = Modifier.clickable(

View File

@ -49,7 +49,7 @@ class FeedsViewModel @Inject constructor(
}
@OptIn(ExperimentalCoroutinesApi::class)
fun pullFeeds(filterState: FilterState) {
fun pullFeeds(filterState: FilterState, hideEmptyGroups: Boolean) {
val isStarred = filterState.filter.isStarred()
val isUnread = filterState.filter.isUnread()
_feedsUiState.update {
@ -77,7 +77,7 @@ class FeedsViewModel @Inject constructor(
while (groupIterator.hasNext()) {
val groupWithFeed = groupIterator.next()
val groupImportant = importantMap[groupWithFeed.group.id] ?: 0
if ((isStarred || isUnread) && groupImportant == 0) {
if (hideEmptyGroups && (isStarred || isUnread) && groupImportant == 0) {
groupIterator.remove()
continue
}
@ -87,7 +87,7 @@ class FeedsViewModel @Inject constructor(
val feed = feedIterator.next()
val feedImportant = importantMap[feed.id] ?: 0
groupWithFeed.group.feeds++
if ((isStarred || isUnread) && feedImportant == 0) {
if (hideEmptyGroups && (isStarred || isUnread) && feedImportant == 0) {
feedIterator.remove()
continue
}

View File

@ -51,7 +51,7 @@ fun GroupItem(
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(if (isExpanded() && !roundedBottomCorner()) ShapeTop32 else Shape32)
.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.combinedClickable(
onClick = {
groupOnClick()
@ -84,7 +84,7 @@ fun GroupItem(
.padding(end = 20.dp)
.size(24.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceTint.copy(alpha = indicatorAlpha))
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
.clickable { onExpanded() },
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,

View File

@ -33,6 +33,8 @@ import me.ash.reader.R
import me.ash.reader.domain.model.account.Account
import me.ash.reader.ui.component.base.RYDialog
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.theme.palette.FixedColorRoles
import me.ash.reader.ui.theme.palette.LocalFixedColorRoles
import me.ash.reader.ui.theme.palette.alwaysLight
@OptIn(ExperimentalLayoutApi::class)
@ -75,20 +77,21 @@ fun AccountsTab(
}
.padding(8.dp),
) {
val selected = account.id == context.currentAccountId
Box(
modifier = Modifier
.size(52.dp)
.clip(CircleShape)
.background(
if (account.id == context.currentAccountId) {
MaterialTheme.colorScheme.primaryContainer alwaysLight true
if (selected) {
LocalFixedColorRoles.current.primaryFixed
} else {
MaterialTheme.colorScheme.surfaceDim alwaysLight true
MaterialTheme.colorScheme.surfaceContainerHighest
}
),
contentAlignment = Alignment.Center,
) {
AccountTypeIcon(account = account)
AccountTypeIcon(account = account, selected = selected)
}
Text(
modifier = Modifier
@ -125,15 +128,19 @@ fun AccountsTab(
@Composable
fun AccountTypeIcon(
account: Account,
selected: Boolean
) {
val icon = account.type.toIcon().takeIf { it is ImageVector }?.let { it as ImageVector }
val iconPainter = account.type.toIcon().takeIf { it is Painter }?.let { it as Painter }
val contentColor =
if (selected) LocalFixedColorRoles.current.onPrimaryFixed else MaterialTheme.colorScheme.onSurfaceVariant
if (icon != null) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = icon,
contentDescription = account.name,
tint = MaterialTheme.colorScheme.onSurface alwaysLight true
tint = contentColor
)
} else {
iconPainter?.let {
@ -141,7 +148,7 @@ fun AccountTypeIcon(
modifier = Modifier.size(24.dp),
painter = it,
contentDescription = account.name,
tint = MaterialTheme.colorScheme.onSurface alwaysLight true
tint = contentColor
)
}
}

View File

@ -9,7 +9,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.animation.with
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CreateNewFolder
@ -81,7 +80,7 @@ fun SubscribeDialog(
icon = {
FeedIcon(
feedName = subscribeUiState.searchedFeed?.title ?: stringResource(R.string.subscribe),
iconUrl = subscribeUiState.searchedFeed?.icon?.url,
iconUrl = subscribeUiState.searchedFeed?.getIconUrl(),
placeholderIcon = Icons.Rounded.RssFeed,
)
},

View File

@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rometools.rome.feed.synd.SyndFeed
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -20,6 +19,7 @@ import me.ash.reader.domain.service.OpmlService
import me.ash.reader.domain.service.RssService
import me.ash.reader.infrastructure.android.AndroidStringsHelper
import me.ash.reader.infrastructure.di.ApplicationScope
import me.ash.reader.infrastructure.rss.FetchedFeed
import me.ash.reader.infrastructure.rss.RssHelper
import me.ash.reader.ui.ext.formatUrl
import java.io.InputStream
@ -59,7 +59,7 @@ class SubscribeViewModel @Inject constructor(
fun importFromInputStream(inputStream: InputStream) {
applicationScope.launch {
opmlService.saveToDatabase(inputStream)
rssService.get().doSync()
rssService.get().doSyncOneTime()
}
}
@ -245,7 +245,7 @@ data class SubscribeUiState(
val errorMessage: String = "",
val linkContent: String = "",
val lockLinkInput: Boolean = false,
val searchedFeed: SyndFeed? = null,
val searchedFeed: FetchedFeed? = null,
val allowNotificationPreset: Boolean = false,
val parseFullContentPreset: Boolean = false,
val selectedGroupId: String = "",

View File

@ -1,6 +1,8 @@
package me.ash.reader.ui.page.home.flow
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Spacer
@ -19,6 +21,7 @@ import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
@ -26,21 +29,29 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.eventFlow
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.work.WorkInfo
@ -67,6 +78,8 @@ import me.ash.reader.ui.component.base.RYExtensibleVisibility
import me.ash.reader.ui.component.base.RYScaffold
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.motion.materialSharedAxisYIn
import me.ash.reader.ui.motion.materialSharedAxisYOut
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.HomeViewModel
@ -80,10 +93,10 @@ fun FlowPage(
homeViewModel: HomeViewModel,
) {
val keyboardController = LocalSoftwareKeyboardController.current
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
val articleListFeedIcon = LocalFlowArticleListFeedIcon.current
val articleListDateStickyHeader = LocalFlowArticleListDateStickyHeader.current
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
val filterBarStyle = LocalFlowFilterBarStyle.current
val filterBarFilled = LocalFlowFilterBarFilled.current
val filterBarPadding = LocalFlowFilterBarPadding.current
@ -99,6 +112,18 @@ fun FlowPage(
val listState =
if (pagingItems.itemCount > 0) flowUiState.listState else rememberLazyListState()
val isTopBarElevated = topBarTonalElevation.value > 0
val isScrolled by remember(listState) { derivedStateOf { listState.firstVisibleItemIndex != 0 } }
val topBarContainerColor by animateColorAsState(with(MaterialTheme.colorScheme) {
if (isScrolled && isTopBarElevated) surfaceContainer else surface
}, label = "")
val titleText = when {
filterUiState.group != null -> filterUiState.group.name
filterUiState.feed != null -> filterUiState.feed.name
else -> filterUiState.filter.toName()
}
if (markAsReadOnScroll) {
LaunchedEffect(listState.isScrollInProgress) {
if (!listState.isScrollInProgress) {
@ -127,7 +152,7 @@ fun FlowPage(
val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
var markAsRead by remember { mutableStateOf(false) }
var onSearch by remember { mutableStateOf(false) }
var onSearch by rememberSaveable { mutableStateOf(false) }
val owner = LocalLifecycleOwner.current
@ -148,6 +173,13 @@ fun FlowPage(
}
DisposableEffect(owner) {
scope.launch {
owner.lifecycle.eventFlow.collect {
if (it == Lifecycle.Event.ON_PAUSE) {
flowViewModel.commitDiff()
}
}
}
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
workInfoList.let {
isSyncing = it.any { workInfo -> workInfo.state == WorkInfo.State.RUNNING }
@ -206,15 +238,10 @@ fun FlowPage(
}
LaunchedEffect(onSearch) {
snapshotFlow { onSearch }.collect {
if (it) {
delay(100) // ???
focusRequester.requestFocus()
} else {
keyboardController?.hide()
if (homeUiState.searchContent.isNotBlank()) {
homeViewModel.inputSearchContent("")
}
if (!onSearch) {
keyboardController?.hide()
if (homeUiState.searchContent.isNotBlank()) {
homeViewModel.inputSearchContent("")
}
}
}
@ -232,7 +259,6 @@ fun FlowPage(
}
RYScaffold(
topBarTonalElevation = topBarTonalElevation.value.dp,
containerTonalElevation = articleListTonalElevation.value.dp,
topBar = {
TopAppBar(
@ -247,7 +273,20 @@ fun FlowPage(
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
title = {},
title = {
AnimatedVisibility(
isScrolled,
enter = materialSharedAxisYIn(initialOffsetY = { it / 4 }),
exit = materialSharedAxisYOut(targetOffsetY = { it / 4 })
) {
Text(
text = titleText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
)
}
},
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
@ -298,12 +337,14 @@ fun FlowPage(
flowUiState.listState.animateScrollToItem(0)
}
onSearch = !onSearch
if (onSearch) {
delay(100)
focusRequester.requestFocus()
}
}
}
}, colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
containerColor = topBarContainerColor
)
)
},
@ -320,11 +361,7 @@ fun FlowPage(
item {
DisplayText(
modifier = Modifier.padding(start = if (articleListFeedIcon.value) 30.dp else 0.dp),
text = when {
filterUiState.group != null -> filterUiState.group.name
filterUiState.feed != null -> filterUiState.feed.name
else -> filterUiState.filter.toName()
},
text = titleText,
desc = "",
)
RYExtensibleVisibility(visible = markAsRead) {
@ -387,7 +424,6 @@ fun FlowPage(
articleListTonalElevation = articleListTonalElevation.value,
isSwipeEnabled = { listState.isScrollInProgress },
onClick = {
onSearch = false
navController.navigate("${RouteName.READING}/${it.article.id}") {
launchSingleTop = true
}

View File

@ -34,7 +34,7 @@ class FlowViewModel @Inject constructor(
fun sync() {
applicationScope.launch(ioDispatcher) {
rssService.get().doSync()
rssService.get().doSyncOneTime()
}
}

View File

@ -2,10 +2,7 @@ package me.ash.reader.ui.page.home.reading
import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
@ -33,15 +30,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
import me.ash.reader.infrastructure.preference.ReadingPageTonalElevationPreference
import me.ash.reader.infrastructure.preference.ReadingRendererPreference
import me.ash.reader.ui.component.base.CanBeDisabledIconButton
import me.ash.reader.ui.component.base.RYExtensibleVisibility
import me.ash.reader.ui.component.webview.BionicReadingIcon
@Composable
@ -60,6 +56,7 @@ fun BottomBar(
onReadAloud: () -> Unit = {},
) {
val tonalElevation = LocalReadingPageTonalElevation.current
val isOutlined = tonalElevation == ReadingPageTonalElevationPreference.Outlined
val renderer = LocalReadingRenderer.current
Box(
@ -70,16 +67,20 @@ fun BottomBar(
) {
AnimatedVisibility(
visible = isShow,
enter = expandVertically(),
exit = shrinkVertically()
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top)
) {
val view = LocalView.current
Column {
HorizontalDivider(
color = MaterialTheme.colorScheme.surfaceContainerHighest,
thickness = 0.5f.dp
)
Surface() {
if (isOutlined) {
HorizontalDivider(
color = MaterialTheme.colorScheme.surfaceContainerHighest,
thickness = 0.5f.dp
)
}
Surface(
color = MaterialTheme.colorScheme.run { if (isOutlined) surface else surfaceContainer }
) {
// TODO: Component styles await refactoring
Row(
modifier = Modifier

View File

@ -5,9 +5,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.ExperimentalMaterialApi
@ -16,9 +14,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -28,11 +24,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.sp
@ -40,7 +34,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
@ -123,7 +116,7 @@ fun ReadingPage(
TopBar(
navController = navController,
isShow = isShowToolBar,
showDivider = showTopDivider,
isScrolled = showTopDivider,
title = readerState.title,
link = readerState.link,
onClick = { bringToTop = true },
@ -194,7 +187,7 @@ fun ReadingPage(
showTopDivider = snapshotFlow {
scrollState.value != 0 || listState.firstVisibleItemIndex != 0
scrollState.value >= 120 || listState.firstVisibleItemIndex != 0
}.collectAsStateValue(initial = false)
CompositionLocalProvider(

View File

@ -22,6 +22,7 @@ import me.ash.reader.infrastructure.di.ApplicationScope
import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.rss.RssHelper
import me.ash.reader.infrastructure.storage.AndroidImageDownloader
import me.ash.reader.ui.ext.isNostrUri
import java.util.Date
import javax.inject.Inject
@ -47,6 +48,8 @@ class ReadingViewModel @Inject constructor(
private val currentFeed: Feed?
get() = readingUiState.value.articleWithFeed?.feed
private var initialArticleItems: List<ArticleFlowItem> = emptyList()
fun initData(articleId: String) {
setLoading()
viewModelScope.launch(ioDispatcher) {
@ -96,10 +99,14 @@ class ReadingViewModel @Inject constructor(
private suspend fun internalRenderFullContent() {
setLoading()
runCatching {
rssHelper.parseFullContent(
currentArticle?.link ?: "",
currentArticle?.title ?: ""
)
if (currentArticle?.link?.isNostrUri() == true) {
currentArticle?.fullContent.toString()
} else {
rssHelper.parseFullContent(
currentArticle?.link ?: "",
currentArticle?.title ?: ""
)
}
}.onSuccess { content ->
_readerState.update { it.copy(content = ReaderState.FullContent(content = content)) }
}.onFailure { th ->
@ -146,7 +153,11 @@ class ReadingViewModel @Inject constructor(
}
fun prefetchArticleId(pagingItems: ItemSnapshotList<ArticleFlowItem>) {
val items = pagingItems.items
if (initialArticleItems.isEmpty()) {
initialArticleItems = pagingItems.items
}
val items = initialArticleItems
val currentId = currentArticle?.id
val index = items.indexOfFirst { item ->
item is ArticleFlowItem.Article && item.articleWithFeed.article.id == currentId

View File

@ -1,6 +1,9 @@
package me.ash.reader.ui.page.home.reading
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
@ -15,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Share
@ -27,20 +29,22 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
import me.ash.reader.infrastructure.preference.LocalSharedContent
import me.ash.reader.infrastructure.preference.ReadingPageTonalElevationPreference
import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.common.RouteName
@OptIn(ExperimentalMaterial3Api::class)
@ -48,7 +52,7 @@ import me.ash.reader.ui.page.common.RouteName
fun TopBar(
navController: NavHostController,
isShow: Boolean,
showDivider: Boolean = false,
isScrolled: Boolean = false,
title: String? = "",
link: String? = "",
onClick: (() -> Unit)? = null,
@ -56,6 +60,13 @@ fun TopBar(
) {
val context = LocalContext.current
val sharedContent = LocalSharedContent.current
val isOutlined =
LocalReadingPageTonalElevation.current == ReadingPageTonalElevationPreference.Outlined
val containerColor by animateColorAsState(with(MaterialTheme.colorScheme) {
if (isOutlined || !isScrolled) surface else surfaceContainer
}, label = "", animationSpec = spring(stiffness = Spring.StiffnessMediumLow))
Box(
modifier = Modifier
@ -63,29 +74,30 @@ fun TopBar(
.zIndex(1f),
contentAlignment = Alignment.TopCenter
) {
Column(modifier = if (onClick == null) Modifier else Modifier.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
Column(
modifier = Modifier.drawBehind { drawRect(containerColor) }
) {
Surface(
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(
WindowInsets.statusBars
.asPaddingValues()
.calculateTopPadding()
)
) {}
),
)
AnimatedVisibility(
visible = isShow,
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top)
enter = expandVertically(expandFrom = Alignment.Bottom),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom)
) {
TopAppBar(
title = {},
modifier = Modifier,
modifier = if (onClick == null) Modifier else Modifier.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
windowInsets = WindowInsets(0.dp),
navigationIcon = {
FeedbackIconButton(
@ -95,7 +107,8 @@ fun TopBar(
) {
onClose()
}
}, actions = {
},
actions = {
FeedbackIconButton(
modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Palette,
@ -114,10 +127,11 @@ fun TopBar(
) {
sharedContent.share(context, title, link)
}
}, colors = TopAppBarDefaults.topAppBarColors()
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
)
}
if (showDivider) {
if (isOutlined && isScrolled) {
HorizontalDivider(
color = MaterialTheme.colorScheme.surfaceContainerHighest,
thickness = 0.5f.dp

View File

@ -1,5 +1,7 @@
package me.ash.reader.ui.page.settings.accounts.addition
import android.app.Activity
import android.security.KeyChain
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@ -55,6 +57,7 @@ fun AddFeverAccountDialog(
var feverServerUrl by rememberSaveable { mutableStateOf("") }
var feverUsername by rememberSaveable { mutableStateOf("") }
var feverPassword by rememberSaveable { mutableStateOf("") }
var feverClientCertificateAlias by rememberSaveable { mutableStateOf("") }
RYDialog(
modifier = Modifier.padding(horizontal = 44.dp),
@ -121,6 +124,19 @@ fun AddFeverAccountDialog(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
)
Spacer(modifier = Modifier.height(10.dp))
RYOutlineTextField(
requestFocus = false,
readOnly = accountUiState.isLoading,
value = feverClientCertificateAlias,
onValueChange = { feverClientCertificateAlias = it },
label = stringResource(R.string.client_certificate),
onClick = {
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
feverClientCertificateAlias = alias ?: ""
}, null, null, null, null)
}
)
Spacer(modifier = Modifier.height(10.dp))
}
},
confirmButton = {
@ -138,6 +154,7 @@ fun AddFeverAccountDialog(
serverUrl = feverServerUrl,
username = feverUsername,
password = feverPassword,
clientCertificateAlias = feverClientCertificateAlias,
).toString(),
)) { account, exception ->
if (account == null) {

View File

@ -1,5 +1,7 @@
package me.ash.reader.ui.page.settings.accounts.addition
import android.app.Activity
import android.security.KeyChain
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@ -55,6 +57,7 @@ fun AddFreshRSSAccountDialog(
var freshRSSServerUrl by rememberSaveable { mutableStateOf("") }
var freshRSSUsername by rememberSaveable { mutableStateOf("") }
var freshRSSPassword by rememberSaveable { mutableStateOf("") }
var freshRSSClientCertificateAlias by rememberSaveable { mutableStateOf("") }
RYDialog(
modifier = Modifier.padding(horizontal = 44.dp),
@ -122,6 +125,19 @@ fun AddFreshRSSAccountDialog(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
)
Spacer(modifier = Modifier.height(10.dp))
RYOutlineTextField(
requestFocus = false,
readOnly = accountUiState.isLoading,
value = freshRSSClientCertificateAlias,
onValueChange = { freshRSSClientCertificateAlias = it },
label = stringResource(R.string.client_certificate),
onClick = {
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
freshRSSClientCertificateAlias = alias ?: ""
}, null, null, null, null)
}
)
Spacer(modifier = Modifier.height(10.dp))
}
},
confirmButton = {
@ -142,6 +158,7 @@ fun AddFreshRSSAccountDialog(
serverUrl = freshRSSServerUrl,
username = freshRSSUsername,
password = freshRSSPassword,
clientCertificateAlias = freshRSSClientCertificateAlias,
).toString(),
)) { account, exception ->
if (account == null) {

View File

@ -1,5 +1,7 @@
package me.ash.reader.ui.page.settings.accounts.addition
import android.app.Activity
import android.security.KeyChain
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@ -56,6 +58,7 @@ fun AddGoogleReaderAccountDialog(
var googleReaderServerUrl by rememberSaveable { mutableStateOf("") }
var googleReaderUsername by rememberSaveable { mutableStateOf("") }
var googleReaderPassword by rememberSaveable { mutableStateOf("") }
var googleReaderClientCertificateAlias by rememberSaveable { mutableStateOf("") }
RYDialog(
modifier = Modifier.padding(horizontal = 44.dp),
@ -123,6 +126,19 @@ fun AddGoogleReaderAccountDialog(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
)
Spacer(modifier = Modifier.height(10.dp))
RYOutlineTextField(
requestFocus = false,
readOnly = accountUiState.isLoading,
value = googleReaderClientCertificateAlias,
onValueChange = { googleReaderClientCertificateAlias = it },
label = stringResource(R.string.client_certificate),
onClick = {
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
googleReaderClientCertificateAlias = alias ?: ""
}, null, null, null, null)
}
)
Spacer(modifier = Modifier.height(10.dp))
}
},
confirmButton = {
@ -143,6 +159,7 @@ fun AddGoogleReaderAccountDialog(
serverUrl = googleReaderServerUrl,
username = googleReaderUsername,
password = googleReaderPassword,
clientCertificateAlias = googleReaderClientCertificateAlias.takeIf { it.isNotEmpty() },
).toString(),
)) { account, exception ->
if (account == null) {

View File

@ -1,7 +1,16 @@
package me.ash.reader.ui.page.settings.accounts.connection
import android.app.Activity
import android.security.KeyChain
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R
@ -17,6 +26,8 @@ fun LazyItemScope.FeverConnection(
account: Account,
viewModel: AccountViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val securityKey by remember {
derivedStateOf { FeverSecurityKey(account.securityKey) }
}
@ -56,6 +67,16 @@ fun LazyItemScope.FeverConnection(
passwordDialogVisible = true
},
) {}
SettingItem(
title = stringResource(R.string.client_certificate),
desc = securityKey.clientCertificateAlias,
onClick = {
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
securityKey.clientCertificateAlias = alias
save(account, viewModel, securityKey)
}, null, null, null, null)
},
) {}
TextFieldDialog(
visible = serverUrlDialogVisible,

View File

@ -1,7 +1,16 @@
package me.ash.reader.ui.page.settings.accounts.connection
import android.app.Activity
import android.security.KeyChain
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R
@ -17,6 +26,8 @@ fun LazyItemScope.FreshRSSConnection(
account: Account,
viewModel: AccountViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val securityKey by remember {
derivedStateOf { FreshRSSSecurityKey(account.securityKey) }
}
@ -56,6 +67,16 @@ fun LazyItemScope.FreshRSSConnection(
passwordDialogVisible = true
},
) {}
SettingItem(
title = stringResource(R.string.client_certificate),
desc = securityKey.clientCertificateAlias,
onClick = {
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
securityKey.clientCertificateAlias = alias
save(account, viewModel, securityKey)
}, null, null, null, null)
},
) {}
TextFieldDialog(
visible = serverUrlDialogVisible,

View File

@ -1,7 +1,16 @@
package me.ash.reader.ui.page.settings.accounts.connection
import android.app.Activity
import android.security.KeyChain
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R
@ -17,6 +26,8 @@ fun LazyItemScope.GoogleReaderConnection(
account: Account,
viewModel: AccountViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val securityKey by remember {
derivedStateOf { GoogleReaderSecurityKey(account.securityKey) }
}
@ -56,6 +67,16 @@ fun LazyItemScope.GoogleReaderConnection(
passwordDialogVisible = true
},
) {}
SettingItem(
title = stringResource(R.string.client_certificate),
desc = securityKey.clientCertificateAlias,
onClick = {
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
securityKey.clientCertificateAlias = alias
save(account, viewModel, securityKey)
}, null, null, null, null)
},
) {}
TextFieldDialog(
visible = serverUrlDialogVisible,

View File

@ -62,9 +62,7 @@ fun FeedsPagePreview(
modifier = Modifier
.animateContentSize()
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
groupListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp)
)
) {

View File

@ -123,7 +123,7 @@ fun FeedsPageStylePage(
(!groupListExpand).put(context, scope)
}
}
SettingItem(
/* SettingItem(
title = stringResource(R.string.tonal_elevation),
desc = "${groupListTonalElevation.value}dp",
onClick = {
@ -131,7 +131,7 @@ fun FeedsPageStylePage(
},
) {}
Tips(text = stringResource(R.string.tips_group_list_tonal_elevation))
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(24.dp))*/
}
// Filter Bar
@ -242,7 +242,7 @@ fun FeedsPageStylePage(
topBarTonalElevationDialogVisible = false
}
RadioDialog(
/* RadioDialog(
visible = groupListTonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation),
options = FeedsGroupListTonalElevationPreference.values.map {
@ -255,5 +255,5 @@ fun FeedsPageStylePage(
}
) {
groupListTonalElevationDialogVisible = false
}
}*/
}

View File

@ -12,13 +12,16 @@ import androidx.compose.material.icons.rounded.DoneAll
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.R
import me.ash.reader.domain.model.article.Article
import me.ash.reader.domain.model.article.ArticleWithFeed
@ -55,8 +58,19 @@ fun FlowPagePreview(
shape = RoundedCornerShape(24.dp)
)
) {
val preview = generateArticleWithFeedPreview()
val feed = preview.feed
val article = preview.article
TopAppBar(
title = {},
title = {
Text(
text = feed.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
)
},
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
@ -83,10 +97,6 @@ fun FlowPagePreview(
)
Spacer(modifier = Modifier.height(12.dp))
val preview = generateArticleWithFeedPreview()
val feed = preview.feed
val article = preview.article
ArticleItem(
modifier = Modifier,
feedName = feed.name,

View File

@ -115,7 +115,7 @@ fun FlowPageStylePage(
topBarTonalElevationDialogVisible = true
},
) {}
// Tips(text = stringResource(R.string.tips_top_bar_tonal_elevation))
Tips(text = stringResource(R.string.tips_top_bar_tonal_elevation))
Spacer(modifier = Modifier.height(24.dp))
}

View File

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

View File

@ -43,7 +43,6 @@ import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
import me.ash.reader.infrastructure.preference.LocalReadingDarkTheme
import me.ash.reader.infrastructure.preference.LocalReadingFonts
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
@ -76,8 +75,6 @@ fun ReadingStylePage(
val scope = rememberCoroutineScope()
val readingTheme = LocalReadingTheme.current
val darkTheme = LocalReadingDarkTheme.current
val darkThemeNot = !darkTheme
val tonalElevation = LocalReadingPageTonalElevation.current
val fonts = LocalReadingFonts.current
val autoHideToolbar = LocalReadingAutoHideToolbar.current
@ -89,12 +86,17 @@ fun ReadingStylePage(
var rendererDialogVisible by remember { mutableStateOf(false) }
var fontsDialogVisible by remember { mutableStateOf(false) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let {
ExternalFonts(context, it, ExternalFonts.FontType.ReadingFont).copyToInternalStorage()
ReadingFontsPreference.External.put(context, scope)
} ?: context.showToast("Cannot get activity result with launcher")
}
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let {
ExternalFonts(
context,
it,
ExternalFonts.FontType.ReadingFont
).copyToInternalStorage()
ReadingFontsPreference.External.put(context, scope)
} ?: context.showToast("Cannot get activity result with launcher")
}
RYScaffold(
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
@ -115,7 +117,8 @@ fun ReadingStylePage(
// Preview
item {
Row(modifier = Modifier.horizontalScroll(rememberScrollState())
Row(
modifier = Modifier.horizontalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.width(24.dp))
ReadingThemePreference.values.map {
@ -187,22 +190,6 @@ fun ReadingStylePage(
desc = fonts.toDesc(context),
onClick = { fontsDialogVisible = true },
) {}
SettingItem(
title = stringResource(R.string.dark_reading_theme),
desc = darkTheme.toDesc(context),
separatedActions = true,
onClick = {
navController.navigate(RouteName.READING_DARK_THEME) {
launchSingleTop = true
}
},
) {
RYSwitch(
activated = darkTheme.isDarkTheme()
) {
darkThemeNot.put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.auto_hide_toolbars),
onClick = {
@ -224,6 +211,10 @@ fun ReadingStylePage(
onClick = { pullToSwitchArticle.toggle(context, scope) }) {
RYSwitch(activated = pullToSwitchArticle.value)
}
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.toolbars)
)
SettingItem(
title = stringResource(R.string.tonal_elevation),
desc = "${tonalElevation.value}dp",
@ -231,6 +222,7 @@ fun ReadingStylePage(
tonalElevationDialogVisible = true
},
) {}
Spacer(modifier = Modifier.height(24.dp))
}
@ -291,7 +283,7 @@ fun ReadingStylePage(
}
)
/* RadioDialog(
RadioDialog(
visible = tonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation),
options = ReadingPageTonalElevationPreference.values.map {
@ -304,7 +296,7 @@ fun ReadingStylePage(
}
) {
tonalElevationDialogVisible = false
}*/
}
RadioDialog(
visible = rendererDialogVisible,

View File

@ -26,6 +26,7 @@ import me.ash.reader.infrastructure.preference.InitialFilterPreference
import me.ash.reader.infrastructure.preference.InitialPagePreference
import me.ash.reader.infrastructure.preference.LocalArticleListSwipeEndAction
import me.ash.reader.infrastructure.preference.LocalArticleListSwipeStartAction
import me.ash.reader.infrastructure.preference.LocalHideEmptyGroups
import me.ash.reader.infrastructure.preference.LocalInitialFilter
import me.ash.reader.infrastructure.preference.LocalInitialPage
import me.ash.reader.infrastructure.preference.LocalMarkAsReadOnScroll
@ -58,6 +59,7 @@ fun InteractionPage(
val swipeToStartAction = LocalArticleListSwipeStartAction.current
val swipeToEndAction = LocalArticleListSwipeEndAction.current
val markAsReadOnScroll = LocalMarkAsReadOnScroll.current
val hideEmptyGroups = LocalHideEmptyGroups.current
val pullToSwitchArticle = LocalPullToSwitchArticle.current
val openLink = LocalOpenLink.current
val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current
@ -112,6 +114,22 @@ fun InteractionPage(
) {}
Spacer(modifier = Modifier.height(24.dp))
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.feeds_page),
)
SettingItem(
title = stringResource(R.string.hide_empty_groups),
onClick = {
hideEmptyGroups.toggle(context, scope)
},
) {
RYSwitch(activated = hideEmptyGroups.value) {
hideEmptyGroups.toggle(context, scope)
}
}
Spacer(modifier = Modifier.height(24.dp))
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.article_list),

View File

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

View File

@ -85,6 +85,8 @@ fun TipsAndSupportPage(
targetValue = pressAMP,
animationSpec = tween()
)
var showSponsorDialog by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
currentVersion = context.getCurrentVersion().toString()
@ -210,7 +212,7 @@ fun TipsAndSupportPage(
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
view.playSoundEffect(SoundEffectConstants.CLICK)
context.showToast(context.getString(R.string.coming_soon))
showSponsorDialog = true
})
Spacer(modifier = Modifier.width(16.dp))
@ -250,6 +252,9 @@ fun TipsAndSupportPage(
)
UpdateDialog()
if (showSponsorDialog) {
SponsorDialog { showSponsorDialog = false }
}
}
@Immutable

View File

@ -11,6 +11,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import me.ash.reader.infrastructure.preference.LocalBasicFonts
import me.ash.reader.infrastructure.preference.LocalThemeIndex
import me.ash.reader.ui.theme.palette.FixedColorRoles
import me.ash.reader.ui.theme.palette.LocalFixedColorRoles
import me.ash.reader.ui.theme.palette.LocalTonalPalettes
import me.ash.reader.ui.theme.palette.TonalPalettes
import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
@ -59,15 +61,21 @@ fun AppTheme(
LocalTonalPalettes provides tonalPalettes.apply { Preparing() },
LocalTextStyle provides LocalTextStyle.current.applyTextDirection()
) {
MaterialTheme(
colorScheme =
if (useDarkTheme) dynamicDarkColorScheme()
else dynamicLightColorScheme(),
typography = LocalBasicFonts.current.asTypography(LocalContext.current)
.applyTextDirection(),
shapes = Shapes,
content = content,
)
val lightColors = dynamicLightColorScheme()
val darkColors = dynamicDarkColorScheme()
CompositionLocalProvider(
LocalFixedColorRoles provides FixedColorRoles.fromColorSchemes(
lightColors, darkColors
)
) {
MaterialTheme(
colorScheme = if (useDarkTheme) darkColors else lightColors,
typography = LocalBasicFonts.current.asTypography(LocalContext.current)
.applyTextDirection(),
shapes = Shapes,
content = content,
)
}
}
}
}

View File

@ -52,7 +52,7 @@ fun dynamicLightColorScheme(): ColorScheme {
@Composable
fun dynamicDarkColorScheme(): ColorScheme {
val palettes = LocalTonalPalettes.current
val amoledDarkTheme = LocalAmoledDarkTheme.current
val useAmoledDarkTheme = LocalAmoledDarkTheme.current.value
return darkColorScheme(
primary = palettes primary 80,
@ -70,7 +70,7 @@ fun dynamicDarkColorScheme(): ColorScheme {
onTertiaryContainer = palettes tertiary 90,
background = palettes neutral 10,
onBackground = palettes neutral 90,
surface = palettes neutral if (amoledDarkTheme.value) 0 else 10,
surface = palettes neutral 6,
onSurface = palettes neutral 90,
surfaceVariant = palettes neutralVariant 30,
onSurfaceVariant = palettes neutralVariant 80,
@ -86,7 +86,16 @@ fun dynamicDarkColorScheme(): ColorScheme {
surfaceContainer = palettes neutral 12,
surfaceContainerHigh = palettes neutral 17,
surfaceContainerHighest = palettes neutral 22,
)
).run {
if (useAmoledDarkTheme) copy(
surface = Color.Black,
surfaceContainerHighest = palettes neutral 8,
surfaceContainerHigh = palettes neutral 6,
surfaceContainer = palettes neutral 4,
surfaceContainerLow = palettes neutral 4,
surfaceContainerLowest = Color.Black,
) else this
}
}
@Composable
@ -111,7 +120,6 @@ infix fun Color.alwaysLight(isAlways: Boolean): Color {
colorScheme.error -> colorScheme.onError
colorScheme.surface -> colorScheme.onSurface
colorScheme.surfaceVariant -> colorScheme.onSurfaceVariant
colorScheme.error -> colorScheme.onError
colorScheme.primaryContainer -> colorScheme.onPrimaryContainer
colorScheme.secondaryContainer -> colorScheme.onSecondaryContainer
colorScheme.tertiaryContainer -> colorScheme.onTertiaryContainer
@ -125,7 +133,6 @@ infix fun Color.alwaysLight(isAlways: Boolean): Color {
colorScheme.onError -> colorScheme.error
colorScheme.onSurface -> colorScheme.surface
colorScheme.onSurfaceVariant -> colorScheme.surfaceVariant
colorScheme.onError -> colorScheme.error
colorScheme.onPrimaryContainer -> colorScheme.primaryContainer
colorScheme.onSecondaryContainer -> colorScheme.secondaryContainer
colorScheme.onTertiaryContainer -> colorScheme.tertiaryContainer
@ -153,7 +160,6 @@ infix fun Color.alwaysDark(isAlways: Boolean): Color {
colorScheme.error -> colorScheme.onError
colorScheme.surface -> colorScheme.onSurface
colorScheme.surfaceVariant -> colorScheme.onSurfaceVariant
colorScheme.error -> colorScheme.onError
colorScheme.primaryContainer -> colorScheme.onPrimaryContainer
colorScheme.secondaryContainer -> colorScheme.onSecondaryContainer
colorScheme.tertiaryContainer -> colorScheme.onTertiaryContainer
@ -167,7 +173,6 @@ infix fun Color.alwaysDark(isAlways: Boolean): Color {
colorScheme.onError -> colorScheme.error
colorScheme.onSurface -> colorScheme.surface
colorScheme.onSurfaceVariant -> colorScheme.surfaceVariant
colorScheme.onError -> colorScheme.error
colorScheme.onPrimaryContainer -> colorScheme.primaryContainer
colorScheme.onSecondaryContainer -> colorScheme.secondaryContainer
colorScheme.onTertiaryContainer -> colorScheme.tertiaryContainer

View File

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

View File

@ -82,7 +82,7 @@ data class TonalPalettes(
infix fun neutral(tone: TonalValue): Color = neutral.getOrPut(tone) {
zcamLch(
L = tone.toZcamLightness(),
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 12.0,
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 8.0,
h = hue,
).clampToRgb().toColor()
}

View File

@ -314,4 +314,8 @@
<string name="browse_bionic_reading_tips">قرا عن القراية البايونيك ع <i><u>bionic-reading.com</u></i>.</string>
<string name="use_bionic_reading">ستعمل القراية البايونيك</string>
<string name="bionic_reading_tips">شو القراية البايونيك؟</string>
<string name="toolbars">شريط الأدوات</string>
<string name="mark_as_read_on_scroll">علّم إنه منرقرا عال تمرير</string>
<string name="become_a_sponsor">صير سپانسر</string>
<string name="sponsor_desc">نحن منعمل و من جدد هيدا الآپ المجاني و مفتوح المصدر برات سعات العمل تبعنا.إذا بيعجبك الآپ، پليز فكر ب دعمنا ب تبرع زغير! ☕️</string>
</resources>

View File

@ -327,4 +327,7 @@
<string name="bionic_reading_tips">ما هي القراءة الحيوية؟?</string>
<string name="browse_bionic_reading_tips">تعرف على المزيد على <i><u>bionic-reading.com</u></i>.</string>
<string name="mark_as_read_on_scroll">وضع علامة مقروء على التمرير</string>
<string name="become_a_sponsor">كن داعما</string>
<string name="sponsor_desc">نحن نبني ونحافظ على هذا التطبيق المجاني والمفتوح المصدر في غير ساعات العمل لدينا. إذا كنت تستمتع به، فيرجى التفكير في دعمنا بتبرع صغير! ☕️</string>
<string name="toolbars">شريط الأدوات</string>
</resources>

View File

@ -312,4 +312,8 @@
<string name="only_available_on_webview">Предлага се само в WebView</string>
<string name="native_component">Собствен компонент</string>
<string name="use_bionic_reading">Използвай на бионично четене</string>
<string name="mark_as_read_on_scroll">Маркирай като прочетено при превъртане</string>
<string name="become_a_sponsor">Станете спонсор</string>
<string name="sponsor_desc">Ние създаваме и поддържаме това безплатно приложение с отворен код в извънработно време. Ако ви харесва, моля, помислете дали да ни подкрепите с малко дарение! ☕️</string>
<string name="toolbars">Ленти с инструменти</string>
</resources>

View File

@ -317,4 +317,8 @@
<string name="about">Informace</string>
<string name="browse_bionic_reading_tips">Více informací najdete na <i><u>bionic-reading.com</u></i>.</string>
<string name="bionic_reading_tips">Co je bionické čtení?</string>
<string name="mark_as_read_on_scroll">Označit jako přečtené při posouvání</string>
<string name="become_a_sponsor">Staňte se sponzorem</string>
<string name="sponsor_desc">Tuto bezplatnou aplikaci s otevřeným zdrojovým kódem vytváříme a udržujeme v našem volném čase. Pokud se vám líbí, zvažte prosím, zda nás podpoříte malým darem! ☕️</string>
<string name="toolbars">Nástrojové lišty</string>
</resources>

View File

@ -314,4 +314,8 @@
<string name="about">Über</string>
<string name="bionic_reading_tips">Was ist bionisches Lesen?</string>
<string name="browse_bionic_reading_tips">Erfahren Sie mehr unter <i><u>bionic-reading.com</u></i>.</string>
<string name="toolbars">Werkzeugleisten</string>
<string name="mark_as_read_on_scroll">Beim Scrollen als gelesen markieren</string>
<string name="become_a_sponsor">Sponsor werden</string>
<string name="sponsor_desc">Wir entwickeln und pflegen diese kostenlose Open-Source-Anwendung in unserer Freizeit. Wenn sie Ihnen gefällt, unterstützen Sie uns bitte mit einer kleinen Spende! ☕️</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="all">Όλα</string>
</resources>

View File

@ -321,4 +321,7 @@
<string name="use_bionic_reading">Utiliza la lectura biónica</string>
<string name="about">Acerca de</string>
<string name="mark_as_read_on_scroll">Marcar como leído al desplazarse</string>
<string name="become_a_sponsor">Conviértete en patrocinador</string>
<string name="sponsor_desc">Construimos y mantenemos esta aplicación gratuita y de código abierto en nuestro tiempo libre. Si la disfrutas, ¡considera apoyarnos con una pequeña donación! ☕️</string>
<string name="toolbars">Barras de herramientas</string>
</resources>

View File

@ -26,4 +26,145 @@
<string name="confirm">Kinnita</string>
<string name="cancel">Katkesta</string>
<string name="allow">Luba</string>
<string name="create_new_group">Loo uus grupp</string>
<string name="name">Nimi</string>
<string name="defaults">Vaikimisi</string>
<string name="unknown">Teadmata</string>
<string name="empty">Tühi</string>
<string name="back">Tagasi</string>
<string name="go_to">Mine</string>
<string name="settings">Seadistused</string>
<string name="refresh">Värskenda andmeid</string>
<string name="search">Otsi</string>
<string name="searching">Otsime…</string>
<string name="subscribe">Telli</string>
<string name="already_subscribed">Tellimus on juba olemas</string>
<string name="clear">Tühjenda</string>
<string name="paste">Aseta</string>
<string name="feed_or_site_url">Uudistevoo või veebisaidi aadress</string>
<string name="import_from_opml">Impordi uudisvoog OPML-failist</string>
<string name="preset">Eelseadistused</string>
<string name="selected">Valitud</string>
<string name="allow_notification">Luba teavitused</string>
<string name="all_allow_notification_tips">Luba kõikidel „%1$s“ rühma uudisvoogudel saata teavitusi</string>
<string name="all_deny_notification_toast">Kõik „%1$s“ rühma uudisvoogude teavitused on keelatud</string>
<string name="rename">Muuda nime</string>
<string name="change_url">Muuda võrguaadressi</string>
<string name="rename_toast">Muutsime uueks nimeks „%1$s“</string>
<string name="open_with">Ava rakendusega %1$s</string>
<string name="options">Valikud</string>
<string name="delete">Kustuta</string>
<string name="delete_toast">„%1$s“ on kustutatud</string>
<string name="unsubscribe">Lõpeta tellimus</string>
<string name="delete_group">Kustuta grupp</string>
<string name="unsubscribe_tips">Lõpetada „%1$s“ tellimus ja kustutada kõik tema arhiveeritud artiklid</string>
<string name="today">Täna</string>
<string name="delete_group_tips">Kustuta „%1$s“ grupp ja kõik sealsed uudisvood ja nende arhiveeritud artiklid</string>
<string name="group_option_tips">Järgnevad valikud kehtivad kõikidele uudisvoogudele selles grupis</string>
<string name="yesterday">Eile</string>
<string name="date_at_time">%1$s kell %2$s</string>
<string name="mark_as_read">Märgi loetuks</string>
<string name="mark_all_as_read">Märgi kõik loetuks</string>
<string name="mark_as_unread">Märgi mitteloetuks</string>
<string name="mark_as_starred">Märgi lemmikuks</string>
<string name="mark_as_unstar">Eemalda lemmiku märge</string>
<string name="mark_as_read_one_day">Märgi loetuks, kui vanus on üle 1 päeva</string>
<string name="mark_as_read_three_days">Märgi loetuks, kui vanus on üle 3 päeva</string>
<string name="mark_as_read_seven_days">Märgi loetuks, kui vanus on üle 7 päeva</string>
<string name="one_day">1 pv</string>
<string name="three_days">3 pv</string>
<string name="seven_days">7 pv</string>
<string name="close">Sulge</string>
<string name="languages">Keeled</string>
<string name="help_translate">Aita tõlkida</string>
<string name="use_device_languages">Kasuta nutiseadme keelt</string>
<string name="tips_and_support">Nõuanded ja kasutajatugi</string>
<string name="wallpaper_colors">Taustapildi värvid</string>
<string name="open_source_licenses">Avatud lähtekoodiga tarkvara litsentsid</string>
<string name="change_log">Muudatuste logi</string>
<string name="update">Uuenda</string>
<string name="skip_this_version">Jäta see versioon vahele</string>
<string name="checking_updates">Kontrollin uuendusi…</string>
<string name="is_latest_version">See on rakenduse viimane versioon</string>
<string name="check_failure">Uuenduste kontrollimine ei õnnestunud</string>
<string name="download_failure">Uuenduste allalaadimine ei õnnestunud</string>
<string name="rate_limit">Päringutel kehtib mahupiirang</string>
<string name="help">Abiteave</string>
<string name="on_start">Rakenduse käivitamisel</string>
<string name="font_size">Kirjatüübi suurus</string>
<string name="letter_spacing">Tähevahed</string>
<string name="line_spacing">Ridade vahed</string>
<string name="alignment">Joondumine</string>
<string name="all_allow_notification_toast">Kõik „%1$s“ rühma uudisvoogude teavitused on lubatud</string>
<string name="parse_full_content">Töötle terviksisu</string>
<string name="all_parse_full_content_tips">Töötleme „%1$s“ grupi kõikide artiklite terviksisu</string>
<string name="all_parse_full_content_toast">Töötleme „%1$s“ grupi kõikide artiklite terviksisu</string>
<string name="all_deny_parse_full_content_toast">„%1$s“ grupi kõikide artiklite terviksisu töötlemine on lõppenud</string>
<string name="clear_articles">Eemalda artiklid</string>
<string name="clear_articles_in_feed_toast">Eemaldasime kõik „%1$s“ uudisvoo arhiveeritud artiklid</string>
<string name="clear_articles_in_group_toast">Eemaldasime kõik „%1$s“ grupi arhiveeritud artiklid</string>
<string name="clear_articles_feed_tips">Eemalda kõik „%1$s“ uudisvoo arhiveeritud artiklid</string>
<string name="clear_articles_group_tips">Eemalda kõik „%1$s“ grupi arhiveeritud artiklid</string>
<string name="add_to_group">Lisa gruppi</string>
<string name="move_to_group">Teisalda gruppi</string>
<string name="all_move_to_group_tips">Teisalda kõik uudisvood „%1$s“ grupist „%2$s“ gruppi</string>
<string name="all_move_to_group_toast">Teisaldasime kõik uudisvood „%1$s“ gruppi</string>
<string name="search_for_in">Otsing: %1$s asukohas „%2$s“</string>
<string name="search_for">Otsing: %1$s</string>
<string name="get_new_updates">Otsi uuendusi</string>
<string name="get_new_updates_desc">Uus versioon %1$s on saadaval</string>
<string name="specific_browser_name">Brauser: %1$s</string>
<string name="default_browser">Sundkorras kasuta vaikimisi brauserit</string>
<string name="always_ask">Alati küsi</string>
<string name="open_link_specific_browser">Brauser</string>
<string name="open_link_something_wrong">Vea tekkemise tõttu eirame „Ava link“ seadistust.</string>
<string name="open_link_ask_dialog_title">Ava rakendusega…</string>
<string name="include_additional_info">Kaasa täiendav teave</string>
<string name="in_coding">Arendamisel</string>
<string name="accounts">Kasutajakontod</string>
<string name="accounts_desc">Kohalik, FreshRSS</string>
<string name="color_and_style">Värv ja stiil</string>
<string name="color_and_style_desc">Kujundus, värvivalik, kirjatüübid</string>
<string name="interaction">Liidese kasutamine</string>
<string name="coming_soon">Avaldatakse varsti</string>
<string name="interaction_desc">Avaleht, haptiline tagasiside</string>
<string name="tips_and_support_desc">Rakenduse teave, avatud lähtekoodi litsentsid</string>
<string name="welcome">Tere tulemnast</string>
<string name="agree">Nõustun</string>
<string name="no_palettes">Värvipalette pole</string>
<string name="only_android_8.1_plus">Vaid Android 8.1+ jaoks</string>
<string name="basic_colors">Põhivärvid</string>
<string name="primary_color">Esmane värv</string>
<string name="primary_color_hint">Näiteks #666666 või 666666</string>
<string name="appearance">Välimus</string>
<string name="style">Vaadete kujundus</string>
<string name="dark_theme">Tume kujundus</string>
<string name="use_device_theme">Kasuta seadme kujundust</string>
<string name="on">Sees</string>
<string name="off">Väljas</string>
<string name="other">Muu</string>
<string name="amoled_dark_theme">AMOLEDi tume kujundus</string>
<string name="tonal_elevation">Tooninihe</string>
<string name="reading_fonts">Kirjatüübid lugemiseks</string>
<string name="basic_fonts">Põhilised kirjatüübid</string>
<string name="feeds_page">Uudisvoogude leht</string>
<string name="flow_page">Uudislindi leht</string>
<string name="reading_page">Lugemisvaade</string>
<string name="sponsor">Sponsor</string>
<string name="update_link">https://api.github.com/repos/Ashinch/ReadYou/releases/latest</string>
<string name="initial_filter">Algne filter</string>
<string name="initial_page">Algne leht</string>
<string name="preview_feed_name">Reddit</string>
<string name="value">väärtus</string>
<string name="browse_tos_tips">Loe <i><u>kasutustingimusi ja privaatsuspoliitikat</u></i></string>
<string name="terms_of_service">Kasutustingimused</string>
<string name="tos_tips">Jätkamiseks palun tutvu ja nõustu Read You kasutustingimuste ja privaatsuspoliitikaga.</string>
<string name="horizontal_padding">Rõhtne veeris</string>
<string name="article_date">Artikli avaldamise aeg</string>
<string name="article_desc">Artikli kirjeldused</string>
<string name="article_images">Artikli pildid</string>
<string name="feed_names">Uudisvoogude nimed</string>
<string name="feed_favicons">Uudisvoogude favikonid</string>
<string name="article_date_sticky_header">Kleepuv avaldamise kuupäeva päis (katseline)</string>
<string name="article_list">Artiklite loend</string>
</resources>

View File

@ -315,4 +315,8 @@
<string name="only_available_on_webview">Hanya tersedia di WebView</string>
<string name="use_bionic_reading">Gunakan Bionic Reading</string>
<string name="bionic_reading_tips">Apa itu Bionic Reading?</string>
<string name="mark_as_read_on_scroll">Baca sebagai dibaca saat digulir</string>
<string name="become_a_sponsor">Jadilah sponsor</string>
<string name="toolbars">Bilah alat</string>
<string name="sponsor_desc">Kami membangun dan memelihara aplikasi sumber terbuka gratis ini di luar jam kerja. Jika Anda menikmatinya, mohon pertimbangkan untuk mendukung kami dengan donasi kecil! ☕️</string>
</resources>

View File

@ -318,4 +318,7 @@
<string name="native_component">Componente nativo</string>
<string name="only_available_on_webview">Disponibile esclusivamente in WebView</string>
<string name="mark_as_read_on_scroll">Contrassegna come letto allo scroll</string>
<string name="become_a_sponsor">Diventa uno sponsor</string>
<string name="sponsor_desc">Sviluppiamo e miglioriamo questa applicazione, gratuita ed open source, nel nostro tempo libero. Se ti piace, considera la possibilità di sostenerci con una piccola donazione! ☕</string>
<string name="toolbars">Barre degli strumenti</string>
</resources>

View File

@ -216,4 +216,30 @@
<string name="always_ask">תמיד לשאול</string>
<string name="unfold_more">הרחבה של הכול</string>
<string name="unfold_less">צמצום של הכול</string>
<string name="group_option_tips">האפשרויות הבאות חלות על כל העדכונים בקבוצה זו</string>
<string name="clear_articles_feed_tips">נקה את כל המאמרים המאוחסנים בארכיון בעדכון \"%1$s\"</string>
<string name="all_move_to_group_toast">העביר את כל העדכונים לקבוצת \"%1$s\"</string>
<string name="all_move_to_group_tips">העבר את כל העדכונים מקבוצת \"%1$s\" לקבוצה \"%2$s\"</string>
<string name="all_parse_full_content_toast">ניתוח תוכן מלא של כל המאמרים בקבוצת \"%1$s\"</string>
<string name="all_deny_parse_full_content_toast">לא עוד ניתוח תוכן מלא של כל המאמרים בקבוצת \"%1$s\"</string>
<string name="clear_articles_in_feed_toast">ניקה את כל המאמרים המאוחסנים בארכיון בעדכון \"%1$s\"</string>
<string name="clear_articles_in_group_toast">ניקה את כל המאמרים המאוחסנים בארכיון בקבוצה \"%1$s\"</string>
<string name="clear_articles_group_tips">נקה את כל המאמרים המאוחסנים בארכיון בקבוצת \"%1$s\"</string>
<string name="rename_toast">השם שונה ל-\"%1$s\"</string>
<string name="open_with">פתח את %1$s</string>
<string name="delete_toast">\"%1$s\" נמחק</string>
<string name="unsubscribe_tips">בטל את הרישום ל-\"%1$s\" ומחק את כל המאמרים שהועברו לארכיון בו</string>
<string name="delete_group_tips">מחק את הקבוצה \"%1$s\" ואת כל העדכונים והמאמרים המאוחסנים בארכיון שלה</string>
<string name="search_for_in">חפש %1$s פריטים ב-\"%2$s\"</string>
<string name="search_for">חפש %1$s פריטים</string>
<string name="mark_as_read">סמן כנקרא</string>
<string name="mark_all_as_read">סמן הכל כנקרא</string>
<string name="mark_as_unread">סמן כלא נקרא</string>
<string name="mark_as_starred">סמן בכוכב</string>
<string name="mark_as_unstar">בטל כוכב</string>
<string name="mark_as_read_one_day">סמן כנקראה לפני יותר מיום אחד</string>
<string name="mark_as_read_three_days">סמן כנקראה לפני יותר מ-3 ימים</string>
<string name="mark_as_read_seven_days">סמן כנקראה לפני יותר מ-7 ימים</string>
<string name="in_coding">בקידוד</string>
<string name="tos_tips">אנא קרא והסכם לתנאי השירות ומדיניות הפרטיות כדי להמשיך.</string>
</resources>

View File

@ -116,4 +116,10 @@
<string name="dark_theme">ഡാർക്ക് തീം</string>
<string name="use_device_theme">ഉപകരണത്തിന്റെ തീം ഉപയോഗിക്കുക</string>
<string name="skip_this_version">ഈ പതിപ്പ് ഒഴിവാക്കുക</string>
<string name="appearance">സ്വരൂപം</string>
<string name="style">ശൈലി</string>
<string name="primary_color">പ്രാഥമിക നിറം</string>
<string name="primary_color_hint">#666666 അല്ലെങ്കിൽ 666666 പോലെ</string>
<string name="initial_filter">ആദ്യഘടകം</string>
<string name="preview_feed_name">റെഡ്ഡിറ്റ്</string>
</resources>

View File

@ -30,7 +30,7 @@
<string name="defaults">Domyślne</string>
<string name="unknown">Nieznane</string>
<string name="back">Wstecz</string>
<string name="go_to">Idź Do</string>
<string name="go_to">Idź do</string>
<string name="settings">Ustawienia</string>
<string name="refresh">Odśwież</string>
<string name="search">Wyszukaj</string>
@ -72,7 +72,7 @@
<string name="unsubscribe">Odsubskrybuj</string>
<string name="unsubscribe_tips">Anuluj subskrypcję kanału \"%1$s\" i usuń wszystkie zawarte w nim artykuły</string>
<string name="delete_group">Usuń grupę</string>
<string name="delete_group_tips">Usuń grupę \"%1$s\" i wszystkie zawarte w niej artykuły.</string>
<string name="delete_group_tips">Usuń grupę \"%1$s\" oraz wszystkie kanały i zarchiwizowane artykuły, które ona zawiera</string>
<string name="group_option_tips">Poniższe zmiany zostaną zastosowane dla wszystkich kanałów w tej grupie</string>
<string name="today">Dziś</string>
<string name="yesterday">Wczoraj</string>
@ -92,7 +92,7 @@
<string name="seven_days">7d</string>
<string name="close">Zamknij</string>
<string name="get_new_updates">Sprawdź aktualizacje</string>
<string name="get_new_updates_desc">Wersja %1$s jest już dostępna</string>
<string name="get_new_updates_desc">Nowa wersja %1$s jest dostępna</string>
<string name="in_coding">W trakcie kodowania</string>
<string name="coming_soon">Dostępne wkrótce</string>
<string name="accounts">Konta</string>
@ -107,117 +107,11 @@
<string name="tips_and_support">Porady i wsparcie</string>
<string name="tips_and_support_desc">O aplikacji i licencjach open source</string>
<string name="welcome">Witamy</string>
<string name="tos_tips">Korzystając z Read You akceptujesz Warunki Świadczenia Usług i Politykę Prywatności. Kliknij "Zgadzam się" aby kontynuować.</string>
<string name="tos_tips">Aby kontynuować, przeczytaj i zaakceptuj Warunki korzystania z usługi i Politykę prywatności Read You.</string>
<string name="browse_tos_tips">Zapoznaj się z &lt;i&gt;&lt;u&gt;Warunkami Świadczenia Usługi oraz Polityką Prywatności&lt;/u&gt;&lt;/i&gt;
</string>
<string name="terms_of_service">Warunki świadczenia usługi</string>
<string name="tos_content">
&lt;h5&gt;
Polityka prywatności
&lt;/h5&gt;
&lt;br/&gt;
&lt;p&gt;
Traktujemy kwestię prywatności użytkownika z należytą powagą.
&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;
&lt;b&gt;Read You&lt;/b&gt;
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.
&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;
&lt;b&gt;Read You&lt;/b&gt;
korzysta z poniższych uprawnień w celu świadczenia usług.
&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;
- Pełny dostęp do sieci (w celu uzyskania dostępu do treści wskazanych przez użytkownika)
&lt;/p&gt;
&lt;p&gt;
- Wyświetlanie połączeń sieciowych (w celu sprawdzenia czy urządzenie jest w stanie połączyć się z siecią)
&lt;/p&gt;
&lt;p&gt;
- Uruchom usługę na pierwszym planie (w celu regularnej automatycznej synchronizacji treści)
&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;h5&gt;
Usługi Stron Trzecich
&lt;/h5&gt;
&lt;br/&gt;
&lt;p&gt;
Ta umowa nie tyczy się stron podmiotów trzecich z którymi użytkownik łączy się za pomocą &lt;b&gt;Read You&lt;/b&gt;.
Zasady ochrony prywatności tych serwisów można sprawdzić na ich poszczególnych stronach internetowych
&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;h5&gt;
Oświadczenie
&lt;/h5&gt;
&lt;br/&gt;
&lt;p&gt;
&lt;b&gt;Read You&lt;/b&gt;
jest jedynie agregatem treści. Sposób, w jaki użytkownik korzysta z &lt;b&gt;Read You&lt;/b&gt; 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.
&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;h5&gt;
Licencja Open Source
&lt;/h5&gt;
&lt;br/&gt;
&lt;p&gt;
&lt;b&gt;Read You&lt;/b&gt;
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 &lt;b&gt;Read You&lt;/b&gt;. 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].
&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;h5&gt;
Załączniki
&lt;/h5&gt;
&lt;br/&gt;
&lt;p&gt;
- [1] https://github.com/Ashinch/ReadYou
&lt;/p&gt;
&lt;p&gt;
- [2] https://www.gnu.org/licenses/gpl-3.0.html
&lt;/p&gt;
</string>
<string name="tos_content"><h5>Polityka prywatności</h5> <br/> <p>Twoją prywatność traktuję bardzo poważnie.</p> <br/> <p> <b>Read You</b> nie zbiera żadnych danych użytkownika, a wszystkie poufne informacje (hasła i inne informacje o koncie) są bezpiecznie przechowywane w lokalnej bazie danych aplikacji na Twoim urządzeniu.</p> <br/> <p> <b>Read You</b> będzie korzystać z następujących uprawnień, aby świadczyć Ci usługę.</p> <br/> <p> - Uprawnienie dostępu do sieci (aby uzyskać dostęp do treści online zgodnie z Twoimi ustawieniami)</p> <p> - Uprawnienie uzyskania stanu sieci (aby określić, czy urządzenie ma obecnie dostępne warunki sieciowe)</p> <p> - Uprawnienie usługi w tle (aby regularnie automatycznie synchronizować Twoje ulubione)</p> <br/> <br/> <h5>Usługi stron trzecich</h5> <br/> <p> Niniejsza polityka nie dotyczy usług stron trzecich, z których korzystasz w <b>Read You</b>. Możesz zapoznać się z polityką prywatności usług stron trzecich, z których korzystasz na ich stronach internetowych </p> <br/> <br/> <h5> Zastrzeżenia </h5> <br/> <p> <b>Read You</b> jest wyłącznie narzędziem do gromadzenia treści. Korzystanie z <b>Read You</b> podlega prawom i regulacjom obowiązującym w Twoim kraju i regionie, a wszelka odpowiedzialność wynikająca z Twoich działań będzie ponoszona przez Ciebie osobiście. </p> <br/> <br/> <h5> Licencja open source </h5> <br/> <p> <b>Read You</b> jest projektem typu open source na podstawie licencji GNU GPL 3.0[1], która pozwala na bezpłatne korzystanie, odwoływanie się do kodu źródłowego <b>Read You</b> i jego modyfikację, ale nie zezwala na dystrybucję i sprzedaż zmodyfikowanego i pochodnego kodu jako zamkniętego oprogramowania komercyjnego. Aby uzyskać szczegółowe informacje, zapoznaj się z pełną licencją GNU GPL 3.0[2]. </p> <br/> <br/> <h5> Załącznik </h5> <br/> <p> - [1] https://github.com/Ashinch/ReadYou </p> <p> - [2] https://www.gnu.org/licenses/gpl-3.0.html </p></string>
<string name="agree">Zgadzam się</string>
<string name="wallpaper_colors">Kolory z tapety</string>
<string name="no_palettes">Brak palet</string>
@ -232,7 +126,7 @@
<string name="on">Włączono</string>
<string name="off">Wyłączono</string>
<string name="other">Inne</string>
<string name="amoled_dark_theme">Czarny motyw AMOLED</string>
<string name="amoled_dark_theme">Ciemny motyw AMOLED</string>
<string name="tonal_elevation">Odcień</string>
<string name="reading_fonts">Czcionka artykułu</string>
<string name="basic_fonts">Podstawowe czcionki</string>
@ -244,10 +138,10 @@
<string name="update_link">https://api.github.com/repos/Ashinch/ReadYou/releases/latest</string>
<string name="change_log">Lista zmian</string>
<string name="update">Aktualizuj</string>
<string name="skip_this_version">Pomiń tą wersję</string>
<string name="skip_this_version">Pomiń tę wersję</string>
<string name="checking_updates">Sprawdzam aktualizacje…</string>
<string name="is_latest_version">Masz najnowszą wersję aplikacji</string>
<string name="check_failure">Nie udało się sprawdzić aktualizacji</string>
<string name="check_failure">Nie udało się sprawdzić dostępności aktualizacji</string>
<string name="download_failure">Nie udało się pobrać aktualizacji</string>
<string name="rate_limit">Limit częstotliwości żądań</string>
<string name="help">Pomoc</string>
@ -277,12 +171,12 @@
<string name="filter_bar">Pasek filtrów</string>
<string name="icons">Ikony</string>
<string name="icons_and_labels">Ikony z tekstem</string>
<string name="icons_and_label_only_selected">Ikony z tekstem (tylko zaznaczone)</string>
<string name="tips_top_bar_tonal_elevation">Ten odcień jest dostępny tylko podczas przewijania.</string>
<string name="tips_article_list_tonal_elevation">Ten odcień jest dostępny tylko w jasnym motywie.</string>
<string name="tips_group_list_tonal_elevation">Ten odcień jest dostępny tylko w jasnym motywie.</string>
<string name="icons_and_label_only_selected">Ikony z tekstem (tylko aktywne)</string>
<string name="tips_top_bar_tonal_elevation">Odcień górnego paska jest dostępny tylko podczas przewijania</string>
<string name="tips_article_list_tonal_elevation">Odcień listy artykułów jest dostępny tylko w jasnym motywie</string>
<string name="tips_group_list_tonal_elevation">Odcień listy grup jest dostępny tylko w jasnym motywie</string>
<string name="share">Udostępnij</string>
<string name="touch_to_play_video">Dotknij aby odtworzyć film</string>
<string name="touch_to_play_video">Stuknij, aby odtworzyć film</string>
<string name="text">Tekst</string>
<string name="font_size">Rozmiar czcionki</string>
<string name="letter_spacing">Odstępy między znakami</string>
@ -295,8 +189,8 @@
<string name="images">Obrazy</string>
<string name="rounded_corners">Zaokrąglone rogi</string>
<string name="videos">Filmy</string>
<string name="align_start">Do lewej</string>
<string name="align_end">Do prawej</string>
<string name="align_start">Wyrównaj początek</string>
<string name="align_end">Wyrównaj koniec</string>
<string name="center_text">Do środka</string>
<string name="justify">Wyjustuj</string>
<string name="external_fonts">Zewnętrzne czcionki</string>
@ -330,7 +224,7 @@
<string name="for_1_month">1 miesiąc</string>
<string name="local">Konto lokalne</string>
<string name="services">Usługi</string>
<string name="fever_desc">Przestarzałe. Nie zalecane.</string>
<string name="fever_desc">Przestarzałe. Niezalecane.</string>
<string name="self_hosted">Własny serwer</string>
<string name="more">Więcej</string>
<string name="add_accounts">Dodaj konta</string>
@ -350,13 +244,13 @@
<string name="delete_account_toast">Konto zostało usunięte</string>
<string name="clear_all_articles">Usuń wszystkie artykuły</string>
<string name="block_list">Lista zablokowanych</string>
<string name="clear_all_articles_tips">Czy na pewno chcesz usunąć wszystkie artykuły z tego konta\?</string>
<string name="clear_all_articles_tips">Czy na pewno chcesz wyczyścić listę artykułów tego konta?</string>
<string name="delete_account_tips">Czy na pewno chcesz usunąć to konto\?</string>
<string name="synchronous_tips">Aby zmiany zaczęły obowiązywać, wymagane jest ponowne uruchomienie.</string>
<string name="synchronous_tips">Aby zmiany zostały wprowadzone, wymagane jest ponowne uruchomienie</string>
<string name="local_desc">Na tym urządzeniu</string>
<string name="switch_account">Przełącz</string>
<string name="add">Dodaj</string>
<string name="accounts_tips">Kliknij nazwę konta na stronie kanału, aby je zmienić.</string>
<string name="accounts_tips">Aby zmienić konto, stuknij jego nazwę na stronie głównej</string>
<string name="empty">Pusty</string>
<string name="username">Nazwa użytkownika</string>
<string name="password">Hasło</string>
@ -396,4 +290,36 @@
<string name="unfold_more">Rozwiń wszystkie</string>
<string name="unfold_less">Zwiń wszystkie</string>
<string name="about">O aplikacji</string>
<string name="import_from_json">Importuj z JSON</string>
<string name="export_as_json">Eksportuj jako JSON</string>
<string name="invalid_json_file_warning">Ten plik może nie być prawidłowym plikiem JSON. Jego importowanie może potencjalnie uszkodzić aplikację i spowodować utratę bieżących preferencji. Czy na pewno chcesz kontynuować?</string>
<string name="line_height_multiple">Wielokrotność wysokości wiersza</string>
<string name="invalid_protobuf_file_warning">Ten plik może nie być prawidłowym plikiem protobuf. Jego importowanie może potencjalnie uszkodzić aplikację i spowodować utratę bieżących preferencji. Czy na pewno chcesz kontynuować?</string>
<string name="import_from_protobuf_file">Importuj z pliku protobuf</string>
<string name="toggle_read">Przełącz stan przeczytania</string>
<string name="grey_out_articles">Wyszarzaj artykuły</string>
<string name="all_read">Wszystko przeczytane</string>
<string name="export_as_protobuf_file">Eksportuj jako plik protobuf</string>
<string name="keep_archived_tips">Zarchiwizowane elementy to artykuły oznaczone jako przeczytane, niewyróżnione i nieotagowane. Ustawienie „Zachowaj zarchiwizowane artykuły” jest skuteczne tylko na tym urządzeniu. Starsze zarchiwizowane artykuły zostaną usunięte z tego urządzenia. Usunięcia artykułów nie można cofnąć.</string>
<string name="initial_open_app">Aplikacja po kliknięciu linku</string>
<string name="additional_info_desc">Dodatkowe informacje obejmują opcje konfiguracji dla każdego kanału, takie jak zezwolenie na powiadomienia, analiza pełnej zawartości itp. Jeśli zamierzasz używać wyeksportowanego pliku OPML z innymi czytnikami, wybierz opcję „Wyklucz”.</string>
<string name="submit_bug_report">zgłoś błąd na GitHub</string>
<string name="unexpected_error_msg">Aplikacja napotkała nieoczekiwany błąd i musiała zostać zamknięta.\n\nAby szybko zidentyfikować i rozwiązać problem, zapoznaj się z poniższym śladem stosu błędów %1$s.</string>
<string name="pull_to_switch_article">Przeciągnij, aby zmienić artykuł</string>
<string name="mark_above_as_read">Oznacz powyższe jako przeczytane</string>
<string name="mark_below_as_read">Oznacz poniższe jako przeczytane</string>
<string name="toolbars">Paski narzędzi</string>
<string name="open_link_something_wrong">Ustawienie „Otwórz link” zostało zignorowane, ponieważ coś poszło nie tak.</string>
<string name="native_component">Komponent natywny</string>
<string name="content_renderer">Renderer treści</string>
<string name="read_aloud">Czytać na głos</string>
<string name="only_available_on_webview">Dostępne tylko w WebView</string>
<string name="use_bionic_reading">Użyj czytania bionicznego</string>
<string name="bionic_reading_tips">Czym jest czytanie bioniczne?</string>
<string name="browse_bionic_reading_tips">Dowiedz się więcej na <i><u>bionic-reading.com</u></i>.</string>
<string name="mark_as_read_on_scroll">Oznacz jako przeczytane podczas przewijania</string>
<string name="become_a_sponsor">Zostań sponsorem</string>
<string name="sponsor_desc">Tworzymy i utrzymujemy tę darmową aplikację typu open source poza godzinami pracy. Jeśli Ci się podoba, rozważ wsparcie nas małą darowizną! ☕️</string>
<string name="troubleshooting_desc">Raport o błędach, narzędzia debugowania</string>
<string name="toggle_starred">Przełącz wyróżnione</string>
</resources>

View File

@ -317,4 +317,8 @@
<string name="about">Sobre</string>
<string name="bionic_reading_tips">O que é Bionic Reading?</string>
<string name="browse_bionic_reading_tips">Saiba mais em <i><u>bionic-reading.com</u></i>.</string>
<string name="toolbars">Barras de Ferramentas</string>
<string name="mark_as_read_on_scroll">Marcar como lido ao rolar a página</string>
<string name="become_a_sponsor">Torne-se um patrocinador</string>
<string name="sponsor_desc">Nós contruímos e mantemos este aplicativo gratuito e de código aberto em nossas horas vagas. Se você gosta, considere nos apoiar com uma pequena doação! ☕️</string>
</resources>

View File

@ -317,4 +317,8 @@
<string name="about">Sobre</string>
<string name="bionic_reading_tips">O que é Bionic Reading?</string>
<string name="browse_bionic_reading_tips">Saiba mais em <i><u>bionic-reading.com</u></i>.</string>
<string name="mark_as_read_on_scroll">Marcar como lido na rolagem</string>
<string name="toolbars">Barras de ferramentas</string>
<string name="become_a_sponsor">Torne-se um patrocinador</string>
<string name="sponsor_desc">Construímos e mantemos este aplicativo gratuito e de código aberto fora do horário comercial. Se você gostou, considere nos apoiar com uma pequena doação! ☕️</string>
</resources>

View File

@ -320,4 +320,8 @@
<string name="read_aloud">Читать вслух</string>
<string name="native_component">Нативный компонент</string>
<string name="browse_bionic_reading_tips">Узнайте больше на <i><u>bionic-reading.com</u></i>.</string>
<string name="toolbars">Панели инструментов</string>
<string name="mark_as_read_on_scroll">Отметить как прочитанное при прокрутке</string>
<string name="become_a_sponsor">Стать спонсором</string>
<string name="sponsor_desc">Мы создаем и поддерживаем это бесплатное приложение с открытым исходным кодом в свободное время. Если оно вам нравится, пожалуйста, поддержите нас небольшим пожертвованием! ☕️</string>
</resources>

View File

@ -317,4 +317,8 @@
<string name="about">Informácie</string>
<string name="content_renderer">Zobrazovač obsahu</string>
<string name="only_available_on_webview">Dostupné len vo WebView</string>
<string name="mark_as_read_on_scroll">Označiť ako prečítané pri posúvaní</string>
<string name="toolbars">Panely nástrojov</string>
<string name="sponsor_desc">Túto bezplatnú aplikáciu s otvoreným zdrojovým kódom vytvárame a udržiavame v mimopracovnom čase. Ak sa vám páči, zvážte, či nás podporíte malým darom! ☕️</string>
<string name="become_a_sponsor">Staňte sa sponzorom</string>
</resources>

View File

@ -111,7 +111,7 @@
<string name="every_15_minutes">Сваких 15 минута</string>
<string name="interaction">Интеракција</string>
<string name="check_failure">Провера ажурирања није успела</string>
<string name="align_start">Поравнајте почетак</string>
<string name="align_start">Поравнај почетак</string>
<string name="keep_archived_articles">Чување архивираних чланака</string>
<string name="selected">Изабрано</string>
<string name="icons">Иконице</string>
@ -160,7 +160,7 @@
<string name="sponsor">Спонзор</string>
<string name="for_3_days">3 дана</string>
<string name="deny">Одбиј</string>
<string name="align_end">Поравнајте крај</string>
<string name="align_end">Поравнај крај</string>
<string name="block_list">Блок листа</string>
<string name="specific_browser_name">Претраживач: %1$s</string>
<string name="specific_browser">Присили одређени претраживач</string>
@ -317,4 +317,8 @@
<string name="use_bionic_reading">Користите бионичко читање</string>
<string name="about">О нама</string>
<string name="browse_bionic_reading_tips">Сазнајте више на <i><u>bionic-reading.com</u></i>.</string>
<string name="mark_as_read_on_scroll">Означи као прочитано при превлачењу</string>
<string name="become_a_sponsor">Постани спонзор</string>
<string name="sponsor_desc">Ми правимо и одржавамо ову бесплатну апликацију отвореног кода ван радног времена. Ако уживате, размислите о томе да нас подржите малом донацијом! ☕</string>
<string name="toolbars">Траке са алаткама</string>
</resources>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -320,4 +320,8 @@
<string name="only_available_on_webview">Доступно тільки у WebView</string>
<string name="about">Про застосунок</string>
<string name="bionic_reading_tips">Що таке біонічне зчитування?</string>
<string name="mark_as_read_on_scroll">Позначити як прочитане при прокручуванні</string>
<string name="become_a_sponsor">Стати спонсором</string>
<string name="sponsor_desc">Ми створюємо і підтримуємо цей безплатний застосунок з відкритим вихідним кодом у вільний від роботи час. Якщо він вам подобається, будь ласка, підтримайте нас невеликою пожертвою! ☕️</string>
<string name="toolbars">Панелі інструментів</string>
</resources>

View File

@ -312,4 +312,7 @@
<string name="bionic_reading_tips">什么是 Bionic Reading</string>
<string name="browse_bionic_reading_tips"><i><u>bionic-reading.com</u></i>.了解更多信息。</string>
<string name="mark_as_read_on_scroll">滚动时标记为已读</string>
<string name="become_a_sponsor">成为赞助者</string>
<string name="sponsor_desc">我们在休息时间构建并维护这个自由开源应用。如果你觉得应用还不错,请考虑用小额捐赠支持哦我们!☕️</string>
<string name="toolbars">工具栏</string>
</resources>

View File

@ -311,4 +311,8 @@
<string name="about">關於</string>
<string name="bionic_reading_tips">什麼是仿生閱讀?</string>
<string name="browse_bionic_reading_tips">了解更多資訊,請造訪 <i><u>bionic-reading.com</u></i></string>
<string name="toolbars">工具列</string>
<string name="mark_as_read_on_scroll">捲動時標記為已讀</string>
<string name="become_a_sponsor">成為贊助者</string>
<string name="sponsor_desc">我們在空閒時間建立並維護這個免費的開源應用程式。如果您喜歡它,請考慮給予我們一點小額捐款支持!☕️</string>
</resources>

View File

@ -392,6 +392,7 @@
<string name="server_url">Server URL</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="client_certificate">Client certificate (optional)</string>
<string name="connection">Connection</string>
<string name="system_default">System</string>
<string name="initial_open_app">App when link is clicked</string>
@ -458,4 +459,8 @@
<string name="bionic_reading_domain" translatable="false">bionic-reading.com</string>
<string name="bionic_reading_link" translatable="false">https://bionic-reading.com</string>
<string name="mark_as_read_on_scroll">Mark as read on scroll</string>
<string name="hide_empty_groups">Hide empty groups</string>
<string name="become_a_sponsor">Become a sponsor</string>
<string name="sponsor_desc">We build and upkeep this free, open-source app in our off-hours. If you enjoy it, please consider supporting us with a small donation! ☕️</string>
<string name="toolbars">Toolbars</string>
</resources>

View File

@ -10,6 +10,7 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.mock
import rust.nostr.sdk.Client
internal const val enclosureUrlString1: String = "https://example.com/enclosure.jpg"
internal const val enclosureUrlString2: String = "https://github.blog/wp-content/uploads/2024/03/github_copilot_header.png"
@ -50,6 +51,9 @@ class RssHelperTest {
@Mock
private lateinit var mockOkHttpClient: OkHttpClient
@Mock
private lateinit var mockNostrClient: Client
private lateinit var rssHelper: RssHelper
@Before
@ -57,7 +61,8 @@ class RssHelperTest {
mockContext = mock<Context> { }
mockIODispatcher = mock<CoroutineDispatcher> {}
mockOkHttpClient = mock<OkHttpClient> {}
rssHelper = RssHelper(mockContext, mockIODispatcher, mockOkHttpClient)
mockNostrClient = mock<Client> { }
rssHelper = RssHelper(mockContext, mockIODispatcher, mockOkHttpClient, mockNostrClient)
}
@Test

View File

@ -0,0 +1,8 @@
## 0.11.0
تغييرات ملحوظة:
1. تقديم قارئ مقالة جديد يعتمد على WebView، مع دعم Bionic Reading
2. وضع علامة تلقائية على العناصر على أنها مقروءة في التمرير
3. انقر فوق شريط التطبيق العلوي للعودة إلى أعلى الصفحة
4. إضافة فواصل إلى أشرطة الأدوات في صفحة القراءة
5. إضافة زر لطي/توسيع مجموعات التغذية

View File

@ -0,0 +1,6 @@
## 0.11.1
تغييرات ملحوظة:
1. دعم الخط المخصص لقارئ WebView
2. إضافة أنماط مرئية جديدة إلى أشرطة الأدوات في صفحة التدفق وصفحة القراءة
3. تحسينات وإصلاحات واجهة المستخدم

View File

@ -7,8 +7,8 @@
5. Подобряване на плъзгане за звезда/премахване на звезда, плъзгане до непрочетено и добавяне на конфигурация (#594, @JunkFood02)
6. Използвайте системния локал, за да форматирате дисплея на часа по подразбиране. (#617, @JunkFood02)
7. Преминете към внедряване на androidx edge to edge (#690, @Moderpach)
8. Добавете програма за преглед на изображения към страницата за четене (#578, #545, @JunkFood02, @nvllz)
9. Добавете жестове за плъзгане нагоре и надолу по страницата за четене, за да превключвате статии (#589, @JunkFood02)
8. Добавяне на програма за преглед на изображения към страницата за четене (#578, #545, @JunkFood02, @nvllz)
9. Добавяне на жестове за плъзгане нагоре и надолу по страницата за четене, за да превключвате статии (#589, @JunkFood02)
10. Добавяне на дейност за докладване на срив, за да се справят с неуловени изключения (#576, @JunkFood02)
11. Добавете контекстно меню при продължително натискане за елементи в страницата на потока (#613, @JunkFood02)
12. Добавяне на многократно предпочитание за височина на реда за страница за четене (#620, @JunkFood02)

View File

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

View File

@ -0,0 +1,8 @@
## 0.11.0
Забележителни промени:
1. Въвеждане на нов четец на статии, базиран на WebView, с поддръжка на Bionic Reading
2. Автоматично маркиране на елементи като прочетени при превъртане
3. Щракнете върху горната лента на приложението, за да се върнете в началото на страницата
4. Добавяне на разделители към лентите с инструменти в страницата за четене
5. Добавяне на бутон за сгъване/разгъване на групите от емисии

View File

@ -3,14 +3,14 @@
<b>Характеристики:</b>
* Абонирайте се за връзки към канали
* Импортиране от OPML
* Внасяне от OPML
* Синхронизиране на статии
* Известие за актуализиране на статия
* Анализирайте пълното съдържание
* Анализ на пълно съдържание
* Филтриране на непрочетени и означени със звезда
* Групиране на емисии
* Локализация
* Експортиране като OPML
* Изнасяне като OPML
* Търсене на статии
Има още…

View 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ů

View 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í

View 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

View File

@ -0,0 +1,8 @@
## 0.11.0
Cambios notables:
1. Introducir un nuevo lector de artículos basado en WebView, con soporte para Bionic Reading.
2. Marcación automática de artículos como leídos al desplazarse
3. Haga clic en la barra superior de la aplicación para volver a la parte superior de la página
4. Añadir separadores a las barras de herramientas en la página de lectura
5. Añadir un botón para contraer/expandir los grupos de noticias

View File

@ -0,0 +1,6 @@
## 0.11.1
Cambios notables:
1. Compatibilidad con fuentes personalizadas para el lector WebView
2. Agregue nuevos estilos visuales a las barras de herramientas en la página de flujo y la página de lectura
3. Mejoras y correcciones de la interfaz de usuario

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