Compare commits

...

73 Commits

Author SHA1 Message Date
8c817b5938 Fetch badges and version on network available. 2022-08-25 12:38:31 +02:00
4857a3d0ac Move Home functionalities to the viewmodel 2022-08-25 12:19:24 +02:00
fbcb428e96 Merge pull request 'Fetch api version on login' (#39) from davidoskky/ReaderForSelfoss-multiplatform:loginApi into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/39
2022-08-25 02:25:27 +00:00
e281751bb0 Fetch api version on login 2022-08-24 23:26:49 +02:00
0eed9a8d07 Translations. 2022-08-24 14:41:57 +02:00
9603860bae Merge pull request 'chore/sonarqube-fixes' (#35) from chore/sonarqube-fixes into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/35
2022-08-24 12:37:37 +00:00
75b566a38d Some cleaning to handle actions on connection restore. 2022-08-24 13:58:24 +02:00
fb572dbb27 Closes #36 2022-08-24 13:34:05 +02:00
34028949d7 More code cleaning. 2022-08-23 22:52:36 +02:00
44a0469b17 Fixes #25; 2022-08-23 22:46:48 +02:00
c87473e8f1 Cache from settings. 2022-08-23 22:45:19 +02:00
de43abf019 Fixed mercury issues. 2022-08-23 22:45:19 +02:00
e60f3a9d91 Push everywhere ? 2022-08-23 22:45:19 +02:00
255fbcb12f Added build cache. 2022-08-23 22:45:19 +02:00
0caeb94e64 Translations. 2022-08-23 22:43:02 +02:00
6f6a42b878 Merge pull request 'DB caching.' (#33) from feature/sqldelight into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/33
2022-08-23 20:39:19 +00:00
67d54f0dd7 Big code cleaning. 2022-08-23 22:16:54 +02:00
437aa0abec Big code cleaning. 2022-08-23 22:03:33 +02:00
216c639a23 Closes #23 2022-08-23 20:47:07 +02:00
7258452625 Closes #23 2022-08-23 20:38:53 +02:00
d0d82751e2 Filtering DB items. 2022-08-23 20:34:20 +02:00
3b8f4991e9 Inserting items in the DB. 2022-08-23 16:56:04 +02:00
2547ce824a Repository should be DB ok. 2022-08-23 16:19:24 +02:00
59eb399cfa File renaming. 2022-08-23 15:17:47 +02:00
495b101355 Replacing room with sqldelight. Big cleaning. 2022-08-23 15:12:01 +02:00
afcc55e907 Urls. 2022-08-22 22:07:52 +02:00
0c570efc47 Urls. 2022-08-22 22:04:07 +02:00
a23a4cea0e Translation. 2022-08-22 21:58:15 +02:00
78cb5d047f Translations. 2022-08-22 21:53:31 +02:00
a9caaefb4d Merge pull request 'network' (#28) from davidoskky/ReaderForSelfoss-multiplatform:network into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/28
2022-08-22 19:01:15 +00:00
4c78b22614 Use the connectivity-status library from the repository rather than the local copy 2022-08-22 19:33:58 +02:00
1d5ab3205e Localize strings 2022-08-21 23:34:47 +02:00
df4903cae5 Include the connectivity status library as a aar file 2022-08-21 23:34:47 +02:00
2a78be69b5 Send toast messages at the application level and not on a per activity basis 2022-08-21 23:34:45 +02:00
8c69bb8c3c Send a message regarding connectivity loss/retrieval on all activities 2022-08-21 23:34:17 +02:00
9203012a97 Include a local copy of the connectivity-status library to solve a bug 2022-08-21 23:34:17 +02:00
2a44162c5a Send toast messages to the home activity on connectivity changes 2022-08-21 23:34:17 +02:00
20588aab81 Add comment to remember the problem with the connectivity-status library 2022-08-21 23:34:17 +02:00
0c8e49214f Don't reset offline override before updating remote 2022-08-21 23:34:17 +02:00
97d5063339 Consider offline override before updating remote 2022-08-21 23:34:17 +02:00
7c37b183d7 Refactor functions 2022-08-21 23:34:17 +02:00
82c4a5a1f9 Don't send toast messages from the repository 2022-08-21 23:34:17 +02:00
47b7062e16 Remove unused function 2022-08-21 23:34:17 +02:00
b9497ca939 Prepare the repository functions for DB implementation 2022-08-21 23:34:17 +02:00
1258ed3ad3 Don't create the mercury api if not connection is available 2022-08-21 23:34:17 +02:00
d838f509d4 Stop monitoring the network when the app goes in background 2022-08-21 23:34:17 +02:00
3c5b606a02 Do not change the network override from within the repository 2022-08-21 23:34:17 +02:00
d1481a1db6 Reintroduce network checks where required 2022-08-21 23:34:17 +02:00
d654b1b0bd Refactor connectivity check 2022-08-21 23:34:17 +02:00
f56861a3c2 Show a message when the network connection is lost 2022-08-21 23:34:17 +02:00
492e7e4aed Update todo comments 2022-08-21 23:34:17 +02:00
551a3e3caa Remove all connectivity checks outside the repository 2022-08-21 23:34:17 +02:00
c224b8a0b3 Remove network checks from the home activity 2022-08-21 23:34:17 +02:00
13ea7a693b Do not fake offline mode when updating remote 2022-08-21 23:34:17 +02:00
0f3c48dd8e Handle the offline override in the repository 2022-08-21 23:34:17 +02:00
d4c2373bac Simplify network connectivity status check 2022-08-21 23:34:17 +02:00
4f32097821 Perform network connectivity checks in the repository 2022-08-21 23:34:17 +02:00
37fa4a1a8e Add multiplatform connectivity check 2022-08-21 23:34:17 +02:00
112194dd4f Merge pull request 'Implement logging in the android application' (#32) from davidoskky/ReaderForSelfoss-multiplatform:logging into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/32
2022-08-20 18:18:48 +00:00
72d9ef92d2 Implement logging in the android application 2022-08-20 12:29:04 +02:00
1392e2a571 Merge pull request 'Fixing some sonarqube issues.' (#30) from chore/sonarqube-fixes into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/30
2022-08-17 19:10:47 +00:00
e9cb3d2f37 Fixing some sonarqube issues. 2022-08-17 21:00:58 +02:00
dec620a409 Merge pull request 'Changed ids to items.' (#29) from id-to-int into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/29
2022-08-17 14:31:11 +00:00
4d29ee0b92 Last fixes. 2022-08-17 16:16:11 +02:00
33333ca998 This may work. 2022-08-17 14:52:03 +02:00
8d87eef0fc More fixes. 2022-08-17 14:24:28 +02:00
5a26513ed7 These params need to be here too. 2022-08-17 14:06:56 +02:00
5b7f5225d8 Can't be detached because of a lock file. 2022-08-17 14:03:32 +02:00
03f53bf9c9 Detached scan 2022-08-17 11:04:44 +02:00
e06e6d580d Detached scan. 2022-08-17 11:00:02 +02:00
63e8649512 Fixing issues with build. 2022-08-17 10:50:04 +02:00
6260c3fc06 Fixes and drone build should work. 2022-08-17 10:43:56 +02:00
77917dd940 Merge pull request 'drone' (#26) from drone into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/26
2022-08-16 19:10:52 +00:00
95 changed files with 1413 additions and 2385 deletions

View File

@ -1,20 +1,21 @@
kind: pipeline
type: docker
name: android
steps:
- name: build
image: mingc/android-build-box:latest
commands:
- ./gradlew build
- name: code-analysis
image: mingc/android-build-box:latest
failure: ignore
commands:
- ls -la
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.sources=. -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\""
environment:
SONAR_HOST_URL:
from_secret: sonarScannerHostUrl
SONAR_LOGIN:
from_secret: sonarScannerLogin
- name: build
image: mingc/android-build-box:latest
commands:
- ./gradlew :androidApp:build -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false

View File

@ -10,7 +10,7 @@ Please read the guidelines before contributing, and follow them (or try to) when
There are many ways to contribute to this project, you could [translate the app](https://crowdin.com/project/readerforselfoss), report bugs, request missing features, suggest enhancements and changes to existing ones. You also can improve the README with useful tips that could help the other users.
You can fork the repository, and [help me solve some issues](https://github.com/aminecmi/ReaderforSelfoss/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://github.com/aminecmi/ReaderforSelfoss/issues)
You can fork the repository, and [help me solve some issues](https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues)
### What I can't help you with.
@ -28,7 +28,7 @@ Always check if the web version of your instance is working.
### Pull requests
* Don't create a PR for translations. See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654) for an explanation why.
* Don't create a PR for translations.
* Please ask before starting to work on an issue. I may be working on it, or someone else could be doing so.
* Each pull request should implement **ONE** feature or bugfix. Keep in mind that you can submit as many PR as you want.
* Your code must be simple and clear enough to avoid using comments to explain what it does.

View File

@ -5,7 +5,7 @@
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] This is **NOT** translation related. (See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654))
- [ ] This is **NOT** translation related.
This closes issue #XXX

2
.gitignore vendored
View File

@ -319,3 +319,5 @@ fabric.properties
# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift
crowdin.properties

View File

@ -22,15 +22,15 @@ If you are a user, you can still create new issues. I'll fix them when I can.
1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/).
2. Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/blob/master/.github/CONTRIBUTING.md).
2. Check the [Contribution guide](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md).
3. Build the project by following [these steps](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/blob/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
3. Build the project by following [these steps](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
## Useful links
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/blob/master/CHANGELOG.md)
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/projects/1)
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/issues)
- [Check what changed](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/CHANGELOG.md)
- [See what I'm doing](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/projects/1)
- [Create an issue, or request a new feature](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/issues)
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
## Contributors (V1) (Alphabetical order) ❤️

View File

@ -1,5 +1,7 @@
import java.io.ByteArrayOutputStream
val ignoreGitVersion: String by project
plugins {
id("com.android.application")
kotlin("android")
@ -32,11 +34,19 @@ fun gitVersion(): String {
}
fun versionCodeFromGit(): Int {
if (ignoreGitVersion == "true") {
// don't care
return 1
}
println("version code " + gitVersion())
return gitVersion().toInt()
}
fun versionNameFromGit(): String {
if (ignoreGitVersion == "true") {
// don't care
return "1"
}
println("version name " + gitVersion())
return gitVersion()
}
@ -68,12 +78,6 @@ android {
// tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf("room.schemaLocation" to "$projectDir/schemas")
}
}
}
buildTypes {
getByName("release") {
@ -171,22 +175,29 @@ dependencies {
implementation("androidx.viewpager2:viewpager2:1.1.0-beta01")
//Dependency Injection
implementation("org.kodein.di:kodein-di:7.12.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.12.0")
implementation("org.kodein.di:kodein-di:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.14.0")
//Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
//Logging
implementation("io.github.aakira:napier:2.6.1")
//PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-common-java8:2.4.0")
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
// implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
// implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
implementation("androidx.room:room-ktx:2.4.0-beta01")
kapt("androidx.room:room-compiler:2.4.0-beta01")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
implementation("android.arch.work:work-runtime-ktx:1.0.1")
// SQLDELIGHT
implementation("com.squareup.sqldelight:android-driver:1.5.3")
}

View File

@ -1,96 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "08ca537d7ac9d4dd216e8e395d70801a",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"08ca537d7ac9d4dd216e8e395d70801a\")"
]
}
}

View File

@ -1,176 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "6fa6944b04100d68eab61039876a8804",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6fa6944b04100d68eab61039876a8804\")"
]
}
}

View File

@ -1,226 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "7ad9c4961992c13b670128485ebb3efc",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "actions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "articleId",
"columnName": "articleid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "read",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unstarred",
"columnName": "unstarred",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"7ad9c4961992c13b670128485ebb3efc\")"
]
}
}

View File

@ -1,226 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "9cf8b03d32f80dfd58160599a1df197d",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "actions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "articleId",
"columnName": "articleid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "read",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unstarred",
"columnName": "unstarred",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9cf8b03d32f80dfd58160599a1df197d\")"
]
}
}

View File

