Compare commits

..

1 Commits

Author SHA1 Message Date
5321becd24 Updated to androidx. 2018-09-24 22:12:44 +02:00
266 changed files with 6645 additions and 5757 deletions

View File

@ -41,12 +41,6 @@ Always check if the web version of your instance is working.
* Remember that PR review can take time. * Remember that PR review can take time.
# Install Selfoss (if you don't have an instance)
I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it.
All the details to need are [here](https://selfoss.aditu.de/).
# Build the project # Build the project
You can directly import this project into IntellIJ/Android Studio. You can directly import this project into IntellIJ/Android Studio.

4
.gitignore vendored
View File

@ -216,4 +216,6 @@ gradle-app.setting
release/ release/
crowdin.properties crowdin.properties
publish-version.sh

View File

@ -1,31 +1,3 @@
**1.7.x**
- Hiding tags with 0 articles
- Fixed issue with basic auth and images loading
- Added the ability to justify or left align the reader text
- Fixed #251
- Added experimental issue to set a default timeout. Should work for #238.
- Closing #220.
- Start of #238. "Add a quick shortcut to open the app on offline mode ?"
- Closes #216. Issue with selfoss version 2.19.
- Closes #179. Sync of read/unread/star/unstar items on background task or on app reload with network available.
- Closes #33. Background sync with settings.
- Closing #1. Initial article caching.
- Closing #228 by removing the list action bar. Action buttons are exclusively on the card view from now on.
- Closing #38. Only doing api calls on network available.
**1.6.x** **1.6.x**
- Handling hidden tags. - Handling hidden tags.

View File

@ -18,11 +18,7 @@ Also, the last APK built from source is available [here](https://jenkins.amine-b
## Want to help ? ## Want to help ?
1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/). Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md)
2. Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md).
3. Build the project by following [these steps](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
## Useful links ## Useful links

View File

@ -1,17 +1,17 @@
buildscript { buildscript {
} }
ext {
configuration = [
buildDate: new Date()
]
// This will make me able to build multiple times a day. May break thinks. I may forget it.
todaysBuilds = "1"
}
def gitVersion() { def gitVersion() {
def process def process = "git describe --abbrev=0 --tags".execute()
def maybeTagOfCurrentCommit = 'git describe --contains HEAD'.execute() return process.text.substring(1).replaceAll("\\.", "").trim()
if (maybeTagOfCurrentCommit.text.isEmpty()) {
println "No tag on current commit. Will take the latest one."
process = "git for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1".execute()
} else {
println "Tag found on current commit"
process = 'git describe --contains HEAD'.execute()
}
return process.text.replaceAll("'", "").substring(1).replaceAll("\\.", "").trim()
} }
def versionCodeFromGit() { def versionCodeFromGit() {
@ -24,8 +24,6 @@ def versionNameFromGit() {
return gitVersion() return gitVersion()
} }
apply plugin: 'kotlin-kapt'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
@ -38,7 +36,7 @@ android {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
compileSdkVersion 28 compileSdkVersion 28
buildToolsVersion '28.0.3' buildToolsVersion '28.0.2'
defaultConfig { defaultConfig {
applicationId "apps.amine.bou.readerforselfoss" applicationId "apps.amine.bou.readerforselfoss"
minSdkVersion 16 minSdkVersion 16
@ -56,18 +54,11 @@ android {
// tests // tests
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
} }
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
shrinkResources false shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro' 'proguard-rules.pro'
} }
@ -75,6 +66,7 @@ android {
buildConfigField "String", "LOGIN_URL", appLoginUrl buildConfigField "String", "LOGIN_URL", appLoginUrl
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
applicationIdSuffix ".dev"
} }
} }
flavorDimensions "build" flavorDimensions "build"
@ -93,29 +85,32 @@ android {
dependencies { dependencies {
// Testing // Testing
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-beta02' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'
androidTestImplementation 'androidx.test:runner:1.1.0-beta02' androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource // Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0-beta02' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0-alpha4'
// Espresso-intents for validation and stubbing of Intents // Espresso-intents for validation and stubbing of Intents
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0-beta02' androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0-alpha4'
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Android Support // Android Support
implementation "androidx.appcompat:appcompat:$android_version" implementation 'androidx.appcompat:appcompat:1.0.0'
implementation "com.google.android.material:material:$android_version" implementation 'com.google.android.material:material:1.0.0'
implementation "androidx.recyclerview:recyclerview:$android_version" implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation "androidx.legacy:legacy-support-v4:$android_version" implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.vectordrawable:vectordrawable:$android_version" implementation 'androidx.vectordrawable:vectordrawable:1.0.0'
implementation "androidx.browser:browser:$android_version" implementation 'androidx.browser:browser:1.0.0'
implementation "androidx.cardview:cardview:$android_version" implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
//multidex //multidex
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.0'
// Intro
implementation 'agency.tango.android:material-intro-screen:0.0.5'
// About // About
implementation('com.mikepenz:aboutlibraries:6.2.0@aar') { implementation('com.mikepenz:aboutlibraries:6.0.0@aar') {
transitive = true transitive = true
} }
@ -126,8 +121,8 @@ dependencies {
implementation 'com.burgstaller:okhttp-digest:1.12' implementation 'com.burgstaller:okhttp-digest:1.12'
// Material-ish things // Material-ish things
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.5' implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.3'
implementation 'com.github.jd-alexander:LikeButton:0.2.3' implementation 'com.github.jd-alexander:LikeButton:0.2.1'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
// glide // glide
@ -135,32 +130,25 @@ dependencies {
implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1' implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
// Asking politely users to rate the app // Asking politely users to rate the app
implementation 'com.github.stkent:amplify:2.2.0' implementation 'com.github.stkent:amplify:2.1.0'
// Drawer // Drawer
implementation 'co.zsmb:materialdrawer-kt:2.0.1' implementation 'co.zsmb:materialdrawer-kt:2.0.0'
implementation 'com.anupcowkur:reservoir:3.1.0'
// Themes // Themes
implementation 'com.52inc:scoops:1.0.0' implementation 'com.52inc:scoops:1.0.0'
implementation 'com.jaredrummler:colorpicker:1.0.2' implementation 'com.jrummyapps:colorpicker:2.1.7'
implementation 'com.github.rubensousa:floatingtoolbar:1.5.1' implementation 'com.github.rubensousa:floatingtoolbar:1.5.1'
// Pager // Pager
implementation 'me.relex:circleindicator:2.0.0@aar' implementation 'me.relex:circleindicator:1.2.2@aar'
implementation 'androidx.core:core-ktx:1.0.0' implementation 'androidx.core:core-ktx:1.0.0'
// Crash // Crash
implementation 'ch.acra:acra-http:5.2.1' implementation 'ch.acra:acra-http:5.2.0'
implementation 'ch.acra:acra-dialog:5.2.1' implementation 'ch.acra:acra-dialog:5.2.0'
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "android.arch.work:work-runtime-ktx:$work_version"
} }
@ -178,4 +166,4 @@ def initAppLoginPropertiesIfNeeded() {
entry(key: "appLoginPassword", value: System.getProperty("appLoginPassword")) entry(key: "appLoginPassword", value: System.getProperty("appLoginPassword"))
} }
} }
} }

View File

@ -30,6 +30,22 @@
<fields>; <fields>;
} }
##Retrofit
#-keep class com.google.gson.** { *; }
#-keep class com.google.inject.** { *; }
#-keep class org.apache.http.** { *; }
#-keep class org.apache.james.mime4j.** { *; }
#-keep class javax.inject.** { *; }
#-keep class retrofit.** { *; }
#-keepclassmembernames interface * {
# @retrofit.http.* <methods>;
#}
#-keep class retrofit.** { *; }
#-keep class apps.amine.bou.readerforselfoss.api.selfoss.model.** { *; }
#-keepclassmembernames interface * {
# @retrofit.http.* <methods>;
#}
-dontwarn okio.** -dontwarn okio.**
-dontwarn retrofit2.Platform$Java8 -dontwarn retrofit2.Platform$Java8
-keep class retrofit.** { *; } -keep class retrofit.** { *; }
@ -59,7 +75,4 @@
-dontwarn javax.annotation.** -dontwarn javax.annotation.**
-keep class android.support.v7.widget.SearchView { *; } -keep class androidx.appcompat.widget.SearchView { *; }
# maybe remove later ?
-keep class * extends androidx.fragment.app.Fragment

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

@ -7,17 +7,14 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.action.ViewActions.pressKey import androidx.test.espresso.action.ViewActions.pressKey
import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText

View File

@ -0,0 +1,91 @@
package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent
import androidx.test.InstrumentationRegistry.getInstrumentation
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import apps.amine.bou.readerforselfoss.utils.Config
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class IntroActivityEspressoTest {
@Rule @JvmField
val rule = ActivityTestRule(IntroActivity::class.java, true, false)
@Before
fun clearData() {
val editor =
getInstrumentation().targetContext
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
.edit()
editor.clear()
editor.commit()
Intents.init()
}
@Test
fun nextEachTimes() {
rule.launchActivity(Intent())
onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
onView(withText(R.string.intro_needs_selfoss_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
intended(hasComponent(IntroActivity::class.java.name), times(1))
intended(hasComponent(LoginActivity::class.java.name), times(1))
}
@Test
fun nextBackRandomTimes() {
val max = 5
val min = 1
val random = (Random().nextInt(max + 1 - min)) + min
rule.launchActivity(Intent())
onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
repeat(random) { _ ->
onView(withText(R.string.intro_needs_selfoss_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_back)).perform(click())
}
onView(withId(R.id.button_next)).perform(click())
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
intended(hasComponent(IntroActivity::class.java.name), times(1))
intended(hasComponent(LoginActivity::class.java.name), times(1))
}
@After
fun releaseIntents() {
Intents.release()
}
}

View File

@ -45,6 +45,7 @@ class MainActivityEspressoTest {
rule.launchActivity(intent) rule.launchActivity(intent)
intended(hasComponent(MainActivity::class.java.name)) intended(hasComponent(MainActivity::class.java.name))
intended(hasComponent(IntroActivity::class.java.name))
intended(hasComponent(LoginActivity::class.java.name), times(0)) intended(hasComponent(LoginActivity::class.java.name), times(0))
} }
@ -57,6 +58,7 @@ class MainActivityEspressoTest {
intended(hasComponent(MainActivity::class.java.name)) intended(hasComponent(MainActivity::class.java.name))
intended(hasComponent(LoginActivity::class.java.name)) intended(hasComponent(LoginActivity::class.java.name))
intended(hasComponent(IntroActivity::class.java.name), times(0))
} }
@After @After

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="apps.amine.bou.readerforselfoss"> package="apps.amine.bou.readerforselfoss"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
@ -11,19 +11,19 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/NoBar"> android:theme="@style/NoBar">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:theme="@style/SplashTheme"> android:theme="@style/SplashTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity>
<meta-data <activity
android:name="android.app.shortcuts" android:name=".IntroActivity"
android:resource="@xml/shortcuts" /> android:theme="@style/Theme.Intro">
</activity> </activity>
<activity <activity
android:name=".LoginActivity" android:name=".LoginActivity"
@ -37,7 +37,7 @@
android:parentActivityName=".HomeActivity"> android:parentActivityName=".HomeActivity">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".HomeActivity" /> android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
</activity> </activity>
<activity <activity
android:name=".SourcesActivity" android:name=".SourcesActivity"
@ -55,7 +55,9 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
</activity> </activity>
@ -74,9 +76,6 @@
android:value="true" /> android:value="true" />
<meta-data android:name="android.max_aspect" android:value="2.1" /> <meta-data android:name="android.max_aspect" android:value="2.1" />
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</application> </application>
</manifest> </manifest>