@ -1,226 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "9cf8b03d32f80dfd58160599a1df197d",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "actions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "articleId",
"columnName": "articleid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "read",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unstarred",
"columnName": "unstarred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9cf8b03d32f80dfd58160599a1df197d')"
]
}
}

View File

@ -1,32 +0,0 @@
// TODO
//package bou.amine.apps.readerforselfossv2.android.utils
//
//import bou.amine.apps.readerforselfossv2.android.utils.Config
//import bou.amine.apps.readerforselfossv2.android.utils.parseDate
//import org.junit.Test
//
//class DateUtilsTest {
//
// @Test
// fun parseDateV4() {
//
// Config.apiVersion = 4
// val dateString = "2013-04-07T13:43:00+01:00"
//
// val milliseconds = parseDate(dateString).toEpochMilli()
// val correctMilliseconds : Long = 1365338580000
//
// assert(milliseconds == correctMilliseconds)
// }
//
// @Test
// fun parseDateV1() {
// Config.apiVersion = 0
// val dateString = "2013-04-07 13:43:00"
//
// val milliseconds = parseDate(dateString).toEpochMilli()
// val correctMilliseconds = 1365342180000
//
// assert(milliseconds == correctMilliseconds)
// }
//}

View File

@ -11,6 +11,7 @@ import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.repository.Repository
import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
@ -109,33 +110,40 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
}
fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast.makeText(
this@AddSourceActivity,
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
Toast.LENGTH_SHORT
).show()
mProgress.visibility = View.GONE
}
CoroutineScope(Dispatchers.Main).launch {
val items = repository.getSpouts()
if (items != null) {
try {
val items = repository.getSpouts()
if (items != null) {
val itemsStrings = items.map { it.value.name }
for ((key, value) in items) {
spoutsKV[value.name] = key
}
val itemsStrings = items.map { it.value.name }
for ((key, value) in items) {
spoutsKV[value.name] = key
mProgress.visibility = View.GONE
formContainer.visibility = View.VISIBLE
val spinnerArrayAdapter =
ArrayAdapter(
this@AddSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spoutsSpinner.adapter = spinnerArrayAdapter
} else {
handleSpoutFailure()
}
mProgress.visibility = View.GONE
formContainer.visibility = View.VISIBLE
val spinnerArrayAdapter =
ArrayAdapter(
this@AddSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spoutsSpinner.adapter = spinnerArrayAdapter
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_get_spouts,
Toast.LENGTH_SHORT
).show()
mProgress.visibility = View.GONE
} catch (e: NetworkUnavailableException) {
handleSpoutFailure(networkIssue = true)
}
}
}

View File

@ -17,8 +17,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.doOnNextLayout
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.*
import androidx.room.Room
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
@ -28,15 +28,6 @@ import bou.amine.apps.readerforselfossv2.android.adapters.ItemListAdapter
import bou.amine.apps.readerforselfossv2.android.adapters.ItemsAdapter
import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding
import bou.amine.apps.readerforselfossv2.android.model.getIcon
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabaseService
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
@ -44,12 +35,12 @@ import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toView
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.longHash
import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem
@ -79,12 +70,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
private lateinit var dataBase: AndroidDeviceDatabase
private lateinit var dbService: AndroidDeviceDatabaseService
private val MENU_PREFERENCES = 12302
private val DRAWER_ID_TAGS = 100101L
private val DRAWER_ID_HIDDEN_TAGS = 101100L
@ -126,16 +114,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private var recyclerAdapter: RecyclerView.Adapter<*>? = null
private var fromTabShortcut: Boolean = false
private var offlineShortcut: Boolean = false
private lateinit var tagsBadge: Map<Long, Int>
private lateinit var db: AppDatabase
private lateinit var config: Config
override val di by closestDI()
private val repository : Repository by instance()
private val viewModel: AppViewModel by instance()
data class DrawerData(val tags: List<SelfossModel.Tag>?, val sources: List<SelfossModel.Source>?)
@ -153,13 +139,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val view = binding.root
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
offlineShortcut = intent.getBooleanExtra("startOffline", false)
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) {
elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
}
setContentView(view)
lifecycleScope.launch {
viewModel.refreshingIndicatorProvider.collect { showRefresh ->
binding.swipeRefreshLayout.isRefreshing = showRefresh
}
}
handleThemeBinding()
@ -170,24 +161,29 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.drawerContainer.addDrawerListener(mDrawerToggle)
mDrawerToggle.syncState()
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
customTabActivityHelper = CustomTabActivityHelper()
handleSettings()
dataBase = AndroidDeviceDatabase(applicationContext)
lifecycleScope.launch {
viewModel.items.collect { fetchedItems ->
items = fetchedItems
handleListResult()
}
}
handleBottomBar()
handleDrawer()
handleSwipeRefreshLayout()
handleSettings()
getElementsAccordingToTab()
handleBadgesContent()
CoroutineScope(Dispatchers.IO).launch {
repository.tryToCacheItemsAndGetNewOnes()
}
}
private fun handleSwipeRefreshLayout() {
@ -197,13 +193,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
R.color.refresh_progress_3
)
binding.swipeRefreshLayout.setOnRefreshListener {
offlineShortcut = false
repository.offlineOverride = false
lastFetchDone = false
handleDrawerItems()
CoroutineScope(Dispatchers.Main).launch {
getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false
}
viewModel.getItems(false, elementsShown)
}
val simpleItemTouchCallback =
@ -239,8 +232,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
adapter.handleItemAtIndex(position)
reloadBadgeContent()
val tagHashes = i.tags.map { it.longHash() }
tagsBadge = tagsBadge.map {
if (tagHashes.contains(it.key)) {
@ -272,16 +263,23 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
tabNewBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.setBackgroundColor(appColors.colorPrimary)
if (!displayUnreadCount) {
tabNewBadge.hide(false)
}
tabArchiveBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.setBackgroundColor(appColors.colorPrimary)
tabStarredBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.setBackgroundColor(appColors.colorPrimary)
if (!displayAllCount) {
tabArchiveBadge.hide(false)
tabStarredBadge.hide(false)
}
val tabNew =
BottomNavigationItem(
@ -338,7 +336,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleRecurringTask()
handleOfflineActions()
CoroutineScope(Dispatchers.Main).launch {
repository.handleDBActions()
}
getElementsAccordingToTab()
}
@ -408,6 +408,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val drawerListener = object : DrawerLayout.DrawerListener {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
// We do nothing
}
override fun onDrawerOpened(drawerView: View) {
@ -419,6 +420,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
override fun onDrawerStateChanged(newState: Int) {
// We do nothing
}
}
@ -466,54 +468,61 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
}
// TODO: refactor this.
private fun handleDrawerItems() {
tagsBadge = emptyMap()
fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) {
fun handleDrawerData(maybeDrawerData: DrawerData?) {
fun createDrawerItem(
it: SelfossModel.Tag
) {
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(it.color)
} catch (e: IllegalArgumentException) {
appColors.colorPrimary
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
val drawerItem = PrimaryDrawerItem()
.apply {
nameText = it.tag.getHtmlDecoded()
identifier = it.tag.longHash()
iconDrawable = gd
badgeStyle = BadgeStyle().apply {
textColor = ColorHolder.fromColor(Color.WHITE)
color = ColorHolder.fromColor(appColors.colorAccent)
}
onDrawerItemClickListener = { _, _, _ ->
repository.tagFilter = it
repository.sourceFilter = null
getElementsAccordingToTab()
fetchOnEmptyList()
false
}
}
if (it.unread > 0) {
drawerItem.badgeText = it.unread.toString()
}
binding.mainDrawer.itemAdapter.add(drawerItem)
}
fun handleTags(maybeTags: List<SelfossModel.Tag>?) {
if (maybeTags == null) {
if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem()
.apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false }
)
}
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem()
.apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false }
)
} else {
val filteredTags = maybeTags
.filterNot { hiddenTags.contains(it.tag) }
.sortedBy { it.unread == 0 }
tagsBadge = filteredTags.map {
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(it.color)
} catch (e: IllegalArgumentException) {
appColors.colorPrimary
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
val drawerItem =
PrimaryDrawerItem()
.apply {
nameText = it.getTitleDecoded()
identifier = it.tag.longHash()
iconDrawable = gd
badgeStyle = BadgeStyle().apply {
textColor = ColorHolder.fromColor(Color.WHITE)
color = ColorHolder.fromColor(appColors.colorAccent) }
onDrawerItemClickListener = { _,_,_ ->
repository.tagFilter = it
repository.sourceFilter = null
getElementsAccordingToTab()
fetchOnEmptyList()
false
} }
if (it.unread > 0) {
drawerItem.badgeText = it.unread.toString()
}
binding.mainDrawer.itemAdapter.add(drawerItem)
createDrawerItem(it)
(it.tag.longHash() to it.unread)
}.toMap()
@ -522,49 +531,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fun handleHiddenTags(maybeTags: List<SelfossModel.Tag>?) {
if (maybeTags == null) {
if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_tags
isSelectable = false
}
)
}
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_tags
isSelectable = false
}
)
} else {
val filteredHiddenTags: List<SelfossModel.Tag> =
maybeTags.filter { hiddenTags.contains(it.tag) }
tagsBadge = filteredHiddenTags.map {
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(it.color)
} catch (e: IllegalArgumentException) {
appColors.colorPrimary
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
val drawerItem =
PrimaryDrawerItem().apply {
nameText = it.getTitleDecoded()
identifier = it.tag.longHash()
iconDrawable = gd
badgeStyle = BadgeStyle().apply {
textColor = ColorHolder.fromColor(Color.WHITE)
color = ColorHolder.fromColor(appColors.colorAccent) }
onDrawerItemClickListener = { _,_,_ ->
repository.tagFilter = it
repository.sourceFilter = null
getElementsAccordingToTab()
fetchOnEmptyList()
false
} }
if (it.unread > 0) {
drawerItem.badgeText = it.unread.toString()
}
binding.mainDrawer.itemAdapter.add(drawerItem)
createDrawerItem(it)
(it.tag.longHash() to it.unread)
}.toMap()
@ -573,18 +550,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fun handleSources(maybeSources: List<SelfossModel.Source>?) {
if (maybeSources == null) {
if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_sources
isSelectable = false
}
)
}
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_sources
isSelectable = false
}
)
} else {
for (source in maybeSources) {
val item = PrimaryDrawerItem().apply {
nameText = source.getTitleDecoded()
nameText = source.title.getHtmlDecoded()
identifier = source.id.toLong()
iconUrl = source.getIcon(repository.baseUrl)
onDrawerItemClickListener = { _,_,_ ->
@ -669,70 +644,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
)
if (!loadedFromCache) {
if (maybeDrawerData.tags != null) {
thread {
val tagEntities = maybeDrawerData.tags.map { it.toEntity() }
db.drawerDataDao().deleteAllTags()
db.drawerDataDao().insertAllTags(*tagEntities.toTypedArray())
}
}
if (maybeDrawerData.sources != null) {
thread {
val sourceEntities =
maybeDrawerData.sources.map { it.toEntity() }
db.drawerDataDao().deleteAllSources()
db.drawerDataDao().insertAllSources(*sourceEntities.toTypedArray())
}
}
}
} else {
if (!loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
PrimaryDrawerItem().apply {
nameRes = R.string.no_tags_loaded
identifier = DRAWER_ID_TAGS
isSelectable = false
},
PrimaryDrawerItem().apply {
nameRes = R.string.no_sources_loaded
identifier = DRAWER_ID_SOURCES
isSelectable = false
}
)
}
}
}
fun drawerApiCalls(maybeDrawerData: DrawerData?) {
var tags: List<SelfossModel.Tag>? = null
var sources: List<SelfossModel.Source>?
fun sourcesApiCall() {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
sources = response
val apiDrawerData = DrawerData(tags, sources)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
} else {
val apiDrawerData = DrawerData(tags, null)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
}
binding.mainDrawer.itemAdapter.add(
PrimaryDrawerItem().apply {
nameRes = R.string.no_tags_loaded
identifier = DRAWER_ID_TAGS
isSelectable = false
},
PrimaryDrawerItem().apply {
nameRes = R.string.no_sources_loaded
identifier = DRAWER_ID_SOURCES
isSelectable = false
}
}
}
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
CoroutineScope(Dispatchers.IO).launch {
tags = repository.getTags()
sourcesApiCall()
}
)
}
}
@ -743,12 +667,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
)
thread {
val drawerData = DrawerData(db.drawerDataDao().tags().map { it.toView() },
db.drawerDataDao().sources().map { it.toView() })
CoroutineScope(Dispatchers.IO).launch {
val drawerData = DrawerData(repository.getTags(),
repository.getSources())
runOnUiThread {
handleDrawerData(drawerData, loadedFromCache = true)
drawerApiCalls(drawerData)
handleDrawerData(drawerData)
}
}
}
@ -885,21 +808,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
firstVisible = if (appendResults) firstVisible else 0
getItems(appendResults, elementsShown)
}
private fun getItems(appendResults: Boolean, itemType: ItemType) {
CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType
items = if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
binding.swipeRefreshLayout.isRefreshing = false
handleListResult()
}
viewModel.getItems(appendResults, elementsShown)
}
private fun handleListResult(appendResults: Boolean = false) {
@ -920,7 +829,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemCardAdapter(
this,
items,
db,
customTabActivityHelper,
internalBrowser,
articleViewer,
@ -935,7 +843,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemListAdapter(
this,
items,
db,
customTabActivityHelper,
internalBrowser,
articleViewer,
@ -963,28 +870,50 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() {
if (displayUnreadCount || displayAllCount) {
CoroutineScope(Dispatchers.Main).launch {
if (applicationContext.isNetworkAvailable()) {
repository.reloadBadges()
reloadBadgeContent()
}
CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges()
}
}
}
private fun reloadBadgeContent() {
private fun handleBadgesContent() {
if (displayUnreadCount) {
tabNewBadge
.setText(repository.badgeUnread.toString())
.maybeShow()
lifecycleScope.launch {
repository.badgeUnread.collect { unreadCount ->
if (unreadCount > 0) {
tabNewBadge
.setText(unreadCount.toString())
.maybeShow()
} else {
tabNewBadge.removeBadge()
}
}
}
}
if (displayAllCount) {
tabArchiveBadge
.setText(repository.badgeAll.toString())
.maybeShow()
tabStarredBadge
.setText(repository.badgeStarred.toString())
.maybeShow()
lifecycleScope.launch {
repository.badgeAll.collect { itemsCount ->
if (itemsCount > 0) {
tabArchiveBadge
.setText(itemsCount.toString())
.maybeShow()
} else {
tabArchiveBadge.removeBadge()
}
}
}
lifecycleScope.launch {
repository.badgeStarred.collect { starredCount ->
if (starredCount > 0) {
tabStarredBadge
.setText(starredCount.toString())
.maybeShow()
} else {
tabStarredBadge.removeBadge()
}
}
}
}
}
@ -1041,62 +970,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.refresh -> {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
// TODO: Use Dispatchers.IO
CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote()
if (updatedRemote) {
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG
)
.show()
} else {
Toast.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT
).show()
}
}
}
return true
} else {
return false
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
viewModel.updateRemote()
}
return true
}
R.id.readAll -> {
if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items.map { it.id })
if (success) {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT
).show()
tabNewBadge.removeBadge()
handleDrawerItems()
getElementsAccordingToTab()
} else {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
}
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
}
}
viewModel.markAllAsRead()
}
}
return true
@ -1110,10 +992,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun maxItemNumber(): Int =
when (elementsShown) {
ItemType.UNREAD -> repository.badgeUnread
ItemType.ALL -> repository.badgeAll
ItemType.STARRED -> repository.badgeStarred
else -> repository.badgeUnread // if !elementsShown then unread are fetched.
ItemType.UNREAD -> repository.badgeUnread.value
ItemType.ALL -> repository.badgeAll.value
ItemType.STARRED -> repository.badgeStarred.value
else -> repository.badgeUnread.value // if !elementsShown then unread are fetched.
}
private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
@ -1137,30 +1019,4 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
WorkManager.getInstance(baseContext).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
}
}
private fun handleOfflineActions() {
fun doAndReportOnFail(success: Boolean, action: ActionEntity) {
if (success) {
thread {
db.actionsDao().delete(action)
}
}
}
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch {
val actions = db.actionsDao().actions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action)
action.unread -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action)
action.starred -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action)
action.unstarred -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action)
}
}
}
}
}
}

View File

@ -15,7 +15,6 @@ import androidx.appcompat.app.AppCompatActivity
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository
import com.mikepenz.aboutlibraries.LibsBuilder
import com.russhwolf.settings.Settings
@ -202,15 +201,13 @@ class LoginActivity() : AppCompatActivity(), DIAware {
repository.refreshLoginInformation(url, login, password, httpLogin, httpPassword, isWithSelfSignedCert)
if (this@LoginActivity.isNetworkAvailable(this@LoginActivity.findViewById(R.id.loginForm))) {
CoroutineScope(Dispatchers.IO).launch {
val result = repository.login()
if (result) {
goToMain()
} else {
CoroutineScope(Dispatchers.Main).launch {
preferenceError(Exception("Not success"))
}
CoroutineScope(Dispatchers.IO).launch {
val result = repository.login()
if (result) {
goToMain()
} else {
CoroutineScope(Dispatchers.Main).launch {
preferenceError(Exception("Not success"))
}
}
}

View File

@ -7,32 +7,54 @@ import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.widget.ImageView
import android.widget.Toast
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.repository.Repository
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
import com.github.ln_12.library.ConnectivityStatus
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.russhwolf.settings.Settings
import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.*
class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy {
import(networkModule)
bind<Repository>() with singleton { Repository(instance(), instance()) }
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with singleton { Repository(instance(), instance(), connectivityStatus, instance()) }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
}
private val repository: Repository by instance()
private val viewModel: AppViewModel by instance()
private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance()
private lateinit var config: Config
private lateinit var settings : Settings
override fun onCreate() {
super.onCreate()
Napier.base(DebugAntilog())
config = Config()
settings = Settings()
@ -43,6 +65,35 @@ class MyApp : MultiDexApplication(), DIAware {
tryToHandleBug()
handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable ->
val toastMessage = if (networkAvailable) {
repository.handleDBActions()
R.string.network_connectivity_retrieved
} else {
R.string.network_connectivity_lost
}
Toast.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
}
CoroutineScope(Dispatchers.Main).launch {
viewModel.toastMessageProvider.collect { toastMessage ->
Toast.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
}
}
private fun handleNotificationChannels() {
@ -102,4 +153,19 @@ class MyApp : MultiDexApplication(), DIAware {
}
}
}
class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
repository.connectionMonitored = true
connectivityStatus.start()
}
override fun onPause(owner: LifecycleOwner) {
repository.connectionMonitored = false
connectivityStatus.stop()
super.onPause(owner)
}
}
}

View File

@ -8,20 +8,14 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.room.Room
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityReaderBinding
import bou.amine.apps.readerforselfossv2.android.fragments.ArticleFragment
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.toggleStar
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import com.ftinc.scoop.Scoop
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope
@ -39,7 +33,6 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private lateinit var toolbarMenu: Menu
private lateinit var db: AppDatabase
private lateinit var binding: ActivityReaderBinding
private var activeAlignment: Int = 1
@ -47,7 +40,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private val ALIGN_LEFT = 2
override val di by closestDI()
private val repository : Repository by instance()
private val repository: Repository by instance()
private fun showMenuItem(willAddToFavorite: Boolean) {
if (willAddToFavorite) {
@ -75,11 +68,6 @@ class ReaderActivity : AppCompatActivity(), DIAware {
setContentView(view)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
@ -111,11 +99,11 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private fun readItem(item: SelfossModel.Item) {
if (markOnScroll) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item.id)
// TODO: Handle failure
}
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item)
// TODO: Handle failure
}
}
}
override fun onSaveInstanceState(oldInstanceState: Bundle) {
@ -128,19 +116,22 @@ class ReaderActivity : AppCompatActivity(), DIAware {
override fun getItemCount(): Int = allItems.size
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
override fun createFragment(position: Int): Fragment =
ArticleFragment.newInstance(allItems[position])
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
val currentFragment = supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.scrollDown()
true
}
KeyEvent.KEYCODE_VOLUME_UP -> {
val currentFragment = supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.scrollUp()
true
}
@ -169,19 +160,19 @@ class ReaderActivity : AppCompatActivity(), DIAware {
alignmentMenu()
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
readItem(allItems[position])
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
readItem(allItems[position])
}
}
)
return true
@ -190,7 +181,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() {
allItems[binding.pager.currentItem] =
allItems[binding.pager.currentItem].toggleStar()
allItems[binding.pager.currentItem].toggleStar()
canRemoveFromFavorite()
}
@ -207,13 +198,13 @@ class ReaderActivity : AppCompatActivity(), DIAware {
R.id.star -> {
if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem].id)
repository.unstarr(allItems[binding.pager.currentItem])
// TODO: Handle failure
}
afterUnsave()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem].id)
repository.starr(allItems[binding.pager.currentItem])
// TODO: Handle failure
}
afterSave()

View File

@ -10,9 +10,8 @@ import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -64,30 +63,28 @@ class SourcesActivity : AppCompatActivity(), DIAware {
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
items = response
val mAdapter = SourcesListAdapter(
this@SourcesActivity, items
)
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
this@SourcesActivity,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
} else {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
items = response
val mAdapter = SourcesListAdapter(
this@SourcesActivity, items
)
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
} else {
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT
).show()
}
}

View File