View File

@ -1,6 +1,5 @@
package apps.amine.bou.readerforselfoss package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -23,13 +22,12 @@ import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_add_source.*
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import android.graphics.PorterDuff import android.graphics.PorterDuff
import androidx.appcompat.widget.Toolbar
import kotlinx.android.synthetic.main.activity_add_source.*
class AddSourceActivity : AppCompatActivity() { class AddSourceActivity : AppCompatActivity() {
@ -86,13 +84,10 @@ class AddSourceActivity : AppCompatActivity() {
try { try {
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
api = SelfossApi( api = SelfossApi(
this, this,
this@AddSourceActivity, this@AddSourceActivity,
settings.getBoolean("isSelfSignedCert", false), prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false) prefs.getBoolean("should_log_everything", false)
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@ -112,7 +107,7 @@ class AddSourceActivity : AppCompatActivity() {
super.onResume() super.onResume()
val config = Config(this) val config = Config(this)
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(false, this@AddSourceActivity)) { if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid()) {
mustLoginToAddSource() mustLoginToAddSource()
} else { } else {
handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer) handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer)

View File

@ -0,0 +1,70 @@
package apps.amine.bou.readerforselfoss
import agency.tango.materialintroscreen.MaterialIntroActivity
import agency.tango.materialintroscreen.MessageButtonBehaviour
import agency.tango.materialintroscreen.SlideFragmentBuilder
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
addSlide(
SlideFragmentBuilder()
.backgroundColor(R.color.colorPrimary)
.buttonsColor(R.color.colorAccent)
.image(R.drawable.web_hi_res_512)
.title(getString(R.string.intro_hello_title))
.description(getString(R.string.intro_hello_message))
.build()
)
addSlide(
SlideFragmentBuilder()
.backgroundColor(R.color.colorAccent)
.buttonsColor(R.color.colorPrimary)
.image(R.drawable.ic_info_outline_white_48px)
.title(getString(R.string.intro_needs_selfoss_title))
.description(getString(R.string.intro_needs_selfoss_message))
.build(),
MessageButtonBehaviour(
View.OnClickListener {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://selfoss.aditu.de")
)
startActivity(browserIntent)
}, getString(R.string.intro_needs_selfoss_link)
)
)
addSlide(
SlideFragmentBuilder()
.backgroundColor(R.color.colorPrimaryDark)
.buttonsColor(R.color.colorAccentDark)
.image(R.drawable.ic_thumb_up_white_48px)
.title(getString(R.string.intro_all_set_title))
.description(getString(R.string.intro_all_set_message))
.build()
)
}
override fun onFinish() {
super.onFinish()
val getPrefs = PreferenceManager.getDefaultSharedPreferences(baseContext)
val e = getPrefs.edit()
e.putBoolean("firstStart", false)
e.apply()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
}

View File

@ -21,7 +21,6 @@ import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.android.synthetic.main.activity_login.* import kotlinx.android.synthetic.main.activity_login.*
@ -54,6 +53,7 @@ class LoginActivity : AppCompatActivity() {
handleBaseUrlFail() handleBaseUrlFail()
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
userIdentifier = settings.getString("unique_id", "") userIdentifier = settings.getString("unique_id", "")
logErrors = settings.getBoolean("login_debug", false) logErrors = settings.getBoolean("login_debug", false)
@ -112,9 +112,8 @@ class LoginActivity : AppCompatActivity() {
alertDialog.setMessage(getString(R.string.base_url_error)) alertDialog.setMessage(getString(R.string.base_url_error))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK", "OK"
{ dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
)
alertDialog.show() alertDialog.show()
} }
} }
@ -144,7 +143,7 @@ class LoginActivity : AppCompatActivity() {
var cancel = false var cancel = false
var focusView: View? = null var focusView: View? = null
if (!url.isBaseUrlValid(logErrors, this@LoginActivity)) { if (!url.isBaseUrlValid()) {
urlView.error = getString(R.string.login_url_problem) urlView.error = getString(R.string.login_url_problem)
focusView = urlView focusView = urlView
cancel = true cancel = true
@ -155,15 +154,14 @@ class LoginActivity : AppCompatActivity() {
alertDialog.setMessage(getString(R.string.text_wrong_url)) alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK", "OK"
{ dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
)
alertDialog.show() alertDialog.show()
inValidCount = 0 inValidCount = 0
} }
} }
if (isWithLogin) { if (isWithLogin || isWithHTTPLogin) {
if (TextUtils.isEmpty(password)) { if (TextUtils.isEmpty(password)) {
passwordView.error = getString(R.string.error_invalid_password) passwordView.error = getString(R.string.error_invalid_password)
focusView = passwordView focusView = passwordView
@ -177,20 +175,6 @@ class LoginActivity : AppCompatActivity() {
} }
} }
if (isWithHTTPLogin) {
if (TextUtils.isEmpty(httpPassword)) {
httpPasswordView.error = getString(R.string.error_invalid_password)
focusView = httpPasswordView
cancel = true
}
if (TextUtils.isEmpty(httpLogin)) {
httpLoginView.error = getString(R.string.error_field_required)
focusView = httpLoginView
cancel = true
}
}
if (cancel) { if (cancel) {
focusView?.requestFocus() focusView?.requestFocus()
} else { } else {
@ -208,53 +192,47 @@ class LoginActivity : AppCompatActivity() {
this, this,
this@LoginActivity, this@LoginActivity,
isWithSelfSignedCert, isWithSelfSignedCert,
-1L,
isWithSelfSignedCert isWithSelfSignedCert
) )
api.login().enqueue(object : Callback<SuccessResponse> {
if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) { private fun preferenceError(t: Throwable) {
api.login().enqueue(object : Callback<SuccessResponse> { editor.remove("url")
private fun preferenceError(t: Throwable) { editor.remove("login")
editor.remove("url") editor.remove("httpUserName")
editor.remove("login") editor.remove("password")
editor.remove("httpUserName") editor.remove("httpPassword")
editor.remove("password") editor.apply()
editor.remove("httpPassword") urlView.error = getString(R.string.wrong_infos)
editor.apply() loginView.error = getString(R.string.wrong_infos)
urlView.error = getString(R.string.wrong_infos) passwordView.error = getString(R.string.wrong_infos)
loginView.error = getString(R.string.wrong_infos) httpLoginView.error = getString(R.string.wrong_infos)
passwordView.error = getString(R.string.wrong_infos) httpPasswordView.error = getString(R.string.wrong_infos)
httpLoginView.error = getString(R.string.wrong_infos) if (logErrors) {
httpPasswordView.error = getString(R.string.wrong_infos) ACRA.getErrorReporter().maybeHandleSilentException(t, this@LoginActivity)
if (logErrors) { Toast.makeText(
ACRA.getErrorReporter().maybeHandleSilentException(t, this@LoginActivity) this@LoginActivity,
Toast.makeText( t.message,
this@LoginActivity, Toast.LENGTH_LONG
t.message, ).show()
Toast.LENGTH_LONG
).show()
}
showProgress(false)
} }
showProgress(false)
}
override fun onResponse( override fun onResponse(
call: Call<SuccessResponse>, call: Call<SuccessResponse>,
response: Response<SuccessResponse> response: Response<SuccessResponse>
) { ) {
if (response.body() != null && response.body()!!.isSuccess) { if (response.body() != null && response.body()!!.isSuccess) {
goToMain() goToMain()
} else { } else {
preferenceError(Exception("No response body...")) preferenceError(Exception("No response body..."))
}
} }
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
preferenceError(t) preferenceError(t)
} }
}) })
} else {
showProgress(false)
}
} }
} }

View File

@ -11,9 +11,17 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
val intent = Intent(this, LoginActivity::class.java) if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean(
"firstStart",
true
)) {
val i = Intent(this@MainActivity, IntroActivity::class.java)
startActivity(i)
} else {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
}
startActivity(intent)
finish() finish()
} }
} }

View File

@ -1,16 +1,14 @@
package apps.amine.bou.readerforselfoss package apps.amine.bou.readerforselfoss
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build
import android.preference.PreferenceManager import android.preference.PreferenceManager
import androidx.multidex.MultiDexApplication
import android.widget.ImageView import android.widget.ImageView
import androidx.multidex.MultiDexApplication
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth import com.anupcowkur.reservoir.Reservoir
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
@ -29,8 +27,10 @@ import java.io.IOException
import java.util.UUID.randomUUID import java.util.UUID.randomUUID
@AcraHttpSender(uri = "http://37.187.110.167/amine/acra/simplest-acra.php", @AcraHttpSender(uri = "http://amine-bou.fr:5984/acra-selfoss/_design/acra-storage/_update/report",
httpMethod = HttpSender.Method.POST) basicAuthLogin = "selfoss",
basicAuthPassword = "selfoss",
httpMethod = HttpSender.Method.PUT)
@AcraDialog(resText = R.string.crash_dialog_text, @AcraDialog(resText = R.string.crash_dialog_text,
resCommentPrompt = R.string.crash_dialog_comment, resCommentPrompt = R.string.crash_dialog_comment,
resTheme = android.R.style.Theme_DeviceDefault_Dialog) resTheme = android.R.style.Theme_DeviceDefault_Dialog)
@ -43,13 +43,14 @@ import java.util.UUID.randomUUID
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA], ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA],
buildConfigClass = BuildConfig::class) buildConfigClass = BuildConfig::class)
class MyApp : MultiDexApplication() { class MyApp : MultiDexApplication() {
private lateinit var config: Config
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
config = Config(baseContext)
initAmplify() initAmplify()
initCache()
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
if (prefs.getString("unique_id", "").isEmpty()) { if (prefs.getString("unique_id", "").isEmpty()) {
val editor = prefs.edit() val editor = prefs.edit()
@ -62,25 +63,6 @@ class MyApp : MultiDexApplication() {
initTheme() initTheme()
tryToHandleBug() tryToHandleBug()
handleNotificationChannels()
}
private fun handleNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val name = getString(R.string.notification_channel_sync)
val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel(Config.syncChannelId, name, importance)
val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel)
}
} }
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
@ -98,6 +80,14 @@ class MyApp : MultiDexApplication() {
.applyAllDefaultRules() .applyAllDefaultRules()
} }
private fun initCache() {
try {
Reservoir.init(this, 8192) //in bytes
} catch (e: IOException) {
//failure
}
}
private fun initDrawerImageLoader() { private fun initDrawerImageLoader() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set( override fun set(
@ -107,7 +97,7 @@ class MyApp : MultiDexApplication() {
tag: String? tag: String?
) { ) {
Glide.with(imageView?.context) Glide.with(imageView?.context)
.loadMaybeBasicAuth(config, uri.toString()) .load(uri)
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder)) .apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
.into(imageView) .into(imageView)
} }