@ -10,15 +10,16 @@ import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide
@ -32,7 +33,6 @@ import org.kodein.di.instance
class ItemCardAdapter(
override val app: Activity,
override var items: ArrayList<SelfossModel.Item>,
override val db: AppDatabase,
private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean,
private val articleViewer: Boolean,
@ -59,7 +59,7 @@ class ItemCardAdapter(
val itm = items[position]
binding.favButton.isSelected = itm.starred
binding.title.text = itm.getTitleDecoded()
binding.title.text = itm.title.getHtmlDecoded()
binding.title.setOnTouchListener(LinkOnTouchListener())
@ -82,13 +82,13 @@ class ItemCardAdapter(
}
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val color = generator.getColor(itm.title.getHtmlDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.sourceImage)
@ -110,28 +110,26 @@ class ItemCardAdapter(
binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition]
if (c.isNetworkAvailable()) {
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item.id)
// TODO: Handle failure
}
item.starred = false
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item.id)
// TODO: Handle failure
}
item.starred = true
binding.favButton.isSelected = true
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item)
// TODO: Handle failure
}
item.starred = false
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item)
// TODO: Handle failure
}
item.starred = true
binding.favButton.isSelected = true
}
}
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
}
binding.browserBtn.setOnClickListener {

View File

@ -7,14 +7,16 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import org.kodein.di.DI
@ -24,7 +26,6 @@ import org.kodein.di.instance
class ItemListAdapter(
override val app: Activity,
override var items: ArrayList<SelfossModel.Item>,
override val db: AppDatabase,
private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean,
private val articleViewer: Boolean,
@ -47,7 +48,7 @@ class ItemListAdapter(
with(holder) {
val itm = items[position]
binding.title.text = itm.getTitleDecoded()
binding.title.text = itm.title.getHtmlDecoded()
binding.title.setOnTouchListener(LinkOnTouchListener())
@ -58,13 +59,13 @@ class ItemListAdapter(
if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val color = generator.getColor(itm.title.getHtmlDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.itemImage.setImageDrawable(drawable)
} else {

View File

@ -5,11 +5,10 @@ import android.graphics.Color
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.ItemType
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
@ -20,7 +19,6 @@ import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
abstract var items: ArrayList<SelfossModel.Item>
abstract val repository: Repository
abstract val db: AppDatabase
abstract val app: Activity
abstract val appColors: AppColors
abstract val config: Config
@ -79,7 +77,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) {
val i = items[position]
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(i.id)
repository.markAsRead(i)
}
if (repository.displayedItems == ItemType.UNREAD) {
items.remove(i)
@ -95,7 +93,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(items[position].id)
repository.unmarkAsRead(items[position])
// Todo: SharedItems.unreadItem(app, api, db, items[position])
// TODO: update db

View File

@ -10,14 +10,13 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.getIcon
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import kotlinx.coroutines.CoroutineScope
@ -50,19 +49,19 @@ class SourcesListAdapter(
config = Config()
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.getTitleDecoded())
val color = generator.getColor(itm.title.getHtmlDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getTitleDecoded().toTextDrawableString(c), color)
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
}
binding.sourceTitle.text = itm.getTitleDecoded()
binding.sourceTitle.text = itm.title.getHtmlDecoded()
}
override fun getItemCount(): Int = items.size
@ -78,21 +77,19 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener {
if (c.isNetworkAvailable(null)) {
val (id) = items[adapterPosition]
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id)
if (successfullyDeletedSource) {
items.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
val (id) = items[adapterPosition]
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id)
if (successfullyDeletedSource) {
items.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
}
}

View File

@ -22,7 +22,7 @@ class MercuryApi() {
val retrofit =
Retrofit
.Builder()
.baseUrl("https://www.amine-bou.fr")
.baseUrl("https://www.amine-louveau.fr")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()

View File

@ -8,23 +8,17 @@ import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationCompat.PRIORITY_LOW
import androidx.room.Room
import androidx.work.Worker
import androidx.work.WorkerParameters
import bou.amine.apps.readerforselfossv2.android.MainActivity
import bou.amine.apps.readerforselfossv2.android.MyApp
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.model.preloadImages
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
import bou.amine.apps.readerforselfossv2.dao.ACTION
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.ItemType
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -36,7 +30,6 @@ import kotlin.concurrent.schedule
import kotlin.concurrent.thread
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params), DIAware {
lateinit var db: AppDatabase
override val di by lazy { (applicationContext as MyApp).di }
private val repository : Repository by instance()
@ -44,67 +37,29 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
override fun doWork(): Result {
val settings = Settings()
val periodicRefresh = settings.getBoolean("periodic_refresh", false)
if (periodicRefresh) {
if (periodicRefresh && isNetworkAccessible(context)) {
if (context.isNetworkAvailable()) {
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification =
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
val notification =
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build())
notificationManager.notify(1, notification.build())
val notifyNewItems = settings.getBoolean("notify_new_items", false)
val notifyNewItems = settings.getBoolean("notify_new_items", false)
repository.handleDBActions()
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4).build()
val actions = db.actionsDao().actions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(
repository.markAsRead(action.articleId.toInt()),
action
)
action.unread -> doAndReportOnFail(
repository.unmarkAsRead(action.articleId.toInt()),
action
)
action.starred -> doAndReportOnFail(
repository.starr(action.articleId.toInt()),
action
)
action.unstarred -> doAndReportOnFail(
repository.unstarr(action.articleId.toInt()),
action
)
}
}
if (context.isNetworkAvailable()) {
launch {
try {
val newItems = repository.allItems(ItemType.UNREAD)
handleNewItemsNotification(newItems, notifyNewItems, notificationManager)
val readItems = repository.allItems(ItemType.ALL)
val starredItems = repository.allItems(ItemType.STARRED)
// TODO: save all to DB
} catch (e: Throwable) {}
}
}
launch {
handleNewItemsNotification(repository.tryToCacheItemsAndGetNewOnes(), notifyNewItems, notificationManager)
}
}
}
@ -158,13 +113,4 @@ override fun doWork(): Result {
}
}
}
private fun doAndReportOnFail(result: Boolean, action: ActionEntity) {
// TODO: Failures should be reported
if (result) {
thread {
db.actionsDao().delete(action)
}
}
}
}

View File

@ -19,25 +19,22 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.res.ResourcesCompat
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.room.Room
import bou.amine.apps.readerforselfossv2.android.ImageActivity
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi
import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -72,7 +69,6 @@ class ArticleFragment : Fragment(), DIAware {
private lateinit var allImages : ArrayList<String>
private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors
private lateinit var db: AppDatabase
private lateinit var textAlignment: String
private lateinit var config: Config
private var _binding: FragmentArticleBinding? = null
@ -104,11 +100,6 @@ class ArticleFragment : Fragment(), DIAware {
val pi: ParecelableItem = requireArguments().getParcelable(ARG_ITEMS)!!
item = pi.toModel()
db = Room.databaseBuilder(
requireContext(),
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
}
override fun onCreateView(
@ -121,7 +112,7 @@ class ArticleFragment : Fragment(), DIAware {
url = item.getLinkDecoded()
contentText = item.content
contentTitle = item.getTitleDecoded()
contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl)
contentSource = item.sourceAndDateText(repository.dateUtils)
allImages = item.getImages()
@ -135,7 +126,6 @@ class ArticleFragment : Fragment(), DIAware {
typeface = try {
ResourcesCompat.getFont(requireContext(), resId)!!
} catch (e: java.lang.Exception) {
// ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext())
// Just to be sure
null
}
@ -169,7 +159,7 @@ class ArticleFragment : Fragment(), DIAware {
R.id.unread_action -> if (context != null) {
if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item.id)
repository.markAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = false
Toast.makeText(
@ -179,7 +169,7 @@ class ArticleFragment : Fragment(), DIAware {
).show()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(this@ArticleFragment.item.id)
repository.unmarkAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = true
Toast.makeText(
@ -276,9 +266,9 @@ class ArticleFragment : Fragment(), DIAware {
}
private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) {
if ((context != null && requireContext().isNetworkAvailable(null)) || context == null) {
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
if (repository.isNetworkAvailable()) {
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> {
@ -317,7 +307,10 @@ class ArticleFragment : Fragment(), DIAware {
Glide
.with(requireContext())
.asBitmap()
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.loadMaybeBasicAuth(
config,
response.body()!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} catch (e: Exception) {

View File

@ -1,43 +1,12 @@
package bou.amine.apps.readerforselfossv2.android.model
import android.content.Context
import android.net.Uri
import android.text.Html
import android.webkit.URLUtil
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.getImages
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import org.jsoup.Jsoup
import java.util.*
/**
* Items extension methods
*/
fun SelfossModel.Item.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
return constructUrl(baseUrl, "thumbnails", thumbnail)
}
fun SelfossModel.Item.getImages() : ArrayList<String> {
val allImages = ArrayList<String>()
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages()
@ -60,66 +29,14 @@ fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
return true
}
fun SelfossModel.Item.getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
fun SelfossModel.Item.getSourceTitle(): String {
return Html.fromHtml(sourcetitle).toString()
}
// TODO: maybe find a better way to handle these kind of urls
fun SelfossModel.Item.getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
fun String.toTextDrawableString(): String {
val textDrawable = StringBuilder()
for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) {
try {
textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) {
// We do nothing
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
/**
* Sources extension methods
*/
fun SelfossModel.Source.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun SelfossModel.Source.getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
/**
* Common methods
*/
private fun constructUrl(baseUrl: String, path: String, file: String?): String {
return if (file == null || file == "null" || file.isEmpty()) {
""
} else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString()
}
return textDrawable.toString()
}

View File

@ -1,10 +1,8 @@
package bou.amine.apps.readerforselfossv2.android.model
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.RequiresApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import com.google.gson.annotations.SerializedName
fun SelfossModel.Item.toParcelable() : ParecelableItem =

View File

@ -1,28 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence
import android.content.Context
import androidx.room.Room
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.dao.DeviceDatabase
class AndroidDeviceDatabase(applicationContext: Context): DeviceDatabase<AndroidItemEntity> {
var db: AppDatabase = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
override suspend fun items(): List<AndroidItemEntity> = db.itemsDao().items()
override suspend fun insertAllItems(vararg items: AndroidItemEntity) = db.itemsDao().insertAllItems(*items)
override suspend fun deleteAllItems() = db.itemsDao().deleteAllItems()
override suspend fun delete(item: AndroidItemEntity) = db.itemsDao().delete(item)
override suspend fun updateItem(item: AndroidItemEntity) = db.itemsDao().updateItem(item)
}

View File

@ -1,40 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.DeviceDataBaseService
import bou.amine.apps.readerforselfossv2.service.SearchService
class AndroidDeviceDatabaseService(db: AndroidDeviceDatabase, searchService: SearchService) :
DeviceDataBaseService<AndroidItemEntity>(db, searchService) {
override suspend fun updateDatabase() {
if (itemsCaching) {
if (items.isEmpty()) {
getFromDB()
}
db.deleteAllItems()
db.insertAllItems(*(items.map { it.toEntity() }).toTypedArray())
}
}
override suspend fun clearDBItems() {
db.deleteAllItems()
}
override fun appendNewItems(newItems: List<SelfossModel.Item>) {
var oldItems = items
if (oldItems != newItems) {
oldItems = oldItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList<SelfossModel.Item>
oldItems.addAll(newItems)
items = oldItems
sortItems()
getFocusedItems()
}
}
override fun getFromDB() {
TODO("Not yet implemented")
}
}

View File

@ -1,23 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
@Dao
interface ActionsDao {
@Query("SELECT * FROM actions order by id asc")
suspend fun actions(): List<ActionEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllActions(vararg actions: ActionEntity)
@Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1")
fun deleteReadActionForArticle(article_id: String)
@Delete
fun delete(action: ActionEntity)
}

View File

@ -1,36 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.dao
import androidx.room.Delete
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity
@Dao
interface DrawerDataDao {
@Query("SELECT * FROM tags")
fun tags(): List<TagEntity>
@Query("SELECT * FROM sources")
fun sources(): List<SourceEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllTags(vararg tags: TagEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllSources(vararg sources: SourceEntity)
@Query("DELETE FROM tags")
fun deleteAllTags()
@Query("DELETE FROM sources")
fun deleteAllSources()
@Delete
fun deleteTag(tag: TagEntity)
@Delete
fun deleteSource(source: SourceEntity)
}

View File

@ -1,29 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import androidx.room.Update
@Dao
interface ItemsDao {
@Query("SELECT * FROM items order by id desc")
suspend fun items(): List<AndroidItemEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllItems(vararg items: AndroidItemEntity)
@Query("DELETE FROM items")
suspend fun deleteAllItems()
@Delete
suspend fun delete(item: AndroidItemEntity)
@Update
suspend fun updateItem(item: AndroidItemEntity)
}

View File

@ -1,20 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.database
import androidx.room.RoomDatabase
import androidx.room.Database
import bou.amine.apps.readerforselfossv2.android.persistence.dao.ActionsDao
import bou.amine.apps.readerforselfossv2.android.persistence.dao.DrawerDataDao
import bou.amine.apps.readerforselfossv2.android.persistence.dao.ItemsDao
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity
@Database(entities = [TagEntity::class, SourceEntity::class, AndroidItemEntity::class, ActionEntity::class], version = 4)
abstract class AppDatabase : RoomDatabase() {
abstract fun drawerDataDao(): DrawerDataDao
abstract fun itemsDao(): ItemsDao
abstract fun actionsDao(): ActionsDao
}

View File

@ -1,22 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "actions")
data class ActionEntity(
@ColumnInfo(name = "articleid")
val articleId: String,
@ColumnInfo(name = "read")
val read: Boolean,
@ColumnInfo(name = "unread")
val unread: Boolean,
@ColumnInfo(name = "starred")
var starred: Boolean,
@ColumnInfo(name = "unstarred")
var unstarred: Boolean
) {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
}

View File

@ -1,33 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
@Entity(tableName = "items")
data class AndroidItemEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String,
@ColumnInfo(name = "datetime")
val datetime: String,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "content")
val content: String,
@ColumnInfo(name = "unread")
val unread: Boolean,
@ColumnInfo(name = "starred")
var starred: Boolean,
@ColumnInfo(name = "thumbnail")
val thumbnail: String?,
@ColumnInfo(name = "icon")
val icon: String?,
@ColumnInfo(name = "link")
val link: String,
@ColumnInfo(name = "sourcetitle")
val sourcetitle: String,
@ColumnInfo(name = "tags")
val tags: String
)

View File

@ -1,33 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tags")
data class TagEntity(
@PrimaryKey
@ColumnInfo(name = "tag")
val tag: String,
@ColumnInfo(name = "color")
val color: String,
@ColumnInfo(name = "unread")
val unread: Int
)
@Entity(tableName = "sources")
data class SourceEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "tags")
val tags: String,
@ColumnInfo(name = "spout")
val spout: String,
@ColumnInfo(name = "error")
val error: String,
@ColumnInfo(name = "icon")
val icon: String
)

View File

@ -1,34 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.migrations
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.room.migration.Migration
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `items` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
val MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))")
}
}
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
// @see https://stackoverflow.com/questions/57392015/how-to-migrate-not-null-table-column-into-null-in-android-room-database
// Create the new table
database.execSQL("CREATE TABLE IF NOT EXISTS `itemstmp` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
// Copy the data
database.execSQL(
"INSERT INTO itemstmp (`id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags`) SELECT `id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags` FROM items")
// Remove the old table
database.execSQL("DROP TABLE items")
// Change the table name to the correct one
database.execSQL("ALTER TABLE itemstmp RENAME TO items")
}
}

View File

@ -46,16 +46,16 @@ class AppColors(a: Activity) {
colorBackground = if (isDarkTheme) {
a.setTheme(R.style.NoBarDark)
R.color.darkBackground
a.resources.getColor(R.color.darkBackground)
} else {
a.setTheme(R.style.NoBar)
R.color.grey_50
a.resources.getColor(R.color.grey_50)
}
textColor = if (isDarkTheme) {
R.color.white
a.resources.getColor(R.color.white)
} else {
R.color.grey_900
a.resources.getColor(R.color.grey_900)
}
}
}

View File

@ -28,13 +28,13 @@ class Config {
companion object {
const val settingsName = "paramsselfoss"
const val feedbackEmail = "aminecmi@gmail.com"
const val feedbackEmail = "aminecmi@pm.me.com"
const val translationUrl = "https://crwd.in/readerforselfoss"
const val sourceUrl = "https://github.com/aminecmi/ReaderforSelfoss"
const val sourceUrl = "https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform"
const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues"
const val trackerUrl = "https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
const val syncChannelId = "sync-channel-id"

View File

@ -1,29 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.content.Context
import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.parseRelativeDate
fun String.toTextDrawableString(c: Context): String {
val textDrawable = StringBuilder()
for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) {
try {
textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) {
}
}
return textDrawable.toString()
}
fun SelfossModel.Item.sourceAndDateText(dateUtils: DateUtils): String {
val formattedDate = parseRelativeDate(dateUtils)
return getSourceTitle() + formattedDate
}
fun SelfossModel.Item.toggleStar(): SelfossModel.Item {
this.starred = !this.starred
return this
}

View File

@ -18,9 +18,8 @@ import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.ReaderActivity
import bou.amine.apps.readerforselfossv2.android.model.getLinkDecoded
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull

View File

@ -1,9 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.content.res.Resources
val Int.toPx: Int
get() = (this * Resources.getSystem().displayMetrics.density).toInt()
val Int.toDp: Int
get() = (this / Resources.getSystem().displayMetrics.density).toInt()

View File

@ -1,52 +1,14 @@
package bou.amine.apps.readerforselfossv2.android.utils.network
import android.content.Context
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.view.View
import android.widget.TextView
import bou.amine.apps.readerforselfossv2.android.R
import com.google.android.material.snackbar.Snackbar
var snackBarShown = false
var view: View? = null
lateinit var s: Snackbar
fun Context.isNetworkAvailable(
v: View? = null,
overrideOffline: Boolean = false
): Boolean {
val networkIsAccessible = isNetworkAccessible(this)
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
view = v
s = Snackbar
.make(
v,
R.string.no_network_connectivity,
Snackbar.LENGTH_INDEFINITE
)
s.setAction(android.R.string.ok) {
snackBarShown = false
s.dismiss()
}
val view = s.view
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
s.show()
snackBarShown = true
}
if (snackBarShown && networkIsAccessible && !overrideOffline) {
s.dismiss()
}
return if(overrideOffline) overrideOffline else networkIsAccessible
}
private fun isNetworkAccessible(context: Context): Boolean {
fun isNetworkAccessible(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

View File

@ -1,72 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils.persistence
import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
fun TagEntity.toView(): SelfossModel.Tag =
SelfossModel.Tag(
this.tag,
this.color,
this.unread
)
fun SourceEntity.toView(): SelfossModel.Source =
SelfossModel.Source(
this.id.toInt(),
this.title,
this.tags.split(","),
this.spout,
this.error,
this.icon
)
fun SelfossModel.Source.toEntity(): SourceEntity =
SourceEntity(
this.id.toString(),
this.getTitleDecoded(),
this.tags.joinToString(","),
this.spout,
this.error,
this.icon.orEmpty()
)
fun SelfossModel.Tag.toEntity(): TagEntity =
TagEntity(
this.tag,
this.color,
this.unread
)
fun AndroidItemEntity.toView(): SelfossModel.Item =
SelfossModel.Item(
this.id.toInt(),
this.datetime,
this.title,
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.sourcetitle,
this.tags.split(",")
)
fun SelfossModel.Item.toEntity(): AndroidItemEntity =
AndroidItemEntity(
this.id.toString(),
this.datetime,
this.getTitleDecoded(),
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.getSourceTitle(),
this.tags.joinToString(",")
)

View File

@ -0,0 +1,83 @@
package bou.amine.apps.readerforselfossv2.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.ItemType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class AppViewModel(private val repository: Repository) : ViewModel() {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private val _refreshingIndicatorProvider = MutableSharedFlow<Boolean>()
val refreshingIndicatorProvider = _refreshingIndicatorProvider.asSharedFlow()
private val _toastMessageProvider = MutableSharedFlow<Int>()
val toastMessageProvider = _toastMessageProvider.asSharedFlow()
private var wasConnected = true
private val _items = MutableStateFlow(ArrayList<SelfossModel.Item>())
val items = _items.asStateFlow()
init {
viewModelScope.launch {
repository.isConnectionAvailable.collect { isConnected ->
if (repository.connectionMonitored) {
if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true)
wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored){
_networkAvailableProvider.emit(false)
wasConnected = false
}
}
}
}
}
fun updateRemote() {
CoroutineScope(Dispatchers.IO).launch {
_toastMessageProvider.emit(R.string.refresh_in_progress)
val updatedRemote = repository.updateRemote()
if (updatedRemote) {
_toastMessageProvider.emit(R.string.refresh_success_response)
} else {
_toastMessageProvider.emit(R.string.refresh_failer_message)
}
}
}
fun getItems(appendResults: Boolean, itemType: ItemType) {
CoroutineScope(Dispatchers.Main).launch {
_refreshingIndicatorProvider.emit(true)
repository.displayedItems = itemType
val items = if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
_items.emit(items)
_refreshingIndicatorProvider.emit(false)
}
}
fun markAllAsRead() {
CoroutineScope(Dispatchers.IO).launch {
_refreshingIndicatorProvider.emit(true)
val success = repository.markAllAsRead(items.value)
if (success) {
_toastMessageProvider.emit(R.string.all_posts_read)
} else {
_toastMessageProvider.emit(R.string.all_posts_not_read)
}
_refreshingIndicatorProvider.emit(false)
}
}
}

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Inicieu la sessió per afegir fonts."</string>
<string name="cant_get_sources">"No es pot obtenir la llista de fonts."</string>
<string name="cant_create_source">"No es pot crear la font."</string>
<string name="cant_get_spouts">"No es pot obtenir la llista de canals."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"El formulari no està complet"</string>
<string name="pref_header_links">"Enllaços"</string>
<string name="issue_tracker_link">"Detector de problemes"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Sense connexió!</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sincronitza els articles</string>
<string name="pref_switch_periodic_refresh_off">Els articles no se sincronitzaran en segon pla</string>
<string name="pref_switch_periodic_refresh_on">Els articles se sincronitzaran periòdicament</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Melde dich an um Quellen hinzuzufügen."</string>
<string name="cant_get_sources">"Quellen können nicht abgerufen werden."</string>
<string name="cant_create_source">"Quelle kann nicht gespeichert werden."</string>
<string name="cant_get_spouts">"Can't get spouts list."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"Das Formular ist nicht vollständig"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Issue Tracker"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Nicht verbunden !</string>
<string name="network_connectivity_lost">"Die Netzwerkverbindung wurde unterbrochen"</string>
<string name="network_connectivity_retrieved">"Netzwerkverbindung ist jetzt verfügbar"</string>
<string name="pref_switch_periodic_refresh">Synchronisiere Artikel</string>
<string name="pref_switch_periodic_refresh_off">Artikel werden nicht im Hintergrund synchronisiert</string>
<string name="pref_switch_periodic_refresh_on">Die Artikel werden regelmäßig synchronisiert</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Iniciar sesión para añadir fuentes."</string>
<string name="cant_get_sources">"No se puede obtener la lista de fuentes."</string>
<string name="cant_create_source">"No se puede crear la fuente."</string>
<string name="cant_get_spouts">"No se puede obtener la lista de fuentes."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"El formulario no está completo"</string>
<string name="pref_header_links">"Enlaces"</string>
<string name="issue_tracker_link">"Rastreador de Incidencias"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Sin conexión!</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sincronizar artículos</string>
<string name="pref_switch_periodic_refresh_off">Los artículos no se sincronizarán en segundo plano</string>
<string name="pref_switch_periodic_refresh_on">Los artículos se sincronizarán periódicamente</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Log in to add sources."</string>
<string name="cant_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</string>
<string name="cant_get_spouts">"Can't get spouts list."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"The form is not complete"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Issue Tracker"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,13 +40,14 @@
<string name="addStringNoUrl">"Identifiez-vous pour ajouter une source."</string>
<string name="cant_get_sources">"Impossible de récupérer la liste des sources"</string>
<string name="cant_create_source">"Impossible de créer la source."</string>
<string name="cant_get_spouts">"Impossible de récupérer vos Spouts pour rajouter des sources"</string>
<string name="cant_get_spouts_no_network">"Impossible d'obtenir la liste des spouts en raison d'un problème de réseau."</string>
<string name="cant_get_spouts">"Impossible d'obtenir la liste des spouts. Cela pourrait venir de l'api."</string>
<string name="form_not_complete">"Il manque des données. Terminez le formulaire."</string>
<string name="pref_header_links">"Liens utiles"</string>
<string name="issue_tracker_link">"Suivi des problèmes"</string>
<string name="issue_tracker_summary">"Pour signaler un bug ou demander une nouvelle fonctionnalité"</string>
<string name="warning_wrong_url">"ATTENTION"</string>
<string name="pref_switch_card_view_title">"Card View"</string>
<string name="pref_switch_card_view_title">"Vue en carte"</string>
<string name="cant_mark_favortie">"Impossible de marquer l'élément comme favoris"</string>
<string name="cant_unmark_favortie">"Impossible de retirer l'élément des favoris"</string>
<string name="share">"Partager"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Vérifier les nouvelles sources et tags</string>
<string name="pref_switch_update_sources_summary">Désactivez cette option si votre serveur reçoit trop de requêtes.</string>
<string name="no_network_connectivity">Hors connexion !</string>
<string name="network_connectivity_lost">"Connexion au réseau perdue"</string>
<string name="network_connectivity_retrieved">"Connexion réseau de nouveau disponible"</string>
<string name="pref_switch_periodic_refresh">Synchroniser les articles</string>
<string name="pref_switch_periodic_refresh_off">Les articles ne seront pas synchronisés en arrière plan</string>
<string name="pref_switch_periodic_refresh_on">Articles seront périodiquement synchronisées</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Accede pra engadir fontes."</string>
<string name="cant_get_sources">"Non se pode obter a lista de fontes."</string>
<string name="cant_create_source">"Non se pode crear unha fonte."</string>
<string name="cant_get_spouts">"Non se pode obter a lista de fontes."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"O formulario non está completo"</string>
<string name="pref_header_links">"Ligazóns"</string>
<string name="issue_tracker_link">"Rastrexador de Incidencias"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Comproba novas fontes e etiquetas</string>
<string name="pref_switch_update_sources_summary">Deshabilita isto se o teu servidor está recibindo demasiadas peticións de base de datos.</string>
<string name="no_network_connectivity">Non conectado!</string>
<string name="network_connectivity_lost">"Perdeuse a conexión de rede"</string>
<string name="network_connectivity_retrieved">"Conexión de rede xa dispoñíbel"</string>
<string name="pref_switch_periodic_refresh">Sincronizar artigos</string>
<string name="pref_switch_periodic_refresh_off">Os artigos non se sincronizarán coa aplicación de fondo</string>
<string name="pref_switch_periodic_refresh_on">Os artigos sincronizaranse periódicamente</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Masuk untuk menambah sumber."</string>
<string name="cant_get_sources">"Tidak bisa mendapatkan daftar sumber."</string>
<string name="cant_create_source">"Tidak dapat membuat sumber."</string>
<string name="cant_get_spouts">"Tidak bisa masuk ke daftar Spouts."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"Formulirnya belum selesai"</string>
<string name="pref_header_links">"Tautan"</string>
<string name="issue_tracker_link">"Pelacak Masalah"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Autenticati per aggiungere fonti."</string>
<string name="cant_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</string>
<string name="cant_get_spouts">"Can't get spouts list."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"Il modulo non è completo"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Traccia problemi"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"로그인 소스를 추가 해야 합니다."</string>
<string name="cant_get_sources">"소스 리스트를 얻을 수 없습니다."</string>
<string name="cant_create_source">"소스를 만들 수 없습니다."</string>
<string name="cant_get_spouts">"Spouts 목록을 가져올 수 없습니다."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"양식이 완료되지 않았습니다."</string>
<string name="pref_header_links">"링크"</string>
<string name="issue_tracker_link">"이슈 트래커"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Login om bronnen toe te voegen"</string>
<string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string>
<string name="cant_create_source">"Kan bron niet creëeren"</string>
<string name="cant_get_spouts">"Ophalen spouts mislukt"</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"Formulier is niet volledig ingevuld"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Bug tracker"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Faça login para adicionar fontes."</string>
<string name="cant_get_sources">"Não é possível obter a lista de fontes."</string>
<string name="cant_create_source">"Não é possível criar fonte."</string>
<string name="cant_get_spouts">"Não é possível obter a lista de spouts."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"O formulário não está completo"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Rastreador de problemas"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Logar para adicionar fontes."</string>
<string name="cant_get_sources">"Não é possível obter a lista de fontes."</string>
<string name="cant_create_source">"Não é possível criar a fonte."</string>
<string name="cant_get_spouts">"Não é possível obter a lista de bicos."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"O formulário não está completo"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Rastreador de problemas"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Log in to add sources."</string>
<string name="cant_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</string>
<string name="cant_get_spouts">"Can't get spouts list."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"The form is not complete"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Issue Tracker"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"Kaynakları eklemek için giriş yapın."</string>
<string name="cant_get_sources">"Kaynakları listesi alınamıyor."</string>
<string name="cant_create_source">"Kaynak oluşturulamıyor."</string>
<string name="cant_get_spouts">"Spouts listesine girilemiyor."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"Form tamamlanamadı"</string>
<string name="pref_header_links">"Bağlantılar"</string>
<string name="issue_tracker_link">"Sorun İzleyici"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"登录以添加数据源。"</string>
<string name="cant_get_sources">"无法获取数据列表。"</string>
<string name="cant_create_source">"无法创建源数据。"</string>
<string name="cant_get_spouts">"无法获取数据列表"</string>
<string name="cant_get_spouts_no_network">"由于网络问题,无法获取 spouts 列表"</string>
<string name="cant_get_spouts">"无法获取 spouts 列表。可能有一个 api 问题。"</string>
<string name="form_not_complete">"窗体未完成"</string>
<string name="pref_header_links">"链接"</string>
<string name="issue_tracker_link">"问题追踪器"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">检查新来源和标签</string>
<string name="pref_switch_update_sources_summary">如果你的服务器接收过多的数据库查询,请禁用此功能。</string>
<string name="no_network_connectivity">未连接!</string>
<string name="network_connectivity_lost">"网络连接丢失"</string>
<string name="network_connectivity_retrieved">"网络连接现在可用"</string>
<string name="pref_switch_periodic_refresh">同步文章</string>
<string name="pref_switch_periodic_refresh_off">文章将不会在后台同步</string>
<string name="pref_switch_periodic_refresh_on">将定期同步文章</string>

View File

@ -40,7 +40,8 @@
<string name="addStringNoUrl">"登录以添加数据源。"</string>
<string name="cant_get_sources">"无法获取数据列表。"</string>
<string name="cant_create_source">"无法创建源数据。"</string>
<string name="cant_get_spouts">"无法获取数据列表"</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"窗体未完成"</string>
<string name="pref_header_links">"链接"</string>
<string name="issue_tracker_link">"问题追踪器"</string>
@ -142,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -39,7 +39,8 @@
<string name="addStringNoUrl">"Log in to add sources."</string>
<string name="cant_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</string>
<string name="cant_get_spouts">"Can't get spouts list."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"The form is not complete"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Issue Tracker"</string>
@ -143,6 +144,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -6,10 +6,13 @@ buildscript {
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
classpath("com.android.tools.build:gradle:7.2.1")
classpath("com.android.tools.build:gradle:7.2.2")
// sonarquve
classpath("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513")
// SqlDelight
classpath("com.squareup.sqldelight:gradle-plugin:1.5.3")
}
}

View File

@ -55,7 +55,7 @@ redirect_from: "/ReaderforSelfoss-multiplatform/"
<script async defer src="https://buttons.github.io/buttons.js"></script>
</head>
<body id="main">
@ -68,9 +68,9 @@ redirect_from: "/ReaderforSelfoss-multiplatform/"
<div id="links">
<a class="github-button" href="https://github.com/aminecmi/readerforselfoss-multiplatform" data-size="large" aria-label="Star aminecmi/readerforselfoss-multiplatform on GitHub">Star</a>
<a class="github-button" href="https://gitea.amine-louveau.fr/Louvorg/readerforselfoss-multiplatform" data-size="large" aria-label="Star aminecmi/readerforselfoss-multiplatform on GitHub">Star</a>
</div>
<meta itemprop="url" content="https://github.com/aminecmi/readerforselfoss-multiplatform">
<meta itemprop="url" content="https://gitea.amine-louveau.fr/Louvorg/readerforselfoss-multiplatform">
<meta itemprop="applicationCategory" content="News & Magazines">
</div>
</body>

View File

@ -18,3 +18,7 @@ kotlin.native.enableDependencyPropagation=false
android.useAndroidX=true
android.enableJetifier=true
kotlin.mpp.enableGranularSourceSetsMetadata=true
org.gradle.parallel=true
org.gradle.caching=true
ignoreGitVersion=false
pushCache=true

View File

@ -1,3 +1,5 @@
val pushCache: String by settings
pluginManagement {
repositories {
google()
@ -6,6 +8,16 @@ pluginManagement {
}
}
buildCache {
remote<HttpBuildCache> {
url = uri("http://18.0.0.7:3071/cache/")
isAllowInsecureProtocol = true
isAllowUntrustedServer = true
isUseExpectContinue = true
isPush = (pushCache == "true")
}
}
rootProject.name = "ReaderForSelfossV2"
include(":androidApp")
include(":shared")

View File

@ -1,6 +1,14 @@
object SqlDelight {
const val runtime = "com.squareup.sqldelight:runtime:1.5.3"
const val android = "com.squareup.sqldelight:android-driver:1.5.3"
const val native = "com.squareup.sqldelight:native-driver:1.5.3"
}
plugins {
kotlin("multiplatform")
id("com.android.library")
id("com.squareup.sqldelight")
kotlin("plugin.serialization") version "1.4.10"
}
@ -36,6 +44,12 @@ kotlin {
//Logging
implementation("io.github.aakira:napier:2.6.1")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// Sql
implementation(SqlDelight.runtime)
}
}
val commonTest by getting {
@ -47,6 +61,9 @@ kotlin {
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:2.0.1")
// Sql
implementation(SqlDelight.android)
}
}
val androidTest by getting {
@ -63,6 +80,11 @@ kotlin {
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
//iosSimulatorArm64Main.dependsOn(this)
// Sql
dependencies {
implementation(SqlDelight.native)
}
}
val iosX64Test by getting
val iosArm64Test by getting
@ -86,4 +108,16 @@ android {
minSdk = 21
targetSdk = 31
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
sqldelight {
database("ReaderForSelfossDB") {
packageName = "bou.amine.apps.readerforselfossv2.dao"
sourceFolders = listOf("sqldelight")
}
}

View File

@ -1,5 +0,0 @@
package bou.amine.apps.readerforselfossv2
actual class Platform actual constructor() {
actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

View File

@ -0,0 +1,10 @@
package bou.amine.apps.readerforselfossv2.dao
import android.content.Context
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.db.SqlDriver
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(ReaderForSelfossDB.Schema, context, "ReaderForSelfossV2-android.db")
}
}

View File

@ -0,0 +1,34 @@
package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
actual class DateUtils actual constructor(private val apiMajorVersion: Int) {
actual fun parseDate(dateString: String): Long {
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (apiMajorVersion >= 4) {
OffsetDateTime.parse(dateString).toInstant().toEpochMilli()
} else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(
ZoneOffset.UTC).toEpochMilli()
}
}
actual fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString)
return " " + DateUtils.getRelativeTimeSpanString(
date,
Instant.now().toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
)
}
}