View File

@ -1,7 +1,5 @@
package apps.amine.bou.readerforselfoss package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.SharedPreferences
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -15,23 +13,14 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.room.Room
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded import apps.amine.bou.readerforselfoss.utils.succeeded
import apps.amine.bou.readerforselfoss.utils.toggleStar import apps.amine.bou.readerforselfoss.utils.toggleStar
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
@ -41,7 +30,6 @@ import org.acra.ACRA
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import kotlin.concurrent.thread
class ReaderActivity : AppCompatActivity() { class ReaderActivity : AppCompatActivity() {
@ -54,13 +42,6 @@ class ReaderActivity : AppCompatActivity() {
private lateinit var toolbarMenu: Menu private lateinit var toolbarMenu: Menu
private lateinit var db: AppDatabase
private lateinit var prefs: SharedPreferences
private var activeAlignment: Int = 1
val JUSTIFY = 1
val ALIGN_LEFT = 2
private fun showMenuItem(willAddToFavorite: Boolean) { private fun showMenuItem(willAddToFavorite: Boolean) {
toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite
toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite
@ -74,21 +55,14 @@ class ReaderActivity : AppCompatActivity() {
showMenuItem(false) showMenuItem(false)
} }
private lateinit var editor: SharedPreferences.Editor
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_reader) setContentView(R.layout.activity_reader)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
val scoop = Scoop.getInstance() val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolBar) scoop.bind(this, Toppings.PRIMARY.value, toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value) scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
} }
@ -96,22 +70,16 @@ class ReaderActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
val settings = val prefs = PreferenceManager.getDefaultSharedPreferences(this)
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
editor = prefs.edit()
debugReadingItems = prefs.getBoolean("read_debug", false) debugReadingItems = prefs.getBoolean("read_debug", false)
userIdentifier = prefs.getString("unique_id", "") userIdentifier = prefs.getString("unique_id", "")
markOnScroll = prefs.getBoolean("mark_on_scroll", false) markOnScroll = prefs.getBoolean("mark_on_scroll", false)
activeAlignment = prefs.getInt("text_align", JUSTIFY)
api = SelfossApi( api = SelfossApi(
this, this,
this@ReaderActivity, this@ReaderActivity,
settings.getBoolean("isSelfSignedCert", false), prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false) prefs.getBoolean("should_log_everything", false)
) )
@ -121,10 +89,9 @@ class ReaderActivity : AppCompatActivity() {
currentItem = intent.getIntExtra("currentItem", 0) currentItem = intent.getIntExtra("currentItem", 0)
readItem(allItems[currentItem]) readItem(allItems[currentItem].id)
pager.adapter = pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
pager.currentItem = currentItem pager.currentItem = currentItem
} }
@ -134,7 +101,8 @@ class ReaderActivity : AppCompatActivity() {
notifyAdapter() notifyAdapter()
pager.setPageTransformer(true, DepthPageTransformer()) pager.setPageTransformer(true, DepthPageTransformer())
(indicator as CircleIndicator).setViewPager(pager) // TODO: add back the page indicator
// (indicator as CircleIndicator).setViewPager(pager as android.support.v4.view.ViewPager)
pager.addOnPageChangeListener( pager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() { object : ViewPager.SimpleOnPageChangeListener() {
@ -146,57 +114,44 @@ class ReaderActivity : AppCompatActivity() {
} else { } else {
canFavorite() canFavorite()
} }
readItem(allItems[pager.currentItem]) readItem(allItems[pager.currentItem].id)
} }
} }
) )
} }
fun readItem(item: Item) { fun readItem(id: String) {
if (markOnScroll) { if (markOnScroll) {
thread { api.markItem(id).enqueue(
db.itemsDao().delete(item.toEntity()) object : Callback<SuccessResponse> {
} override fun onResponse(
if (this@ReaderActivity.isNetworkAccessible(this@ReaderActivity.findViewById(R.id.reader_activity_view))) { call: Call<SuccessResponse>,
api.markItem(item.id).enqueue( response: Response<SuccessResponse>
object : Callback<SuccessResponse> { ) {
override fun onResponse( if (!response.succeeded() && debugReadingItems) {
call: Call<SuccessResponse>, val message =
response: Response<SuccessResponse> "message: ${response.message()} " +
) { "response isSuccess: ${response.isSuccessful} " +
if (!response.succeeded() && debugReadingItems) { "response code: ${response.code()} " +
val message = "response message: ${response.message()} " +
"message: ${response.message()} " + "response errorBody: ${response.errorBody()?.string()} " +
"response isSuccess: ${response.isSuccessful} " + "body success: ${response.body()?.success} " +
"response code: ${response.code()} " + "body isSuccess: ${response.body()?.isSuccess}"
"response message: ${response.message()} " + ACRA.getErrorReporter()
"response errorBody: ${response.errorBody()?.string()} " + .maybeHandleSilentException(Exception(message), this@ReaderActivity)
"body success: ${response.body()?.success} " + }
"body isSuccess: ${response.body()?.isSuccess}" }
ACRA.getErrorReporter()
.maybeHandleSilentException(Exception(message), this@ReaderActivity) override fun onFailure(
} call: Call<SuccessResponse>,
} t: Throwable
) {
override fun onFailure( if (debugReadingItems) {
call: Call<SuccessResponse>, ACRA.getErrorReporter().maybeHandleSilentException(t, this@ReaderActivity)
t: Throwable
) {
thread {
db.itemsDao().insertAllItems(item.toEntity())
}
if (debugReadingItems) {
ACRA.getErrorReporter()
.maybeHandleSilentException(t, this@ReaderActivity)
}
} }
} }
)
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(item.id, true, false, false, false))
} }
} )
} }
} }
@ -219,6 +174,7 @@ class ReaderActivity : AppCompatActivity() {
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) : private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
FragmentStatePagerAdapter(fm) { FragmentStatePagerAdapter(fm) {
override fun getCount(): Int { override fun getCount(): Int {
return allItems.size return allItems.size
} }
@ -230,20 +186,10 @@ class ReaderActivity : AppCompatActivity() {
override fun startUpdate(container: ViewGroup) { override fun startUpdate(container: ViewGroup) {
super.startUpdate(container) super.startUpdate(container)
container.background = ColorDrawable( container.background = ColorDrawable(ContextCompat.getColor(this@ReaderActivity, appColors.colorBackground))
ContextCompat.getColor(
this@ReaderActivity,
appColors.colorBackground
)
)
} }
} }
fun alignmentMenu(showJustify: Boolean) {
toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater val inflater = menuInflater
inflater.inflate(R.menu.reader_menu, menu) inflater.inflate(R.menu.reader_menu, menu)
@ -254,115 +200,68 @@ class ReaderActivity : AppCompatActivity() {
} else { } else {
canFavorite() canFavorite()
} }
if (activeAlignment == JUSTIFY) {
alignmentMenu(false)
} else {
alignmentMenu(true)
}
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() {
allItems[pager.currentItem] =
allItems[pager.currentItem].toggleStar()
notifyAdapter()
canRemoveFromFavorite()
}
fun afterUnsave() {
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
notifyAdapter()
canFavorite()
}
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
onBackPressed() onBackPressed()
return true return true
} }
R.id.save -> { R.id.save -> {
if (this@ReaderActivity.isNetworkAccessible(null)) { api.starrItem(allItems[pager.currentItem].id)
api.starrItem(allItems[pager.currentItem].id) .enqueue(object : Callback<SuccessResponse> {
.enqueue(object : Callback<SuccessResponse> { override fun onResponse(
override fun onResponse( call: Call<SuccessResponse>,
call: Call<SuccessResponse>, response: Response<SuccessResponse>
response: Response<SuccessResponse> ) {
) { allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
afterSave() notifyAdapter()
} canRemoveFromFavorite()
}
override fun onFailure( override fun onFailure(
call: Call<SuccessResponse>, call: Call<SuccessResponse>,
t: Throwable t: Throwable
) { ) {
Toast.makeText( Toast.makeText(
baseContext, baseContext,
R.string.cant_mark_favortie, R.string.cant_mark_favortie,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) })
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, true, false))
afterSave()
}
}
} }
R.id.unsave -> { R.id.unsave -> {
if (this@ReaderActivity.isNetworkAccessible(null)) { api.unstarrItem(allItems[pager.currentItem].id)
api.unstarrItem(allItems[pager.currentItem].id) .enqueue(object : Callback<SuccessResponse> {
.enqueue(object : Callback<SuccessResponse> { override fun onResponse(
override fun onResponse( call: Call<SuccessResponse>,
call: Call<SuccessResponse>, response: Response<SuccessResponse>
response: Response<SuccessResponse> ) {
) { allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
afterUnsave() notifyAdapter()
} canFavorite()
}
override fun onFailure( override fun onFailure(
call: Call<SuccessResponse>, call: Call<SuccessResponse>,
t: Throwable t: Throwable
) { ) {
Toast.makeText( Toast.makeText(
baseContext, baseContext,
R.string.cant_unmark_favortie, R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) })
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, false, true))
afterUnsave()
}
}
}
R.id.align_left -> {
editor.putInt("text_align", ALIGN_LEFT)
editor.apply()
alignmentMenu(true)
refreshFragment()
}
R.id.align_justify -> {
editor.putInt("text_align", JUSTIFY)
editor.apply()
alignmentMenu(false)
refreshFragment()
} }
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun refreshFragment() {
finish()
overridePendingTransition(0, 0)
startActivity(intent)
overridePendingTransition(0, 0)
}
companion object { companion object {
var allItems: ArrayList<Item> = ArrayList() var allItems: ArrayList<Item> = ArrayList()
} }

View File

@ -1,21 +1,18 @@
package apps.amine.bou.readerforselfoss package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Source import apps.amine.bou.readerforselfoss.api.selfoss.Sources
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_sources.* import kotlinx.android.synthetic.main.activity_sources.*
import retrofit2.Call import retrofit2.Call
@ -56,52 +53,47 @@ class SourcesActivity : AppCompatActivity() {
super.onResume() super.onResume()
val mLayoutManager = LinearLayoutManager(this) val mLayoutManager = LinearLayoutManager(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val api = SelfossApi( val api = SelfossApi(
this, this,
this@SourcesActivity, this@SourcesActivity,
settings.getBoolean("isSelfSignedCert", false), prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false) prefs.getBoolean("should_log_everything", false)
) )
var items: ArrayList<Source> = ArrayList() var items: ArrayList<Sources> = ArrayList()
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = mLayoutManager recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAccessible(this@SourcesActivity.findViewById(R.id.recyclerView))) { api.sources.enqueue(object : Callback<List<Sources>> {
api.sources.enqueue(object : Callback<List<Source>> { override fun onResponse(
override fun onResponse( call: Call<List<Sources>>,
call: Call<List<Source>>, response: Response<List<Sources>>
response: Response<List<Source>> ) {
) { if (response.body() != null && response.body()!!.isNotEmpty()) {
if (response.body() != null && response.body()!!.isNotEmpty()) { items = response.body() as ArrayList<Sources>
items = response.body() as ArrayList<Source>
}
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
this@SourcesActivity,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
} }
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
override fun onFailure(call: Call<List<Source>>, t: Throwable) { recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText( Toast.makeText(
this@SourcesActivity, this@SourcesActivity,
R.string.cant_get_sources, R.string.nothing_here,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) }
}
override fun onFailure(call: Call<List<Sources>>, t: Throwable) {
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT
).show()
}
})
fab.setOnClickListener { fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java)) startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))

View File

@ -14,16 +14,11 @@ import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink import apps.amine.bou.readerforselfoss.utils.shareLink
@ -38,13 +33,11 @@ import kotlinx.android.synthetic.main.card_item.view.*
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import kotlin.concurrent.thread
class ItemCardAdapter( class ItemCardAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<Item>, override var items: ArrayList<Item>,
override val api: SelfossApi, override val api: SelfossApi,
override val db: AppDatabase,
private val helper: CustomTabActivityHelper, private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean, private val internalBrowser: Boolean,
private val articleViewer: Boolean, private val articleViewer: Boolean,
@ -52,7 +45,6 @@ class ItemCardAdapter(
override val appColors: AppColors, override val appColors: AppColors,
override val debugReadingItems: Boolean, override val debugReadingItems: Boolean,
override val userIdentifier: String, override val userIdentifier: String,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
@ -71,7 +63,6 @@ class ItemCardAdapter(
holder.mView.favButton.isLiked = itm.starred holder.mView.favButton.isLiked = itm.starred
holder.mView.title.text = Html.fromHtml(itm.title) holder.mView.title.text = Html.fromHtml(itm.title)
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.title.setLinkTextColor(appColors.colorAccent) holder.mView.title.setLinkTextColor(appColors.colorAccent)
@ -88,7 +79,7 @@ class ItemCardAdapter(
holder.mView.itemImage.setImageDrawable(null) holder.mView.itemImage.setImageDrawable(null)
} else { } else {
holder.mView.itemImage.visibility = View.VISIBLE holder.mView.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(c), holder.mView.itemImage) c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
} }
if (itm.getIcon(c).isEmpty()) { if (itm.getIcon(c).isEmpty()) {
@ -101,7 +92,7 @@ class ItemCardAdapter(
.build(itm.sourcetitle.toTextDrawableString(c), color) .build(itm.sourcetitle.toTextDrawableString(c), color)
holder.mView.sourceImage.setImageDrawable(drawable) holder.mView.sourceImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(c), holder.mView.sourceImage) c.circularBitmapDrawable(itm.getIcon(c), holder.mView.sourceImage)
} }
holder.mView.favButton.isLiked = itm.starred holder.mView.favButton.isLiked = itm.starred
@ -123,66 +114,53 @@ class ItemCardAdapter(
mView.favButton.setOnLikeListener(object : OnLikeListener { mView.favButton.setOnLikeListener(object : OnLikeListener {
override fun liked(likeButton: LikeButton) { override fun liked(likeButton: LikeButton) {
val (id) = items[adapterPosition] val (id) = items[adapterPosition]
if (c.isNetworkAccessible(null)) { api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
api.starrItem(id).enqueue(object : Callback<SuccessResponse> { override fun onResponse(
override fun onResponse( call: Call<SuccessResponse>,
call: Call<SuccessResponse>, response: Response<SuccessResponse>
response: Response<SuccessResponse> ) {
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(id, false, false, true, false))
} }
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} }
override fun unLiked(likeButton: LikeButton) { override fun unLiked(likeButton: LikeButton) {
val (id) = items[adapterPosition] val (id) = items[adapterPosition]
if (c.isNetworkAccessible(null)) { api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> { override fun onResponse(
override fun onResponse( call: Call<SuccessResponse>,
call: Call<SuccessResponse>, response: Response<SuccessResponse>
response: Response<SuccessResponse> ) {
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(id, false, false, false, true))
} }
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} }
}) })
mView.shareBtn.setOnClickListener { mView.shareBtn.setOnClickListener {
val item = items[adapterPosition] c.shareLink(items[adapterPosition].getLinkDecoded())
c.shareLink(item.getLinkDecoded(), item.title)
} }
mView.browserBtn.setOnClickListener { mView.browserBtn.setOnClickListener {

View File

@ -5,23 +5,16 @@ import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.text.Html import android.text.Html
import android.text.Spannable
import android.text.style.ClickableSpan
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
@ -46,18 +39,18 @@ class ItemListAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<Item>, override var items: ArrayList<Item>,
override val api: SelfossApi, override val api: SelfossApi,
override val db: AppDatabase,
private val helper: CustomTabActivityHelper, private val helper: CustomTabActivityHelper,
private val clickBehavior: Boolean,
private val internalBrowser: Boolean, private val internalBrowser: Boolean,
private val articleViewer: Boolean, private val articleViewer: Boolean,
override val debugReadingItems: Boolean, override val debugReadingItems: Boolean,
override val userIdentifier: String, override val userIdentifier: String,
override val appColors: AppColors, override val appColors: AppColors,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() { ) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
private val c: Context = app.baseContext private val c: Context = app.baseContext
private val bars: ArrayList<Boolean> = ArrayList(Collections.nCopies(items.size + 1, false))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate( val v = LayoutInflater.from(c).inflate(
@ -74,8 +67,6 @@ class ItemListAdapter(
holder.mView.title.text = Html.fromHtml(itm.title) holder.mView.title.text = Html.fromHtml(itm.title)
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.title.setLinkTextColor(appColors.colorAccent) holder.mView.title.setLinkTextColor(appColors.colorAccent)
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText() holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
@ -110,11 +101,24 @@ class ItemListAdapter(
holder.mView.itemImage.setImageDrawable(drawable) holder.mView.itemImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(c), holder.mView.itemImage) c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
} }
} else { } else {
c.bitmapCenterCrop(config, itm.getThumbnail(c), holder.mView.itemImage) c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
} }
// TODO: maybe handle this differently. It crashes when changing tab
try {
if (bars[position]) {
holder.mView.actionBar.visibility = View.VISIBLE
} else {
holder.mView.actionBar.visibility = View.GONE
}
} catch (e: IndexOutOfBoundsException) {
holder.mView.actionBar.visibility = View.GONE
}
holder.mView.favButton.isLiked = itm.starred
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
@ -122,23 +126,114 @@ class ItemListAdapter(
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) { inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
init { init {
handleClickListeners()
handleCustomTabActions() handleCustomTabActions()
} }
private fun handleClickListeners() {
mView.favButton.setOnLikeListener(object : OnLikeListener {
override fun liked(likeButton: LikeButton) {
val (id) = items[adapterPosition]
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
}
override fun unLiked(likeButton: LikeButton) {
val (id) = items[adapterPosition]
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
}
})
mView.shareBtn.setOnClickListener {
c.shareLink(items[adapterPosition].getLinkDecoded())
}
mView.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[adapterPosition])
}
}
private fun handleCustomTabActions() { private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent() val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app) helper.bindCustomTabsService(app)
mView.setOnClickListener {
c.openItemUrl( if (!clickBehavior) {
items, mView.setOnClickListener {
adapterPosition, c.openItemUrl(
items[adapterPosition].getLinkDecoded(), items,
customTabsIntent, adapterPosition,
internalBrowser, items[adapterPosition].getLinkDecoded(),
articleViewer, customTabsIntent,
app internalBrowser,
) articleViewer,
app
)
}
mView.setOnLongClickListener {
actionBarShowHide()
true
}
} else {
mView.setOnClickListener { actionBarShowHide() }
mView.setOnLongClickListener {
c.openItemUrl(
items,
adapterPosition,
items[adapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,
app
)
true
}
}
}
private fun actionBarShowHide() {
bars[adapterPosition] = true
if (mView.actionBar.visibility == View.GONE) {
mView.actionBar.visibility = View.VISIBLE
} else {
mView.actionBar.visibility = View.GONE
} }
} }
} }

View File

@ -2,7 +2,6 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.graphics.Color import android.graphics.Color
import com.google.android.material.snackbar.Snackbar
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@ -10,29 +9,22 @@ import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded import apps.amine.bou.readerforselfoss.utils.succeeded
import com.google.android.material.snackbar.Snackbar
import org.acra.ACRA import org.acra.ACRA
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import kotlin.concurrent.thread
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() { abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
abstract var items: ArrayList<Item> abstract var items: ArrayList<Item>
abstract val api: SelfossApi abstract val api: SelfossApi
abstract val db: AppDatabase
abstract val debugReadingItems: Boolean abstract val debugReadingItems: Boolean
abstract val userIdentifier: String abstract val userIdentifier: String
abstract val app: Activity abstract val app: Activity
abstract val appColors: AppColors abstract val appColors: AppColors
abstract val config: Config
abstract val updateItems: (ArrayList<Item>) -> Unit abstract val updateItems: (ArrayList<Item>) -> Unit
fun updateAllItems(newItems: ArrayList<Item>) { fun updateAllItems(newItems: ArrayList<Item>) {
@ -41,7 +33,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
updateItems(items) updateItems(items)
} }
private fun unmarkSnackbar(i: Item, position: Int) { private fun doUnmark(i: Item, position: Int) {
val s = Snackbar val s = Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
@ -50,34 +42,23 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
items.add(position, i) items.add(position, i)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
notifyItemInserted(position) notifyItemInserted(position)
updateItems(items) updateItems(items)
if (app.isNetworkAccessible(null)) { api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> { override fun onResponse(
override fun onResponse( call: Call<SuccessResponse>,
call: Call<SuccessResponse>, response: Response<SuccessResponse>
response: Response<SuccessResponse> ) {
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().delete(i.toEntity())
}
notifyItemRemoved(position)
updateItems(items)
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
} }
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
doUnmark(i, position)
}
})
} }
val view = s.view val view = s.view
@ -86,177 +67,51 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show() s.show()
} }
private fun markSnackbar(i: Item, position: Int) { fun removeItemAtIndex(position: Int) {
val s = Snackbar
.make( val i = items[position]
app.findViewById(R.id.coordLayout),
R.string.marked_as_unread, items.remove(i)
Snackbar.LENGTH_LONG notifyItemRemoved(position)
) updateItems(items)
.setAction(R.string.undo_string) {
items.add(position, i)
thread { api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
db.itemsDao().delete(i.toEntity()) override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), app)
Toast.makeText(app.baseContext, message, Toast.LENGTH_LONG).show()
} }
doUnmark(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
}
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(i)
notifyItemInserted(position) notifyItemInserted(position)
updateItems(items) updateItems(items)
if (app.isNetworkAccessible(null)) {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
notifyItemRemoved(position)
updateItems(items)
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
}
} }
})
val view = s.view
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
s.show()
}
fun handleItemAtIndex(position: Int) {
if (unreadItemStatusAtIndex(position)) {
readItemAtIndex(position)
} else {
unreadItemAtIndex(position)
}
}
fun unreadItemStatusAtIndex(position: Int): Boolean {
return items[position].unread
}
private fun readItemAtIndex(position: Int) {
val i = items[position]
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
thread {
db.itemsDao().delete(i.toEntity())
}
if (app.isNetworkAccessible(null)) {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"MARK message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), app)
Toast.makeText(app.baseContext, message, Toast.LENGTH_LONG).show()
}
unmarkSnackbar(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
}
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(position, i)
notifyItemInserted(position)
updateItems(items)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
}
}
private fun unreadItemAtIndex(position: Int) {
val i = items[position]
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"UNMARK message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), app)
Toast.makeText(app.baseContext, message, Toast.LENGTH_LONG).show()
}
markSnackbar(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
}
Toast.makeText(
app,
app.getString(R.string.cant_mark_unread),
Toast.LENGTH_SHORT
).show()
items.add(i)
notifyItemInserted(position)
updateItems(items)
thread {
db.itemsDao().delete(i.toEntity())
}
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
}
} }
fun addItemAtIndex(item: Item, position: Int) { fun addItemAtIndex(item: Item, position: Int) {

View File

@ -10,11 +10,9 @@ import android.widget.Button
import android.widget.Toast import android.widget.Toast
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Source import apps.amine.bou.readerforselfoss.api.selfoss.Sources
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
@ -25,12 +23,11 @@ import retrofit2.Response
class SourcesListAdapter( class SourcesListAdapter(
private val app: Activity, private val app: Activity,
private val items: ArrayList<Source>, private val items: ArrayList<Sources>,
private val api: SelfossApi private val api: SelfossApi
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() { ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
private lateinit var config: Config
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate( val v = LayoutInflater.from(c).inflate(
@ -43,7 +40,6 @@ class SourcesListAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position] val itm = items[position]
config = Config(c)
if (itm.getIcon(c).isEmpty()) { if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.title) val color = generator.getColor(itm.title)
@ -55,7 +51,7 @@ class SourcesListAdapter(
.build(itm.title.toTextDrawableString(c), color) .build(itm.title.toTextDrawableString(c), color)
holder.mView.itemImage.setImageDrawable(drawable) holder.mView.itemImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(c), holder.mView.itemImage) c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
} }
holder.mView.sourceTitle.text = itm.title holder.mView.sourceTitle.text = itm.title
@ -74,35 +70,33 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener { deleteBtn.setOnClickListener {
if (c.isNetworkAccessible(null)) { val (id) = items[adapterPosition]
val (id) = items[adapterPosition] api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> { override fun onResponse(
override fun onResponse( call: Call<SuccessResponse>,
call: Call<SuccessResponse>, response: Response<SuccessResponse>
response: Response<SuccessResponse> ) {
) { if (response.body() != null && response.body()!!.isSuccess) {
if (response.body() != null && response.body()!!.isSuccess) { items.removeAt(adapterPosition)
items.removeAt(adapterPosition) notifyItemRemoved(adapterPosition)
notifyItemRemoved(adapterPosition) notifyItemRangeChanged(adapterPosition, itemCount)
notifyItemRangeChanged(adapterPosition, itemCount) } else {
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText( Toast.makeText(
app, app,
R.string.can_delete_source, R.string.can_delete_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) }
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
})
} }
} }
} }

View File

@ -18,13 +18,11 @@ import retrofit2.Call
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
class SelfossApi( class SelfossApi(
c: Context, c: Context,
callingActivity: Activity?, callingActivity: Activity,
isWithSelfSignedCert: Boolean, isWithSelfSignedCert: Boolean,
timeout: Long,
shouldLog: Boolean shouldLog: Boolean
) { ) {
@ -40,25 +38,16 @@ class SelfossApi(
this this
} }
fun OkHttpClient.Builder.maybeWithSettingsTimeout(timeout: Long): OkHttpClient.Builder =
if (timeout != -1L) {
this.readTimeout(timeout, TimeUnit.SECONDS)
.connectTimeout(timeout, TimeUnit.SECONDS)
} else {
this
}
fun Credentials.createAuthenticator(): DispatchingAuthenticator = fun Credentials.createAuthenticator(): DispatchingAuthenticator =
DispatchingAuthenticator.Builder() DispatchingAuthenticator.Builder()
.with("digest", DigestAuthenticator(this)) .with("digest", DigestAuthenticator(this))
.with("basic", BasicAuthenticator(this)) .with("basic", BasicAuthenticator(this))
.build() .build()
fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean, timeout: Long): OkHttpClient.Builder { fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean): OkHttpClient.Builder {
val authCache = ConcurrentHashMap<String, CachingAuthenticator>() val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
return OkHttpClient return OkHttpClient
.Builder() .Builder()
.maybeWithSettingsTimeout(timeout)
.maybeWithSelfSigned(isWithSelfSignedCert) .maybeWithSelfSigned(isWithSelfSignedCert)
.authenticator(CachingAuthenticatorDecorator(this, authCache)) .authenticator(CachingAuthenticatorDecorator(this, authCache))
.addInterceptor(AuthenticationCacheInterceptor(authCache)) .addInterceptor(AuthenticationCacheInterceptor(authCache))
@ -77,7 +66,6 @@ class SelfossApi(
val gson = val gson =
GsonBuilder() GsonBuilder()
.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter()) .registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
.registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter())
.setLenient() .setLenient()
.create() .create()
@ -89,7 +77,7 @@ class SelfossApi(
HttpLoggingInterceptor.Level.NONE HttpLoggingInterceptor.Level.NONE
} }
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout) val httpClient = authenticator.getHttpClien(isWithSelfSignedCert)
httpClient.addInterceptor(logging) httpClient.addInterceptor(logging)
@ -103,9 +91,7 @@ class SelfossApi(
.build() .build()
service = retrofit.create(SelfossService::class.java) service = retrofit.create(SelfossService::class.java)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
if (callingActivity != null) { Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
}
} }
} }
@ -139,9 +125,6 @@ class SelfossApi(
): Call<List<Item>> = ): Call<List<Item>> =
getItems("starred", tag, sourceId, search, itemsNumber, offset) getItems("starred", tag, sourceId, search, itemsNumber, offset)
fun allItems(): Call<List<Item>> =
service.allItems(userName, password)
private fun getItems( private fun getItems(
type: String, type: String,
tag: String?, tag: String?,
@ -176,7 +159,7 @@ class SelfossApi(
fun update(): Call<String> = fun update(): Call<String> =
service.update(userName, password) service.update(userName, password)
val sources: Call<List<Source>> val sources: Call<List<Sources>>
get() = service.sources(userName, password) get() = service.sources(userName, password)
fun deleteSource(id: String): Call<SuccessResponse> = fun deleteSource(id: String): Call<SuccessResponse> =

View File

@ -9,13 +9,13 @@ import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
private fun constructUrl(config: Config?, path: String, file: String?): String { private fun constructUrl(config: Config?, path: String, file: String): String {
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
return if (file.isEmptyOrNullOrNullString()) { return if (file.isEmptyOrNullOrNullString()) {
"" ""
} else { } else {
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString() baseUriBuilder.toString()
} }
} }
@ -42,10 +42,10 @@ data class Spout(
@SerializedName("description") val description: String @SerializedName("description") val description: String
) )
data class Source( data class Sources(
@SerializedName("id") val id: String, @SerializedName("id") val id: String,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@SerializedName("tags") val tags: SelfossTagType, @SerializedName("tags") val tags: String,
@SerializedName("spout") val spout: String, @SerializedName("spout") val spout: String,
@SerializedName("error") val error: String, @SerializedName("error") val error: String,
@SerializedName("icon") val icon: String @SerializedName("icon") val icon: String
@ -71,7 +71,7 @@ data class Item(
@SerializedName("icon") val icon: String, @SerializedName("icon") val icon: String,
@SerializedName("link") val link: String, @SerializedName("link") val link: String,
@SerializedName("sourcetitle") val sourcetitle: String, @SerializedName("sourcetitle") val sourcetitle: String,
@SerializedName("tags") val tags: SelfossTagType @SerializedName("tags") val tags: String
) : Parcelable { ) : Parcelable {
var config: Config? = null var config: Config? = null
@ -94,7 +94,7 @@ data class Item(
icon = source.readString(), icon = source.readString(),
link = source.readString(), link = source.readString(),
sourcetitle = source.readString(), sourcetitle = source.readString(),
tags = source.readParcelable(ClassLoader.getSystemClassLoader()) tags = source.readString()
) )
override fun describeContents() = 0 override fun describeContents() = 0
@ -110,7 +110,7 @@ data class Item(
dest.writeString(icon) dest.writeString(icon)
dest.writeString(link) dest.writeString(link)
dest.writeString(sourcetitle) dest.writeString(sourcetitle)
dest.writeParcelable(tags, flags) dest.writeString(tags)
} }
fun getIcon(app: Context): String { fun getIcon(app: Context): String {
@ -153,27 +153,4 @@ data class Item(
return stringUrl return stringUrl
} }
}
data class SelfossTagType(val tags: String) : Parcelable {
companion object {
@JvmField val CREATOR: Parcelable.Creator<SelfossTagType> =
object : Parcelable.Creator<SelfossTagType> {
override fun createFromParcel(source: Parcel): SelfossTagType =
SelfossTagType(source)
override fun newArray(size: Int): Array<SelfossTagType?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
tags = source.readString()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(tags)
}
} }

View File

@ -27,12 +27,6 @@ internal interface SelfossService {
@Query("offset") offset: Int @Query("offset") offset: Int
): Call<List<Item>> ): Call<List<Item>>
@GET("items")
fun allItems(
@Query("username") username: String,
@Query("password") password: String
): Call<List<Item>>
@Headers("Content-Type: application/x-www-form-urlencoded") @Headers("Content-Type: application/x-www-form-urlencoded")
@POST("mark/{id}") @POST("mark/{id}")
fun markAsRead( fun markAsRead(
@ -101,7 +95,7 @@ internal interface SelfossService {
fun sources( fun sources(
@Query("username") username: String, @Query("username") username: String,
@Query("password") password: String @Query("password") password: String
): Call<List<Source>> ): Call<List<Sources>>
@DELETE("source/{id}") @DELETE("source/{id}")
fun deleteSource( fun deleteSource(

View File

@ -1,22 +0,0 @@
package apps.amine.bou.readerforselfoss.api.selfoss
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import java.lang.reflect.Type
internal class SelfossTagTypeTypeAdapter : JsonDeserializer<SelfossTagType> {
@Throws(JsonParseException::class)
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): SelfossTagType? =
if (json.isJsonArray) {
SelfossTagType(json.asJsonArray.joinToString(",") { it.toString() })
} else {
SelfossTagType(json.toString())
}
}

View File

@ -1,152 +0,0 @@
package apps.amine.bou.readerforselfoss.background
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.preference.PreferenceManager
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 apps.amine.bou.readerforselfoss.MainActivity
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
import kotlin.concurrent.schedule
import kotlin.concurrent.thread
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
lateinit var db: AppDatabase
override fun doWork(): Result {
if (context.isNetworkAccessible(null)) {
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)
notificationManager.notify(1, notification.build())
val settings =
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
val api = SelfossApi(
this.context,
null,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1").toLong(),
sharedPref.getBoolean("should_log_everything", false)
)
api.allItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>)
db.itemsDao().deleteAllItems()
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
val newSize = apiItems.filter { it.unread }.size
if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val newItemsNotification = NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(context.getString(R.string.new_items_notification_text, newSize))
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
}
}
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
}
})
thread {
val actions = db.actionsDao().actions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(api.markItem(action.articleId), action)
action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action)
action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action)
action.unstarred -> doAndReportOnFail(
api.unstarrItem(action.articleId),
action
)
}
}
}
}
return Result.SUCCESS
}
private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) {
call.enqueue(object : Callback<T> {
override fun onResponse(
call: Call<T>,
response: Response<T>
) {
thread {
db.actionsDao().delete(action)
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
ACRA.getErrorReporter().maybeHandleSilentException(t, context)
}
})
}
}