View File

@ -0,0 +1,51 @@
package bou.amine.apps.readerforselfossv2.utils
import android.net.Uri
import android.text.Html
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import org.jsoup.Jsoup
import java.util.*
import kotlin.collections.ArrayList
actual fun String.getHtmlDecoded(): String {
return Html.fromHtml(this).toString()
}
actual fun SelfossModel.Item.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
return constructUrl(baseUrl, "thumbnails", thumbnail)
}
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
val allImages = ArrayList<String>()
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
actual fun constructUrl(baseUrl: String, path: String, file: String?): String {
return if (file == null || file == "null" || file.isEmpty()) {
""
} else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString()
}
}

View File

@ -1,12 +0,0 @@
package bou.amine.apps.readerforselfossv2
import org.junit.Assert.assertTrue
import org.junit.Test
class AndroidGreetingTest {
@Test
fun testExample() {
assertTrue("Check Android is mentioned", Greeting().greeting().contains("Android"))
}
}

View File

@ -1,7 +0,0 @@
package bou.amine.apps.readerforselfossv2
class Greeting {
fun greeting(): String {
return "Hello, ${Platform().platform}!"
}
}

View File

@ -1,5 +0,0 @@
package bou.amine.apps.readerforselfossv2
expect class Platform() {
val platform: String
}

View File

@ -1,9 +1,7 @@
package bou.amine.apps.readerforselfossv2.dao
interface DeviceDatabase<ItemEntity> {
suspend fun items(): List<ItemEntity>
suspend fun insertAllItems(vararg items: ItemEntity)
suspend fun deleteAllItems()
suspend fun delete(item: ItemEntity)
suspend fun updateItem(item: ItemEntity)
}
import com.squareup.sqldelight.db.SqlDriver
expect class DriverFactory {
fun createDriver(): SqlDriver
}

View File

@ -0,0 +1,3 @@
package bou.amine.apps.readerforselfossv2.model
class NetworkUnavailableException : Exception()

View File

@ -1,7 +1,7 @@
package bou.amine.apps.readerforselfossv2.rest
package bou.amine.apps.readerforselfossv2.model
import android.os.Parcelable
import android.text.Html
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import kotlinx.serialization.Serializable
class SelfossModel {
@ -11,11 +11,7 @@ class SelfossModel {
val tag: String,
val color: String,
val unread: Int
) {
fun getTitleDecoded(): String {
return Html.fromHtml(tag).toString()
}
}
)
@Serializable
class SuccessResponse(val success: Boolean) {
@ -73,5 +69,40 @@ class SelfossModel {
val link: String,
val sourcetitle: String,
val tags: List<String>
)
) {
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
fun sourceAndDateText(dateUtils: DateUtils): String =
this.sourcetitle.getHtmlDecoded() + dateUtils.parseRelativeDate(this.datetime)
fun toggleStar(): Item {
this.starred = !this.starred
return this
}
}
}

View File

@ -1,20 +1,26 @@
package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.dao.*
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.*
import com.github.ln_12.library.ConnectivityStatus
import com.russhwolf.settings.Settings
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService) {
class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService, private val connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) {
val settings = Settings()
var items = ArrayList<SelfossModel.Item>()
val isConnectionAvailable = connectivityStatus.isNetworkConnected
var connectionMonitored = false
var baseUrl = apiDetails.getBaseUrl()
lateinit var dateUtils: DateUtils
@ -26,149 +32,232 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
var searchFilter: String? = null
var itemsCaching = settings.getBoolean("items_caching", false)
var offlineOverride = false
var apiMajorVersion = 0
var badgeUnread = 0
set(value) {field = if (value < 0) { 0 } else { value } }
var badgeAll = 0
set(value) {field = if (value < 0) { 0 } else { value } }
var badgeStarred = 0
set(value) {field = if (value < 0) { 0 } else { value } }
private val _badgeUnread = MutableStateFlow(0)
val badgeUnread = _badgeUnread.asStateFlow()
private val _badgeAll = MutableStateFlow(0)
val badgeAll = _badgeAll.asStateFlow()
private val _badgeStarred = MutableStateFlow(0)
val badgeStarred = _badgeStarred.asStateFlow()
init {
// TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
CoroutineScope(Dispatchers.Main).launch {
updateApiVersion()
dateUtils = DateUtils(apiMajorVersion)
reloadBadges()
isConnectionAvailable.collect { connectionAvailable ->
if (connectionAvailable) {
updateApiVersion()
reloadBadges()
}
}
}
}
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Check connectivity, use the updatedSince parameter
val fetchedItems = api.getItems(displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset = 0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null)
// TODO: Use the updatedSince parameter
var fetchedItems: List<SelfossModel.Item>? = null
if (isNetworkAvailable()) {
fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset = 0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
} else {
if (itemsCaching) {
fetchedItems = getDBItems().filter {
displayedItems == ItemType.ALL ||
(it.unread && displayedItems == ItemType.UNREAD) ||
(it.starred && displayedItems == ItemType.STARRED)
}.map { it.toView() }
}
}
if (fetchedItems != null) {
items = ArrayList(fetchedItems)
sortItems()
}
return items
}
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
// TODO: Check connectivity
val offset = items.size
val fetchedItems = api.getItems(displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null)
var fetchedItems: List<SelfossModel.Item>? = null
if (isNetworkAvailable()) {
val offset = items.size
fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
} // When using the db cache, we load everything the first time, so there should be nothing more to load.
if (fetchedItems != null) {
appendItems(fetchedItems)
items.addAll(fetchedItems)
sortItems()
}
return items
}
suspend fun allItems(itemType: ItemType): List<SelfossModel.Item>? =
api.getItems(itemType.type, 200, 0, tagFilter?.tag, sourceFilter?.id?.toLong(), searchFilter, null)
private fun appendItems(fetchedItems: List<SelfossModel.Item>) {
// TODO: Store in DB if enabled by user
val fetchedIDS = fetchedItems.map { it.id }
val tmpItems = ArrayList(items.filterNot { it.id in fetchedIDS })
tmpItems.addAll(fetchedItems)
sortItems(tmpItems)
items = tmpItems
suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item>? {
return if (isNetworkAvailable()) {
api.getItems(
itemType.type,
200,
0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
} else {
emptyList()
}
}
private fun sortItems(items: ArrayList<SelfossModel.Item>) {
private fun sortItems() {
items.sortByDescending { dateUtils.parseDate(it.datetime) }
}
suspend fun reloadBadges(): Boolean {
// TODO: Check connectivity, calculate from DB
var success = false
val response = api.stats()
if (response != null) {
badgeUnread = response.unread
badgeAll = response.total
badgeStarred = response.starred
success = true
if (isNetworkAvailable()) {
val response = api.stats()
if (response != null) {
_badgeUnread.value = response.unread
_badgeAll.value = response.total
_badgeStarred.value = response.starred
success = true
}
} else if (itemsCaching) {
// TODO: do this differently, because it's not efficient
val dbItems = getDBItems()
_badgeUnread.value = dbItems.filter { item -> item.unread }.size
_badgeStarred.value = dbItems.filter { item -> item.starred }.size
_badgeAll.value = dbItems.size
}
return success
}
suspend fun getTags(): List<SelfossModel.Tag>? {
// TODO: Check success, store in DB
return api.tags()
val tags = if (isNetworkAvailable()) {
api.tags()
} else {
getDBTags().map { it.toView() }
}
if (tags != null) {
resetDBTagsWithData(tags)
}
return tags
}
suspend fun getSpouts(): Map<String, SelfossModel.Spout>? {
// TODO: Check success, store in DB
return api.spouts()
return if (isNetworkAvailable()) {
api.spouts()
} else {
throw NetworkUnavailableException()
}
}
suspend fun getSources(): ArrayList<SelfossModel.Source>? {
// TODO: Check success
return api.sources()
val sources = if (isNetworkAvailable()) {
api.sources()
} else {
ArrayList(getDBSources().map { it.toView() })
}
if (sources != null) {
resetDBSourcesWithData(sources)
}
return sources
}
suspend fun markAsRead(id: Int): Boolean {
// TODO: Check internet connection
val success = api.markAsRead(id.toString())?.isSuccess == true
suspend fun markAsRead(item: SelfossModel.Item): Boolean {
val success = markAsReadById(item.id)
if (success) {
markAsReadLocally(items.first {it.id == id})
markAsReadLocally(item)
}
return success
}
suspend fun unmarkAsRead(id: Int): Boolean {
// TODO: Check internet connection
val success = api.unmarkAsRead(id.toString())?.isSuccess == true
suspend fun markAsReadById(id: Int): Boolean {
return if (isNetworkAvailable()) {
api.markAsRead(id.toString())?.isSuccess == true
} else {
insertDBAction(id.toString(), read = true)
true
}
}
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id)
if (success) {
unmarkAsReadLocally(items.first {it.id == id})
unmarkAsReadLocally(item)
}
return success
}
suspend fun starr(id: Int): Boolean {
// TODO: Check success, store in DB
val success = api.starr(id.toString())?.isSuccess == true
suspend fun unmarkAsReadById(id: Int): Boolean {
return if (isNetworkAvailable()) {
api.unmarkAsRead(id.toString())?.isSuccess == true
} else {
insertDBAction(id.toString(), unread = true)
true
}
}
suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id)
if (success) {
starrLocally(items.first {it.id == id})
starrLocally(item)
}
return success
}
suspend fun unstarr(id: Int): Boolean {
// TODO: Check internet connection
val success = api.unstarr(id.toString())?.isSuccess == true
suspend fun starrById(id: Int): Boolean {
return if (isNetworkAvailable()) {
api.starr(id.toString())?.isSuccess == true
} else {
insertDBAction(id.toString(), starred = true)
true
}
}
suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id)
if (success) {
unstarrLocally(items.first {it.id == id})
unstarrLocally(item)
}
return success
}
suspend fun markAllAsRead(ids: List<Int>): Boolean {
// TODO: Check Internet connectivity, store in DB
suspend fun unstarrById(id: Int): Boolean {
return if (isNetworkAvailable()) {
api.unstarr(id.toString())?.isSuccess == true
} else {
insertDBAction(id.toString(), starred = true)
true
}
}
val success = api.markAllAsRead(ids.map { it.toString() })?.isSuccess == true
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false
if (success) {
val itemsToMark = items.filter { it.id in ids }
for (item in itemsToMark) {
if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() })?.isSuccess == true) {
success = true
for (item in items) {
markAsReadLocally(item)
}
}
@ -176,34 +265,46 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
}
private fun markAsReadLocally(item: SelfossModel.Item) {
// TODO: Mark also in the database
if (item.unread) {
item.unread = false
badgeUnread -= 1
_badgeUnread.value -= 1
}
CoroutineScope(Dispatchers.Main).launch {
updateDBItem(item)
}
}
private fun unmarkAsReadLocally(item: SelfossModel.Item) {
// TODO: Mark also in the database
if (!item.unread) {
item.unread = true
badgeUnread += 1
_badgeUnread.value += 1
}
CoroutineScope(Dispatchers.Main).launch {
updateDBItem(item)
}
}
private fun starrLocally(item: SelfossModel.Item) {
// TODO: Mark also in the database
if (!item.starred) {
item.starred = true
badgeStarred += 1
_badgeStarred.value += 1
}
CoroutineScope(Dispatchers.Main).launch {
updateDBItem(item)
}
}
private fun unstarrLocally(item: SelfossModel.Item) {
// TODO: Mark also in the database
if (item.starred) {
item.starred = false
badgeStarred -= 1
_badgeStarred.value -= 1
}
CoroutineScope(Dispatchers.Main).launch {
updateDBItem(item)
}
}
@ -214,45 +315,53 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
tags: String,
filter: String
): Boolean {
// TODO: Check connectivity
val response = api.createSourceForVersion(
title,
url,
spout,
tags,
filter,
apiMajorVersion
)
var response = false
if (isNetworkAvailable()) {
response = api.createSourceForVersion(
title,
url,
spout,
tags,
filter,
apiMajorVersion
)?.isSuccess == true
}
return response != null
return response
}
suspend fun deleteSource(id: Int): Boolean {
// TODO: Check connectivity, store in DB
var success = false
val response = api.deleteSource(id)
if (response != null) {
success = response.isSuccess
if (isNetworkAvailable()) {
val response = api.deleteSource(id)
if (response != null) {
success = response.isSuccess
}
}
return success
}
suspend fun updateRemote(): Boolean {
// TODO: Handle connectivity issues
val response = api.update()
return response?.isSuccess ?: false
return if (isNetworkAvailable()) {
api.update()?.equals("finished") ?: false
} else {
false
}
}
suspend fun login(): Boolean {
var result = false
try {
val response = api.login()
if (response != null && response.isSuccess) {
result = true
if (isNetworkAvailable()) {
try {
val response = api.login()
result = response?.isSuccess == true
if (result) {
updateApiVersion()
}
} catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote")
}
} catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(),tag = "RepositoryImpl.updateRemote")
}
return result
}
@ -271,15 +380,110 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
}
private suspend fun updateApiVersion() {
// TODO: Handle connectivity issues
val fetchedVersion = api.version()
if (fetchedVersion != null) {
apiMajorVersion = fetchedVersion.getApiMajorVersion()
settings.putInt("apiVersionMajor", apiMajorVersion)
} else {
apiMajorVersion = settings.getInt("apiVersionMajor", 0)
apiMajorVersion = settings.getInt("apiVersionMajor", 0)
if (isNetworkAvailable()) {
val fetchedVersion = api.version()
if (fetchedVersion != null) {
apiMajorVersion = fetchedVersion.getApiMajorVersion()
settings.putInt("apiVersionMajor", apiMajorVersion)
}
}
dateUtils = DateUtils(apiMajorVersion)
}
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride
fun getDBActions(): List<ACTION> =
db.actionsQueries.actions().executeAsList()
fun deleteDBAction(action: ACTION) =
db.actionsQueries.deleteAction(action.id)
fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList()
fun getDBSources(): List<SOURCE> = db.sourcesQueries.sources().executeAsList()
fun resetDBTagsWithData(tagEntities: List<SelfossModel.Tag>) {
db.tagsQueries.deleteAllTags()
db.tagsQueries.transaction {
tagEntities.forEach { tag ->
db.tagsQueries.insertTag(tag.toEntity())
}
}
}
// TODO: Handle offline actions
fun resetDBSourcesWithData(sources: List<SelfossModel.Source>) {
db.sourcesQueries.deleteAllSources()
db.sourcesQueries.transaction {
sources.forEach { source ->
db.sourcesQueries.insertSource(source.toEntity())
}
}
}
private fun insertDBItems(items: List<SelfossModel.Item>) {
db.itemsQueries.transaction {
items.forEach { item ->
db.itemsQueries.insertItem(item.toEntity())
}
}
}
private fun getDBItems(): List<ITEM> = db.itemsQueries.items().executeAsList()
private fun insertDBAction(articleid: String, read: Boolean = false, unread: Boolean = false, starred: Boolean = false, unstarred: Boolean = false) =
db.actionsQueries.insertAction(articleid, read, unread, starred, unstarred)
private fun updateDBItem(item: SelfossModel.Item) =
db.itemsQueries.updateItem(item.datetime, item.title.getHtmlDecoded(), item.content, item.unread, item.starred, item.thumbnail, item.icon, item.link, item.sourcetitle, item.tags.joinToString(","), item.id.toString())
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item>? {
if (itemsCaching) {
try {
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
val allItems = getMaxItemsForBackground(ItemType.ALL)
val starredItems = getMaxItemsForBackground(ItemType.STARRED)
insertDBItems(newItems.orEmpty() + allItems.orEmpty() + starredItems.orEmpty())
return newItems
} catch (e: Throwable) {
}
}
return emptyList()
}
suspend fun handleDBActions() {
val actions: List<ACTION> = getDBActions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(
markAsReadById(action.articleid.toInt()),
action
)
action.unread -> doAndReportOnFail(
unmarkAsReadById(action.articleid.toInt()),
action
)
action.starred -> doAndReportOnFail(
starrById(action.articleid.toInt()),
action
)
action.unstarred -> doAndReportOnFail(
unstarrById(action.articleid.toInt()),
action
)
}
}
}
private fun doAndReportOnFail(result: Boolean, action: ACTION) {
if (result) {
deleteDBAction(action)
}
}
}