View File

@ -1,54 +1,45 @@
package apps.amine.bou.readerforselfoss.fragments package apps.amine.bou.readerforselfoss.fragments
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.view.InflateException
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.appcompat.app.AlertDialog
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.WebSettings import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
import androidx.room.Room
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.openItemUrl import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.succeeded import apps.amine.bou.readerforselfoss.utils.succeeded
import apps.amine.bou.readerforselfoss.utils.toPx
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import kotlinx.android.synthetic.main.fragment_article.*
import kotlinx.android.synthetic.main.fragment_article.view.* import kotlinx.android.synthetic.main.fragment_article.view.*
import org.acra.ACRA import org.acra.ACRA
import retrofit2.Call import retrofit2.Call
@ -56,353 +47,269 @@ import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import kotlin.concurrent.thread
class ArticleFragment : Fragment() { class ArticleFragment : Fragment() {
private lateinit var pageNumber: Number private lateinit var pageNumber: Number
private var fontSize: Int = 16 private var fontSize: Int = 14
private lateinit var allItems: ArrayList<Item> private lateinit var allItems: ArrayList<Item>
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null; private lateinit var mCustomTabActivityHelper: CustomTabActivityHelper
private lateinit var url: String private lateinit var url: String
private lateinit var contentText: String private lateinit var contentText: String
private lateinit var contentSource: String private lateinit var contentSource: String
private lateinit var contentImage: String private lateinit var contentImage: String
private lateinit var contentTitle: String private lateinit var contentTitle: String
private var showMalformedUrl: Boolean = false
private lateinit var editor: SharedPreferences.Editor private lateinit var editor: SharedPreferences.Editor
private lateinit var fab: FloatingActionButton private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var db: AppDatabase
private lateinit var textAlignment: String
private lateinit var config: Config
private var rootView: ViewGroup? = null
private lateinit var prefs: SharedPreferences
private var typeface: Typeface? = null
private var resId: Int = 0
private var font = ""
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
if (mCustomTabActivityHelper != null) { mCustomTabActivityHelper.unbindCustomTabsService(activity)
mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(activity!!) appColors = AppColors(activity!!)
config = Config(activity!!)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
pageNumber = arguments!!.getInt(ARG_POSITION) pageNumber = arguments!!.getInt(ARG_POSITION)
allItems = arguments!!.getParcelableArrayList(ARG_ITEMS) allItems = arguments!!.getParcelableArrayList(ARG_ITEMS)
db = Room.databaseBuilder(
context!!,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
} }
private lateinit var rootView: ViewGroup
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
try { rootView = inflater
rootView = inflater .inflate(R.layout.fragment_article, container, false) as ViewGroup
.inflate(R.layout.fragment_article, container, false) as ViewGroup
url = allItems[pageNumber.toInt()].getLinkDecoded() url = allItems[pageNumber.toInt()].getLinkDecoded()
contentText = allItems[pageNumber.toInt()].content contentText = allItems[pageNumber.toInt()].content
contentTitle = allItems[pageNumber.toInt()].title contentTitle = allItems[pageNumber.toInt()].title
contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!) contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!)
contentSource = allItems[pageNumber.toInt()].sourceAndDateText() contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
prefs = PreferenceManager.getDefaultSharedPreferences(activity) val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit() editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "16").toInt() fontSize = prefs.getString("reader_font_size", "14").toInt()
showMalformedUrl = prefs.getBoolean("show_error_malformed_url", true)
font = prefs.getString("reader_font", "") val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
if (font.isNotEmpty()) { val debugReadingItems = prefs.getBoolean("read_debug", false)
resId = context!!.resources.getIdentifier(font, "font", context!!.packageName)
typeface = try {
ResourcesCompat.getFont(context!!, resId)!!
} catch (e: java.lang.Exception) {
ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), context!!)
// Just to be sure
null
}
}
refreshAlignment() val api = SelfossApi(
context!!,
activity!!,
settings.getBoolean("isSelfSignedCert", false),
prefs.getBoolean("should_log_everything", false)
)
val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) fab = rootView.fab
val debugReadingItems = prefs.getBoolean("read_debug", false)
val api = SelfossApi( fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
context!!,
activity!!,
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
fab = rootView!!.fab fab.rippleColor = appColors.colorAccentDark
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent) val floatingToolbar: FloatingToolbar = rootView.floatingToolbar
floatingToolbar.attachFab(fab)
fab.rippleColor = appColors.colorAccentDark floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val floatingToolbar: FloatingToolbar = rootView!!.floatingToolbar val customTabsIntent = activity!!.buildCustomTabsIntent()
floatingToolbar.attachFab(fab) mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper.bindCustomTabsService(activity)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val customTabsIntent = activity!!.buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper!!.bindCustomTabsService(activity)
floatingToolbar.setClickListener( floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener { object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) { override fun onItemClick(item: MenuItem) {
when (item.itemId) { when (item.itemId) {
R.id.more_action -> getContentFromMercury(customTabsIntent, prefs) R.id.more_action -> getContentFromMercury(customTabsIntent, prefs)
R.id.share_action -> activity!!.shareLink(url, contentTitle) R.id.share_action -> activity!!.shareLink(url)
R.id.open_action -> activity!!.openItemUrl( R.id.open_action -> activity!!.openItemUrl(
allItems, allItems,
pageNumber.toInt(), pageNumber.toInt(),
url, url,
customTabsIntent, customTabsIntent,
false, false,
false, false,
activity!! activity!!
) )
R.id.unread_action -> if ((context != null && context!!.isNetworkAccessible(null)) || context == null) { R.id.unread_action -> api.unmarkItem(allItems[pageNumber.toInt()].id).enqueue(
api.unmarkItem(allItems[pageNumber.toInt()].id).enqueue( object : Callback<SuccessResponse> {
object : Callback<SuccessResponse> { override fun onResponse(
override fun onResponse( call: Call<SuccessResponse>,
call: Call<SuccessResponse>, response: Response<SuccessResponse>
response: Response<SuccessResponse> ) {
) { if (!response.succeeded() && debugReadingItems) {
if (!response.succeeded() && debugReadingItems) { val message =
val message = "message: ${response.message()} " +
"message: ${response.message()} " + "response isSuccess: ${response.isSuccessful} " +
"response isSuccess: ${response.isSuccessful} " + "response code: ${response.code()} " +
"response code: ${response.code()} " + "response message: ${response.message()} " +
"response message: ${response.message()} " + "response errorBody: ${response.errorBody()?.string()} " +
"response errorBody: ${response.errorBody()?.string()} " + "body success: ${response.body()?.success} " +
"body success: ${response.body()?.success} " + "body isSuccess: ${response.body()?.isSuccess}"
"body isSuccess: ${response.body()?.isSuccess}" ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), activity!!)
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), activity!!) }
} }
}
override fun onFailure(
override fun onFailure( call: Call<SuccessResponse>,
call: Call<SuccessResponse>, t: Throwable
t: Throwable ) {
) { if (debugReadingItems) {
if (debugReadingItems) { ACRA.getErrorReporter().maybeHandleSilentException(t, activity!!)
ACRA.getErrorReporter().maybeHandleSilentException(t, activity!!)
}
}
} }
)
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pageNumber.toInt()].id, false, true, false, false))
} }
} }
else -> Unit )
} else -> Unit
}
override fun onItemLongClick(item: MenuItem?) {
} }
} }
)
rootView!!.source.text = contentSource override fun onItemLongClick(item: MenuItem?) {
if (typeface != null) { }
rootView!!.source.typeface = typeface
} }
)
if (contentText.isEmptyOrNullOrNullString()) { rootView.source.text = contentSource
getContentFromMercury(customTabsIntent, prefs)
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent, prefs)
} else {
rootView.titleView.text = contentTitle
htmlToWebview(contentText, prefs)
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
rootView.imageView.visibility = View.VISIBLE
Glide
.with(context!!)
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(rootView.imageView)
} else { } else {
rootView!!.titleView.text = contentTitle rootView.imageView.visibility = View.GONE
if (typeface != null) { }
rootView!!.titleView.typeface = typeface }
}
htmlToWebview() rootView.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (!contentImage.isEmptyOrNullOrNullString() && context != null) { if (scrollY > oldScrollY) {
rootView!!.imageView.visibility = View.VISIBLE fab.hide()
Glide
.with(context!!)
.asBitmap()
.loadMaybeBasicAuth(config, contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(rootView!!.imageView)
} else { } else {
rootView!!.imageView.visibility = View.GONE if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
} }
} }
)
rootView!!.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
fab.hide()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
}
)
} catch (e: InflateException) {
AlertDialog.Builder(context!!)
.setMessage(context!!.getString(R.string.webview_dialog_issue_message))
.setTitle(context!!.getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok
) { dialog, which ->
val sharedPref = PreferenceManager.getDefaultSharedPreferences(context!!)
val editor = sharedPref.edit()
editor.putBoolean("prefer_article_viewer", false)
editor.commit()
activity!!.finish()
}
.create()
.show()
}
return rootView return rootView
} }
private fun refreshAlignment() {
textAlignment = when (prefs.getInt("text_align", 1)) {
1 -> "justify"
2 -> "left"
else -> "justify"
}
}
private fun getContentFromMercury( private fun getContentFromMercury(
customTabsIntent: CustomTabsIntent, customTabsIntent: CustomTabsIntent,
prefs: SharedPreferences prefs: SharedPreferences
) { ) {
if ((context != null && context!!.isNetworkAccessible(null)) || context == null) { rootView.progressBar.visibility = View.VISIBLE
rootView!!.progressBar.visibility = View.VISIBLE val parser = MercuryApi(
val parser = MercuryApi( prefs.getBoolean("should_log_everything", false)
prefs.getBoolean("should_log_everything", false) )
)
parser.parseUrl(url).enqueue( parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> { object : Callback<ParsedContent> {
override fun onResponse( override fun onResponse(
call: Call<ParsedContent>, call: Call<ParsedContent>,
response: Response<ParsedContent> response: Response<ParsedContent>
) { ) {
// TODO: clean all the following after finding the mercury content issue // TODO: clean all the following after finding the mercury content issue
try { try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) { if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try { try {
rootView!!.titleView.text = response.body()!!.title rootView.titleView.text = response.body()!!.title
if (typeface != null) { url = response.body()!!.url
rootView!!.titleView.typeface = typeface } catch (e: Exception) {
} if (context != null) {
try { ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url)
url = response.body()!!.url
} catch (e: MalformedURLException) {
// Mercury returned a relative url. We do nothing.
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
contentText = response.body()!!.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
rootView!!.imageView.visibility = View.VISIBLE
try {
Glide
.with(context!!)
.asBitmap()
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.apply(RequestOptions.fitCenterTransform())
.into(rootView!!.imageView)
} catch (e: Exception) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} else {
rootView!!.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
rootView!!.nestedScrollView.scrollTo(0, 0)
rootView!!.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
} else {
try {
openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} }
} }
} catch (e: Exception) {
if (context != null) { try {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!) htmlToWebview(response.body()!!.content.orEmpty(), prefs)
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
rootView.imageView.visibility = View.VISIBLE
try {
Glide
.with(context!!)
.asBitmap()
.load(response.body()!!.lead_image_url)
.apply(RequestOptions.fitCenterTransform())
.into(rootView.imageView)
} catch (e: Exception) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} else {
rootView.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
rootView.nestedScrollView.scrollTo(0, 0)
rootView.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
} else {
try {
openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} }
} }
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} }
override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing(customTabsIntent)
} }
)
} override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing(customTabsIntent)
}
)
} }
private fun htmlToWebview() { private fun htmlToWebview(c: String, prefs: SharedPreferences) {
val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent) val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) rootView.webcontent.visibility = View.VISIBLE
val a: TypedArray = context!!.obtainStyledAttributes(resId, attrs)
rootView!!.webcontent.settings.standardFontFamily = a.getString(0)
rootView!!.webcontent.visibility = View.VISIBLE
val (textColor, backgroundColor) = if (appColors.isDarkTheme) { val (textColor, backgroundColor) = if (appColors.isDarkTheme) {
if (context != null) { if (context != null) {
rootView!!.webcontent.setBackgroundColor( rootView.webcontent.setBackgroundColor(
ContextCompat.getColor( ContextCompat.getColor(
context!!, context!!,
R.color.dark_webview R.color.dark_webview
@ -414,7 +321,7 @@ class ArticleFragment : Fragment() {
} }
} else { } else {
if (context != null) { if (context != null) {
rootView!!.webcontent.setBackgroundColor( rootView.webcontent.setBackgroundColor(
ContextCompat.getColor( ContextCompat.getColor(
context!!, context!!,
R.color.light_webview R.color.light_webview
@ -438,15 +345,15 @@ class ArticleFragment : Fragment() {
"#FFFFFF" "#FFFFFF"
} }
rootView!!.webcontent.settings.useWideViewPort = true rootView.webcontent.settings.useWideViewPort = true
rootView!!.webcontent.settings.loadWithOverviewMode = true rootView.webcontent.settings.loadWithOverviewMode = true
rootView!!.webcontent.settings.javaScriptEnabled = false rootView.webcontent.settings.javaScriptEnabled = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
rootView!!.webcontent.settings.layoutAlgorithm = rootView.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} else { } else {
rootView!!.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN rootView.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
} }
var baseUrl: String? = null var baseUrl: String? = null
@ -455,74 +362,80 @@ class ArticleFragment : Fragment() {
val itemUrl = URL(url) val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!) if (showMalformedUrl && context != null) {
val alertDialog = AlertDialog.Builder(context!!).create()
alertDialog.setTitle("Error")
alertDialog.setMessage("You are encountering a bug that I can't solve. Can you please contact me to solve the issue, please ?")
alertDialog.setButton(
AlertDialog.BUTTON_POSITIVE,
"Send mail"
) { dialog, _ ->
// This won't be translated because it should only be temporary.
val to = Config.feedbackEmail
val subject= "[ReaderForSelfoss MalformedURLException]"
val body= "Please specify the source, item and spout you are using for the url below : \n ${e.message}"
val mailTo = "mailto:" + to + "?&subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body)
val emailIntent = Intent(Intent.ACTION_VIEW)
emailIntent.data = Uri.parse(mailTo)
startActivity(emailIntent)
dialog.dismiss()
}
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"Not now"
) { dialog, _ -> dialog.dismiss() }
alertDialog.setButton(
AlertDialog.BUTTON_NEGATIVE,
"Don't show anymore."
) { dialog, _ ->
editor.putBoolean("show_error_malformed_url", false)
editor.apply()
dialog.dismiss()
}
alertDialog.show()
}
} }
val fontName = when (font) { rootView.webcontent.loadDataWithBaseURL(
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
else -> ""
}
val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
|<style>
| * {
| font-family: '$fontName';
| }
|</style>
""".trimMargin()
} else {
""
}
rootView!!.webcontent.loadDataWithBaseURL(
baseUrl, baseUrl,
"""<html> """<style>
|<head> |img {
| <meta name="viewport" content="width=device-width, initial-scale=1"> | display: inline-block;
| <style> | height: auto;
| img { | width: 100%;
| display: inline-block; | max-width: 100%;
| height: auto; |}
| width: 100%; |a {
| max-width: 100%; | color: $stringColor !important;
| } |}
| a { |*:not(a) {
| color: $stringColor !important; | color: $stringTextColor;
| } |}
| *:not(a) { |* {
| color: $stringTextColor; | font-size: ${fontSize.toPx}px;
| } | text-align: justify;
| * { | word-break: break-word;
| font-size: ${fontSize}px; | overflow:hidden;
| text-align: $textAlignment; |}
| word-break: break-word; |a, pre, code {
| overflow:hidden; | text-align: left;
| line-height: 1.5em; |}
| } |pre, code {
| a, pre, code { | white-space: pre-wrap;
| text-align: $textAlignment; | width:100%;
| } | background-color: $stringBackgroundColor;
| pre, code { |}</style>$c""".trimMargin(),
| white-space: pre-wrap; "text/html; charset=utf-8",
| width:100%;
| background-color: $stringBackgroundColor;
| }
| </style>
| $fontLinkAndStyle
|</head>
|<body>
| $contentText
|</body>""".trimMargin(),
"text/html",
"utf-8", "utf-8",
null null
) )
} }
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) { private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
rootView!!.progressBar.visibility = View.GONE rootView.progressBar.visibility = View.GONE
activity!!.openItemUrl( activity!!.openItemUrl(
allItems, allItems,
pageNumber.toInt(), pageNumber.toInt(),

View File

@ -1,23 +0,0 @@
package apps.amine.bou.readerforselfoss.persistence.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
@Dao
interface ActionsDao {
@Query("SELECT * FROM actions order by id asc")
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 apps.amine.bou.readerforselfoss.persistence.dao
import androidx.room.Delete
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
import apps.amine.bou.readerforselfoss.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 apps.amine.bou.readerforselfoss.persistence.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
import androidx.room.Update
@Dao
interface ItemsDao {
@Query("SELECT * FROM items order by id desc")
fun items(): List<ItemEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllItems(vararg items: ItemEntity)
@Query("DELETE FROM items")
fun deleteAllItems()
@Delete
fun delete(item: ItemEntity)
@Update
fun updateItem(item: ItemEntity)
}

View File

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

View File

@ -1,22 +0,0 @@
package apps.amine.bou.readerforselfoss.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 apps.amine.bou.readerforselfoss.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,32 +0,0 @@
package apps.amine.bou.readerforselfoss.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "items")
data class ItemEntity(
@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,16 +0,0 @@
package apps.amine.bou.readerforselfoss.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`))")
}
}

View File

@ -7,7 +7,6 @@ import android.preference.PreferenceActivity;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.material.appbar.AppBarLayout;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
@ -18,6 +17,7 @@ import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import com.ftinc.scoop.Scoop; import com.ftinc.scoop.Scoop;
import com.google.android.material.appbar.AppBarLayout;
import apps.amine.bou.readerforselfoss.R; import apps.amine.bou.readerforselfoss.R;
import apps.amine.bou.readerforselfoss.themes.AppColors; import apps.amine.bou.readerforselfoss.themes.AppColors;

View File

@ -8,7 +8,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -25,7 +24,6 @@ import android.text.Editable;
import android.text.InputFilter; import android.text.InputFilter;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
@ -126,27 +124,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
@TargetApi(Build.VERSION_CODES.HONEYCOMB) @TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) { public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.pref_headers, target); loadHeadersFromResource(R.xml.pref_headers, target);
AppColors appColors = new AppColors(this);
if (appColors != null && appColors.isDarkTheme()) {
for (Header header : target) {
tryLoadIconDark(header);
}
}
}
private void tryLoadIconDark(Header header){
try{
if (header.fragmentArguments != null) {
String iconDark = header.fragmentArguments.getString("iconDark");
int iconDarkId = getResources().getIdentifier(iconDark, "drawable", getPackageName());
if (iconDarkId != 0) {
header.iconRes = iconDarkId;
}
}
} catch (Exception e) {
Log.e("SettingsActivity", "Can not load dark icon", e);
}
} }
/** /**
@ -158,8 +135,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
return PreferenceFragment.class.getName().equals(fragmentName) return PreferenceFragment.class.getName().equals(fragmentName)
|| GeneralPreferenceFragment.class.getName().equals(fragmentName) || GeneralPreferenceFragment.class.getName().equals(fragmentName)
|| ArticleViewerPreferenceFragment.class.getName().equals(fragmentName) || ArticleViewerPreferenceFragment.class.getName().equals(fragmentName)
|| OfflinePreferenceFragment.class.getName().equals(fragmentName)
|| ExperimentalPreferenceFragment.class.getName().equals(fragmentName)
|| DebugPreferenceFragment.class.getName().equals(fragmentName) || DebugPreferenceFragment.class.getName().equals(fragmentName)
|| LinksPreferenceFragment.class.getName().equals(fragmentName) || LinksPreferenceFragment.class.getName().equals(fragmentName)
|| ThemePreferenceFragment.class.getName().equals(fragmentName); || ThemePreferenceFragment.class.getName().equals(fragmentName);
@ -177,6 +152,17 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
addPreferencesFromResource(R.xml.pref_general); addPreferencesFromResource(R.xml.pref_general);
setHasOptionsMenu(true); setHasOptionsMenu(true);
SwitchPreference cardViewActive = (SwitchPreference) findPreference("card_view_active");
final SwitchPreference tabOnTap = (SwitchPreference) findPreference("tab_on_tap");
tabOnTap.setEnabled(!cardViewActive.isChecked());
cardViewActive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean isEnabled = (Boolean) newValue;
tabOnTap.setEnabled(!isEnabled);
return true;
}
});
EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number"); EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number");
itemsNumber.getEditText().setFilters(new InputFilter[]{ itemsNumber.getEditText().setFilters(new InputFilter[]{
new InputFilter() { new InputFilter() {
@ -388,47 +374,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
} }
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class OfflinePreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_offline);
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class ExperimentalPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_experimental);
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId(); int id = item.getItemId();

View File

@ -3,12 +3,9 @@ package apps.amine.bou.readerforselfoss.themes
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.preference.PreferenceManager import android.preference.PreferenceManager
import androidx.annotation.ColorInt
import androidx.appcompat.view.ContextThemeWrapper
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.ColorInt
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import android.view.LayoutInflater
import android.view.ViewGroup
class AppColors(a: Activity) { class AppColors(a: Activity) {

View File

@ -2,21 +2,11 @@ package apps.amine.bou.readerforselfoss.utils
import android.content.Context import android.content.Context
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.provider.Settings
import org.acra.ErrorReporter import org.acra.ErrorReporter
fun ErrorReporter.maybeHandleSilentException(throwable: Throwable, ctx: Context) { fun ErrorReporter.maybeHandleSilentException(throwable: Throwable, ctx: Context) {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(ctx) val sharedPref = PreferenceManager.getDefaultSharedPreferences(ctx)
val isTestLab = Settings.System.getString(ctx.contentResolver, "firebase.test.lab") == "true" if (sharedPref.getBoolean("acra_should_log", false)) {
if (sharedPref.getBoolean("acra_should_log", false) && !isTestLab) {
this.handleSilentException(throwable)
}
}
fun ErrorReporter.doHandleSilentException(throwable: Throwable, ctx: Context) {
val isTestLab = Settings.System.getString(ctx.contentResolver, "firebase.test.lab") == "true"
if (!isTestLab) {
this.handleSilentException(throwable) this.handleSilentException(throwable)
} }
} }

View File

@ -25,12 +25,11 @@ fun String.toStringUriWithHttp(): String =
this this
} }
fun Context.shareLink(itemUrl: String, itemTitle: String) { fun Context.shareLink(itemUrl: String) {
val sendIntent = Intent() val sendIntent = Intent()
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
sendIntent.action = Intent.ACTION_SEND sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp()) sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp())
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
sendIntent.type = "text/plain" sendIntent.type = "text/plain"
startActivity( startActivity(
Intent.createChooser( Intent.createChooser(

View File

@ -36,10 +36,6 @@ class Config(c: Context) {
const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues" const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues"
const val syncChannelId = "sync-channel-id"
const val newItemsChannelId = "new-items-channel-id"
fun logoutAndRedirect( fun logoutAndRedirect(
c: Context, c: Context,
callingActivity: Activity, callingActivity: Activity,

View File

@ -3,7 +3,6 @@ package apps.amine.bou.readerforselfoss.utils
import android.content.Context import android.content.Context
import android.text.format.DateUtils import android.text.format.DateUtils
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
import org.acra.ACRA import org.acra.ACRA
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -45,8 +44,8 @@ fun Item.toggleStar(): Item {
fun List<Item>.flattenTags(): List<Item> = fun List<Item>.flattenTags(): List<Item> =
this.flatMap { this.flatMap {
val item = it val item = it
val tags: List<String> = it.tags.tags.split(",") val tags: List<String> = it.tags.split(",")
tags.map { t -> tags.map {
item.copy(tags = SelfossTagType(t.trim())) item.copy(tags = it.trim())
} }
} }

View File

@ -2,25 +2,18 @@ package apps.amine.bou.readerforselfoss.utils
import android.app.Activity import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.text.Spannable
import android.text.style.ClickableSpan
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import android.util.Patterns import android.util.Patterns
import android.view.MotionEvent
import android.view.View
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.ReaderActivity import apps.amine.bou.readerforselfoss.ReaderActivity
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.acra.ACRA
fun Context.buildCustomTabsIntent(): CustomTabsIntent { fun Context.buildCustomTabsIntent(): CustomTabsIntent {
@ -130,17 +123,13 @@ fun Context.openItemUrl(
private fun openInBrowser(linkDecoded: String, app: Activity) { private fun openInBrowser(linkDecoded: String, app: Activity) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(linkDecoded) intent.data = Uri.parse(linkDecoded)
try { app.startActivity(intent)
app.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(app.baseContext, e.message, Toast.LENGTH_LONG).show()
}
} }
fun String.isUrlValid(): Boolean = fun String.isUrlValid(): Boolean =
HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches() HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlValid(logErrors: Boolean, ctx: Context): Boolean { fun String.isBaseUrlValid(): Boolean {
val baseUrl = HttpUrl.parse(this) val baseUrl = HttpUrl.parse(this)
var existsAndEndsWithSlash = false var existsAndEndsWithSlash = false
if (baseUrl != null) { if (baseUrl != null) {
@ -157,40 +146,3 @@ fun Context.openInBrowserAsNewTask(i: Item) {
intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp()) intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp())
startActivity(intent) startActivity(intent)
} }
class LinkOnTouchListener: View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
var ret = false
val widget: TextView = v as TextView
val text: CharSequence = widget.text
val stext = Spannable.Factory.getInstance().newSpannable(text)
val action = event!!.action
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
var x: Float = event.x
var y: Float = event.y
x -= widget.totalPaddingLeft
y -= widget.totalPaddingTop
x += widget.scrollX
y += widget.scrollY
val layout = widget.layout
val line = layout.getLineForVertical(y.toInt())
val off = layout.getOffsetForHorizontal(line, x)
val link = stext.getSpans(off, off, ClickableSpan::class.java)
if (link.isNotEmpty()) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget)
}
ret = true
}
}
return ret
}
}

View File

@ -0,0 +1,54 @@
package apps.amine.bou.readerforselfoss.utils
import android.content.Context
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import android.util.AttributeSet
import android.view.View
class ScrollAwareFABBehavior(
context: Context,
attrs: AttributeSet
) : CoordinatorLayout.Behavior<FloatingActionButton>() {
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
directTargetChild: View,
target: View,
nestedScrollAxes: Int
): Boolean {
return true
}
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int
) {
super.onNestedScroll(
coordinatorLayout,
child,
target,
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed
)
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
child.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton?) {
super.onHidden(fab)
fab!!.visibility = View.INVISIBLE
}
})
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
child.show()
}
}
}

View File

@ -24,7 +24,7 @@ class CustomTabsHelper {
private static final String DEV_PACKAGE = "com.chrome.dev"; private static final String DEV_PACKAGE = "com.chrome.dev";
private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
"android.support.customtabs.extra.KEEP_ALIVE"; "androidx.browser.customtabs.extra.KEEP_ALIVE";
private static String sPackageNameToUse; private static String sPackageNameToUse;

View File

@ -2,30 +2,30 @@ package apps.amine.bou.readerforselfoss.utils.glide
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.util.Base64
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import android.widget.ImageView import android.widget.ImageView
import apps.amine.bou.readerforselfoss.utils.Config
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.BitmapImageViewTarget import com.bumptech.glide.request.target.BitmapImageViewTarget
fun Context.bitmapCenterCrop(config: Config, url: String, iv: ImageView) = fun Context.bitmapCenterCrop(url: String, iv: ImageView) =
Glide.with(this) Glide.with(this)
.asBitmap() .asBitmap()
.loadMaybeBasicAuth(config, url) .load(url)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(iv) .into(iv)
fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) = fun Context.bitmapFitCenter(url: String, iv: ImageView) =
Glide.with(this) Glide.with(this)
.asBitmap() .asBitmap()
.loadMaybeBasicAuth(config, url) .load(url)
.apply(RequestOptions.fitCenterTransform())
.into(iv)
fun Context.circularBitmapDrawable(url: String, iv: ImageView) =
Glide.with(this)
.asBitmap()
.load(url)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(object : BitmapImageViewTarget(iv) { .into(object : BitmapImageViewTarget(iv) {
override fun setResource(resource: Bitmap?) { override fun setResource(resource: Bitmap?) {
@ -36,24 +36,4 @@ fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) =
circularBitmapDrawable.isCircular = true circularBitmapDrawable.isCircular = true
iv.setImageDrawable(circularBitmapDrawable) iv.setImageDrawable(circularBitmapDrawable)
} }
}) })
fun RequestBuilder<Bitmap>.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Bitmap> {
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
builder.addHeader("Authorization", basicAuth)
}
val glideUrl = GlideUrl(url, builder.build())
return this.load(glideUrl)
}
fun RequestManager.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Drawable> {
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
builder.addHeader("Authorization", basicAuth)
}
val glideUrl = GlideUrl(url, builder.build())
return this.load(glideUrl)
}

View File

@ -1,45 +0,0 @@
package apps.amine.bou.readerforselfoss.utils.network
import android.content.Context
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.NetworkInfo
import android.view.View
import android.widget.TextView
import apps.amine.bou.readerforselfoss.R
import com.google.android.material.snackbar.Snackbar
var snackBarShown = false
var view: View? = null
lateinit var s: Snackbar
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean {
val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
val networkIsAccessible = activeNetwork != null && activeNetwork.isConnectedOrConnecting
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
}

View File

@ -1,73 +0,0 @@
package apps.amine.bou.readerforselfoss.utils.persistence
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
import apps.amine.bou.readerforselfoss.api.selfoss.Source
import apps.amine.bou.readerforselfoss.api.selfoss.Tag
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
fun TagEntity.toView(): Tag =
Tag(
this.tag,
this.color,
this.unread
)
fun SourceEntity.toView(): Source =
Source(
this.id,
this.title,
SelfossTagType(this.tags),
this.spout,
this.error,
this.icon
)
fun Source.toEntity(): SourceEntity =
SourceEntity(
this.id,
this.title,
this.tags.tags,
this.spout,
this.error,
this.icon.orEmpty()
)
fun Tag.toEntity(): TagEntity =
TagEntity(
this.tag,
this.color,
this.unread
)
fun ItemEntity.toView(): Item =
Item(
this.id,
this.datetime,
this.title,
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.sourcetitle,
SelfossTagType(this.tags)
)
fun Item.toEntity(): ItemEntity =
ItemEntity(
this.id,
this.datetime,
this.title,
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.sourcetitle,
this.tags.tags
)

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

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