View File

@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import io.ktor.client.*
import io.ktor.client.call.*
@ -99,7 +100,7 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
parameter("password", apiDetailsService.getPassword())
}.body()
suspend fun update(): SelfossModel.SuccessResponse? =
suspend fun update(): String? =
client.get(url("/update")) {
parameter("username", apiDetailsService.getUserName())
parameter("password", apiDetailsService.getPassword())

View File

@ -1,34 +0,0 @@
package bou.amine.apps.readerforselfossv2.service
import bou.amine.apps.readerforselfossv2.dao.DeviceDatabase
import bou.amine.apps.readerforselfossv2.utils.parseDate
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
abstract class DeviceDataBaseService<ItemEntity>(val db: DeviceDatabase<ItemEntity>, private val searchService: SearchService) {
var itemsCaching = false
var items: ArrayList<SelfossModel.Item> = arrayListOf()
get() {
return ArrayList(field)
}
set(value) {
field = ArrayList(value)
}
abstract suspend fun updateDatabase()
abstract suspend fun clearDBItems()
abstract fun appendNewItems(items: List<SelfossModel.Item>)
abstract fun getFromDB()
fun sortItems() {
val tmpItems = ArrayList(items.sortedByDescending { it.parseDate(searchService.dateUtils) })
items = tmpItems
}
// This filtered items from items val. Do not use
fun getFocusedItems() {}
fun computeBadges() {
searchService.badgeUnread = items.filter { item -> item.unread }.size
searchService.badgeStarred = items.filter { item -> item.starred }.size
searchService.badgeAll = items.size
}
}

View File

@ -1,42 +1,13 @@
package bou.amine.apps.readerforselfossv2.utils
//import android.text.format.DateUtils
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import bou.amine.apps.readerforselfossv2.model.SelfossModel
fun SelfossModel.Item.parseDate(dateUtils: bou.amine.apps.readerforselfossv2.utils.DateUtils): Instant =
fun SelfossModel.Item.parseDate(dateUtils: DateUtils): Long =
dateUtils.parseDate(this.datetime)
fun SelfossModel.Item.parseRelativeDate(dateUtils: bou.amine.apps.readerforselfossv2.utils.DateUtils): String =
dateUtils.parseRelativeDate(this.datetime)
expect class DateUtils(apiMajorVersion: Int) {
fun parseDate(dateString: String): Long
class DateUtils(private val apiMajorVersion: Int) {
fun parseDate(dateString: String): Instant {
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (apiMajorVersion >= 4) {
OffsetDateTime.parse(dateString).toInstant()
} else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC)
}
}
fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString)
// TODO:
// return " " + DateUtils.getRelativeTimeSpanString(
// date.toEpochMilli(),
// Instant.now().toEpochMilli(),
// 60000L, // DateUtils.MINUTE_IN_MILLIS,
// 262144 // DateUtils.FORMAT_ABBREV_RELATIVE
// )
return dateString
}
}
fun parseRelativeDate(dateString: String): String
}

View File

@ -0,0 +1,70 @@
package bou.amine.apps.readerforselfossv2.utils
import bou.amine.apps.readerforselfossv2.dao.ITEM
import bou.amine.apps.readerforselfossv2.dao.SOURCE
import bou.amine.apps.readerforselfossv2.dao.TAG
import bou.amine.apps.readerforselfossv2.model.SelfossModel
fun TAG.toView(): SelfossModel.Tag =
SelfossModel.Tag(
this.name,
this.color,
this.unread.toInt()
)
fun SOURCE.toView(): SelfossModel.Source =
SelfossModel.Source(
this.id.toInt(),
this.title,
this.tags.split(","),
this.spout,
this.error,
this.icon
)
fun SelfossModel.Source.toEntity(): SOURCE =
SOURCE(
this.id.toString(),
this.title.getHtmlDecoded(),
this.tags.joinToString(","),
this.spout,
this.error,
this.icon.orEmpty()
)
fun SelfossModel.Tag.toEntity(): TAG =
TAG(
this.tag,
this.color,
this.unread.toLong()
)
fun ITEM.toView(): SelfossModel.Item =
SelfossModel.Item(
this.id.toInt(),
this.datetime,
this.title,
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.sourcetitle,
this.tags.split(",")
)
fun SelfossModel.Item.toEntity(): ITEM =
ITEM(
this.id.toString(),
this.datetime,
this.title.getHtmlDecoded(),
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.title.getHtmlDecoded(),
this.tags.joinToString(",")
)

View File

@ -0,0 +1,15 @@
package bou.amine.apps.readerforselfossv2.utils
import bou.amine.apps.readerforselfossv2.model.SelfossModel
expect fun String.getHtmlDecoded(): String
expect fun SelfossModel.Item.getIcon(baseUrl: String): String
expect fun SelfossModel.Item.getThumbnail(baseUrl: String): String
expect fun SelfossModel.Item.getImages(): ArrayList<String>
expect fun SelfossModel.Source.getIcon(baseUrl: String): String
expect fun constructUrl(baseUrl: String, path: String, file: String?): String

View File

@ -0,0 +1,18 @@
CREATE TABLE `ACTION` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`articleid` TEXT NOT NULL,
`read` INTEGER AS Boolean DEFAULT 0 NOT NULL,
`unread` INTEGER AS Boolean DEFAULT 0 NOT NULL,
`starred` INTEGER AS Boolean DEFAULT 0 NOT NULL,
`unstarred` INTEGER AS Boolean DEFAULT 0 NOT NULL
);
actions:
SELECT *
FROM `ACTION`;
insertAction:
INSERT OR REPLACE INTO `ACTION` (`articleid`, `read`, `unread`, `starred`, `unstarred`) VALUES (?, ?, ?, ?, ?);
deleteAction:
DELETE FROM `ACTION` WHERE id = ?;

View File

@ -0,0 +1,30 @@
CREATE TABLE ITEM (
`id` TEXT NOT NULL,
`datetime` TEXT NOT NULL,
`title` TEXT NOT NULL,
`content` TEXT NOT NULL,
`unread` INTEGER AS Boolean DEFAULT 0 NOT NULL,
`starred` INTEGER AS Boolean DEFAULT 0 NOT NULL,
`thumbnail` TEXT,
`icon` TEXT,
`link` TEXT NOT NULL,
`sourcetitle` TEXT NOT NULL,
`tags` TEXT NOT NULL,
PRIMARY KEY(`id`)
);
CREATE INDEX item_title ON ITEM(`title`);
CREATE INDEX item_source ON ITEM(`sourcetitle`);
items:
SELECT * FROM ITEM ORDER BY `id` DESC;
insertItem:
INSERT OR REPLACE INTO ITEM VALUES ?;
deleteItem:
DELETE FROM ITEM WHERE `id` = ?;
updateItem:
UPDATE ITEM SET `datetime` = ?, `title` = ?, `content` = ?, `unread` = ?, `starred` = ?, `thumbnail` = ?, `icon` = ?, `link` = ?, `sourcetitle` = ?, `tags` = ? WHERE `id` = ?;

View File

@ -0,0 +1,21 @@
CREATE TABLE SOURCE (
`id` TEXT NOT NULL,
`title` TEXT NOT NULL,
`tags` TEXT NOT NULL,
`spout` TEXT NOT NULL,
`error` TEXT NOT NULL,
`icon` TEXT NOT NULL,
PRIMARY KEY(`id`)
);
sources:
SELECT * FROM SOURCE;
insertSource:
INSERT OR REPLACE INTO SOURCE VALUES ?;
deleteAllSources:
DELETE FROM SOURCE;
deleteSource:
DELETE FROM SOURCE WHERE `id` = ?;

View File

@ -0,0 +1,20 @@
CREATE TABLE TAG (
`name` TEXT NOT NULL,
`color` TEXT NOT NULL,
`unread` INTEGER NOT NULL,
PRIMARY KEY(`name`)
);
CREATE INDEX tag_name ON TAG(`name`);
tags:
SELECT * FROM TAG;
insertTag:
INSERT OR REPLACE INTO TAG VALUES ?;
deleteAllTags:
DELETE FROM TAG;
deleteTag:
DELETE FROM TAG WHERE `name` = ? AND `color` = ?;

View File

@ -1,12 +0,0 @@
package bou.amine.apps.readerforselfossv2
import kotlin.test.Test
import kotlin.test.assertTrue
class CommonGreetingTest {
@Test
fun testExample() {
assertTrue(Greeting().greeting().contains("Hello"), "Check 'Hello' is mentioned")
}
}

View File

@ -0,0 +1,10 @@
package bou.amine.apps.readerforselfossv2.dao
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
}
}

View File

@ -0,0 +1,12 @@
package bou.amine.apps.readerforselfossv2.utils
actual class DateUtils actual constructor(apiMajorVersion: Int) {
actual fun parseDate(dateString: String): Long {
TODO("Not yet implemented")
}
actual fun parseRelativeDate(dateString: String): String {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,27 @@
package bou.amine.apps.readerforselfossv2.utils
import bou.amine.apps.readerforselfossv2.model.SelfossModel
actual fun String.getHtmlDecoded(): String {
TODO("Not yet implemented")
}
actual fun SelfossModel.Item.getIcon(baseUrl: String): String {
TODO("Not yet implemented")
}
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
TODO("Not yet implemented")
}
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
TODO("Not yet implemented")
}
actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
TODO("Not yet implemented")
}
actual fun constructUrl(baseUrl: String, path: String, file: String?): String {
TODO("Not yet implemented")
}

View File

@ -1,7 +0,0 @@
package bou.amine.apps.readerforselfossv2
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

View File

@ -0,0 +1,10 @@
package bou.amine.apps.readerforselfossv2.dao
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
}
}

View File

@ -0,0 +1,12 @@
package bou.amine.apps.readerforselfossv2.utils
actual class DateUtils actual constructor(apiMajorVersion: Int) {
actual fun parseDate(dateString: String): Long {
TODO("Not yet implemented")
}
actual fun parseRelativeDate(dateString: String): String {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,27 @@
package bou.amine.apps.readerforselfossv2.utils
import bou.amine.apps.readerforselfossv2.model.SelfossModel
actual fun String.getHtmlDecoded(): String {
TODO("Not yet implemented")
}
actual fun SelfossModel.Item.getIcon(baseUrl: String): String {
TODO("Not yet implemented")
}
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
TODO("Not yet implemented")
}
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
TODO("Not yet implemented")
}
actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
TODO("Not yet implemented")
}
actual fun constructUrl(baseUrl: String, path: String, file: String?): String {
TODO("Not yet implemented")
}