├── .github
├── issue_template.md
└── workflows
│ └── android.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── email
│ │ └── schaal
│ │ └── ocreader
│ │ ├── APIDispatcher.kt
│ │ ├── DatabaseTest.kt
│ │ ├── ItemPagerActivityTest.kt
│ │ └── TestGenerator.kt
│ ├── debug
│ └── res
│ │ ├── values
│ │ └── strings.xml
│ │ └── xml-v25
│ │ └── shortcuts.xml
│ ├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ └── font
│ │ │ └── crimsontextregular.ttf
│ ├── ic_launcher-web.png
│ ├── java
│ │ └── email
│ │ │ └── schaal
│ │ │ └── ocreader
│ │ │ ├── ItemPageFragment.kt
│ │ │ ├── ItemPagerActivity.kt
│ │ │ ├── ListActivity.kt
│ │ │ ├── LoginFlowActivity.kt
│ │ │ ├── ManageFeedsActivity.kt
│ │ │ ├── OCReaderApplication.kt
│ │ │ ├── Preferences.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ ├── SettingsFragment.kt
│ │ │ ├── api
│ │ │ ├── API.kt
│ │ │ ├── APIv12Interface.kt
│ │ │ ├── CommonAPI.kt
│ │ │ ├── Level.kt
│ │ │ ├── OCSAPI.kt
│ │ │ └── json
│ │ │ │ ├── APILevels.kt
│ │ │ │ ├── Collections.kt
│ │ │ │ ├── FeedJsonTypeAdapter.kt
│ │ │ │ ├── ItemJsonTypeAdapter.kt
│ │ │ │ ├── NewsError.kt
│ │ │ │ ├── Status.kt
│ │ │ │ ├── TypeAdapters.kt
│ │ │ │ ├── UserJsonTypeAdapter.kt
│ │ │ │ └── v2
│ │ │ │ └── SyncResponse.kt
│ │ │ ├── database
│ │ │ ├── FeedViewModel.kt
│ │ │ ├── FolderViewModel.kt
│ │ │ ├── LiveRealmObject.kt
│ │ │ ├── LiveRealmResults.kt
│ │ │ ├── PagerViewModel.kt
│ │ │ ├── Queries.kt
│ │ │ ├── RealmViewModel.kt
│ │ │ └── model
│ │ │ │ ├── AllUnreadFolder.kt
│ │ │ │ ├── Feed.kt
│ │ │ │ ├── Folder.kt
│ │ │ │ ├── FreshFolder.kt
│ │ │ │ ├── Insertable.kt
│ │ │ │ ├── Item.kt
│ │ │ │ ├── SpecialFolder.kt
│ │ │ │ ├── StarredFolder.kt
│ │ │ │ ├── TemporaryFeed.kt
│ │ │ │ ├── TreeItem.kt
│ │ │ │ └── User.kt
│ │ │ ├── http
│ │ │ └── HttpManager.kt
│ │ │ ├── service
│ │ │ ├── SyncType.kt
│ │ │ └── SyncWorker.kt
│ │ │ ├── ui
│ │ │ └── loginflow
│ │ │ │ ├── LoginFlowFragment.kt
│ │ │ │ └── LoginFlowWebViewFragment.kt
│ │ │ ├── util
│ │ │ ├── ColorGenerator.kt
│ │ │ ├── FaviconLoader.kt
│ │ │ ├── FeedColors.kt
│ │ │ ├── LoginError.kt
│ │ │ ├── MyAppGlideModule.kt
│ │ │ ├── StringUtils.kt
│ │ │ └── TextDrawable.kt
│ │ │ └── view
│ │ │ ├── AddNewFeedDialogFragment.kt
│ │ │ ├── ArticleWebView.kt
│ │ │ ├── DividerItemDecoration.kt
│ │ │ ├── FeedManageListener.kt
│ │ │ ├── FeedsAdapter.kt
│ │ │ ├── FolderBottomSheetDialogFragment.kt
│ │ │ ├── FolderSpinnerAdapter.kt
│ │ │ ├── ItemViewHolder.kt
│ │ │ ├── LiveItemsAdapter.kt
│ │ │ ├── LoginViewModel.kt
│ │ │ ├── NestedScrollWebView.kt
│ │ │ ├── ProgressFloatingActionButton.kt
│ │ │ ├── ScrollAwareFABBehavior.kt
│ │ │ ├── TreeItemsAdapter.kt
│ │ │ └── UserBottomSheetDialogFragment.kt
│ └── res
│ │ ├── drawable
│ │ ├── account_circle.xml
│ │ ├── favicon_background.xml
│ │ ├── fresh.xml
│ │ ├── ic_add.xml
│ │ ├── ic_app_shortcut_feed.xml
│ │ ├── ic_app_shortcut_fresh.xml
│ │ ├── ic_app_shortcut_star.xml
│ │ ├── ic_check_box.xml
│ │ ├── ic_check_box_outline_blank.xml
│ │ ├── ic_delete.xml
│ │ ├── ic_done_above.xml
│ │ ├── ic_done_all.xml
│ │ ├── ic_feed_icon.xml
│ │ ├── ic_folder.xml
│ │ ├── ic_info.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_open_in_browser.xml
│ │ ├── ic_play_arrow.xml
│ │ ├── ic_play_circle_outline.xml
│ │ ├── ic_refresh.xml
│ │ ├── ic_settings.xml
│ │ ├── ic_share.xml
│ │ ├── ic_star.xml
│ │ ├── ic_star_outline.xml
│ │ ├── ic_warning.xml
│ │ └── selectable_background.xml
│ │ ├── font
│ │ ├── crimson.xml
│ │ └── crimsontextregular.ttf
│ │ ├── layout
│ │ ├── activity_item_pager.xml
│ │ ├── activity_list.xml
│ │ ├── activity_manage_feeds.xml
│ │ ├── activity_settings.xml
│ │ ├── bottom_navigationview.xml
│ │ ├── dialog_folders.xml
│ │ ├── fontpreference.xml
│ │ ├── fragment_add_new_feed.xml
│ │ ├── fragment_item_pager.xml
│ │ ├── fragment_login_flow_web_view.xml
│ │ ├── list_divider.xml
│ │ ├── list_empty.xml
│ │ ├── list_error.xml
│ │ ├── list_feed.xml
│ │ ├── list_folder.xml
│ │ ├── list_item.xml
│ │ ├── list_loadmore.xml
│ │ ├── login_flow_activity.xml
│ │ ├── login_flow_fragment.xml
│ │ ├── spinner_folder.xml
│ │ ├── spinner_folder_dropdown.xml
│ │ ├── toolbar.xml
│ │ ├── toolbar_pager.xml
│ │ └── user_bottomsheet.xml
│ │ ├── menu
│ │ ├── menu_item_bottomsheet.xml
│ │ ├── menu_item_list_action.xml
│ │ ├── menu_item_list_bottom.xml
│ │ ├── menu_item_list_top.xml
│ │ └── menu_item_pager_bottom.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values-de
│ │ └── strings.xml
│ │ ├── values-fr
│ │ └── strings.xml
│ │ ├── values-it
│ │ └── strings.xml
│ │ ├── values-night
│ │ └── colors.xml
│ │ ├── values-ru
│ │ └── strings.xml
│ │ ├── values
│ │ ├── arrays.xml
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── html.xml
│ │ ├── ids.xml
│ │ ├── md_colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ ├── xml-v25
│ │ └── shortcuts.xml
│ │ └── xml
│ │ ├── network_security_config.xml
│ │ └── preferences.xml
│ └── test
│ ├── java
│ └── email
│ │ └── schaal
│ │ └── ocreader
│ │ ├── APITest.kt
│ │ ├── FaviconLoaderTest.kt
│ │ ├── JsonTest.kt
│ │ ├── StringUtilsTest.kt
│ │ └── TestApplication.kt
│ └── resources
│ └── email
│ └── schaal
│ └── ocreader
│ └── robolectric.properties
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── icons
├── app_shortcut.svg
├── create_launcher_icon_from_svg.sh
├── feature_graphic.svg
├── ic_done_above.svg
└── ic_launcher_main.svg
├── metadata
└── en-US
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── drawer.png
│ │ ├── itempager-dark.png
│ │ ├── itempager.png
│ │ └── list.png
│ ├── short_description.txt
│ ├── title.txt
│ └── video.txt
└── settings.gradle.kts
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | ## Description of the issue
2 |
3 |
4 | ## Expected behavior
5 |
6 |
7 | ## Steps to reproduce
8 |
9 | 1.
10 | 2.
11 | 3.
12 |
13 | ## Your Environment
14 |
15 | * OCReader version:
16 | * Nextcloud news version:
17 | * Android version:
18 |
19 | ### Logcat
20 |
21 |
22 | ```
23 | Please paste the relevant logcat output here
24 | ```
25 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: set up JDK 1.8
17 | uses: actions/setup-java@v1
18 | with:
19 | java-version: 1.8
20 | - name: Cache Gradle packages
21 | uses: actions/cache@v1
22 | with:
23 | path: ~/.gradle/caches
24 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
25 | restore-keys: ${{ runner.os }}-gradle
26 | - name: Test and build with Gradle
27 | run: ./gradlew test assembleRelease
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | *.apk
9 | /app/release/
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/schaal/ocreader/actions?query=workflow%3A%22Android+CI%22)
2 |
3 | # [OCReader][1] - Android client for [Nextcloud News][0]
4 |
5 | OCReader is a client for a [Nextcloud News][0] instance (needs at least version 8.8.2).
6 |
7 | [
](https://f-droid.org/app/email.schaal.ocreader)
10 |
11 | ## Screenshots
12 |
13 | 
14 | 
15 | 
16 |
17 | ### Dark theme
18 |
19 | 
20 |
21 | ## License
22 | OCReader is released under the [GNU General Public License version 3](https://www.gnu.org/licenses/gpl-3.0) (or later versions).
23 |
24 | [TextDrawable](https://github.com/amulyakhare/TextDrawable) licensed under the MIT License, Copyright (c) 2014 Amulya Khare
25 |
26 | ## Contact
27 | Maintainer: [Daniel Schaal](https://github.com/schaal) <>
28 |
29 | [0]: https://github.com/nextcloud/news
30 | [1]: https://github.com/schaal/ocreader
31 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # For more details, see
2 | # http://developer.android.com/guide/developing/tools/proguard.html
3 |
4 | -dontobfuscate
5 |
6 | # AboutLibraries
7 | #-keep class .R
8 | #-keep class **.R$* {
9 | # ;
10 | #}
--------------------------------------------------------------------------------
/app/src/androidTest/java/email/schaal/ocreader/APIDispatcher.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Daniel Schaal
3 | *
4 | * This file is part of OCReader.
5 | *
6 | * OCReader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * OCReader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with OCReader. If not, see .
18 | *
19 | */
20 | package email.schaal.ocreader
21 |
22 | import okhttp3.mockwebserver.Dispatcher
23 | import okhttp3.mockwebserver.MockResponse
24 | import okhttp3.mockwebserver.RecordedRequest
25 |
26 | /**
27 | * Created by daniel on 15.10.16.
28 | */
29 | internal class APIDispatcher : Dispatcher() {
30 | var version = "8.8.2"
31 | @Throws(InterruptedException::class)
32 | override fun dispatch(request: RecordedRequest): MockResponse {
33 | val PATH_PREFIX = "/index.php/apps/news/api"
34 | if (request.path == PATH_PREFIX) return MockResponse().setBody("{ \"apiLevels\": [\"v1-2\"]}") else if (request.path == "$PATH_PREFIX/v1-2/status") return MockResponse().setBody(String.format("{ \"version\": \"%s\", \"warnings\": { \"improperlyConfiguredCron\": false }}", version))
35 | return MockResponse().setResponseCode(404)
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/email/schaal/ocreader/DatabaseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015-2016 Daniel Schaal
3 | *
4 | * This file is part of OCReader.
5 | *
6 | * OCReader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * OCReader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with OCReader. If not, see .
18 | *
19 | */
20 | package email.schaal.ocreader
21 |
22 | import androidx.test.ext.junit.rules.ActivityScenarioRule
23 | import androidx.test.ext.junit.runners.AndroidJUnit4
24 | import email.schaal.ocreader.database.Queries
25 | import email.schaal.ocreader.database.model.Feed
26 | import email.schaal.ocreader.database.model.Folder
27 | import email.schaal.ocreader.database.model.Item
28 | import io.realm.Realm
29 | import io.realm.kotlin.where
30 | import org.junit.*
31 | import org.junit.runner.RunWith
32 |
33 | /**
34 | * [Testing Fundamentals](http://d.android.com/tools/testing/testing_android.html)
35 | */
36 | @RunWith(AndroidJUnit4::class)
37 | class DatabaseTest {
38 | @get:Rule
39 | var activityTestRule = ActivityScenarioRule(ListActivity::class.java)
40 |
41 | @Before
42 | fun setUp() {
43 | Queries.resetDatabase()
44 | }
45 |
46 | @After
47 | fun tearDown() {
48 | Queries.resetDatabase()
49 | }
50 |
51 | @Test
52 | fun testDatabaseSetup() {
53 | val realm = Realm.getDefaultInstance()
54 | realm.close()
55 | Assert.assertTrue(realm.isClosed)
56 | }
57 |
58 | @Test
59 | fun testFolderInsert() {
60 | Realm.getDefaultInstance().use { realm ->
61 | realm.beginTransaction()
62 | TestGenerator.testFolder.insert(realm)
63 | realm.commitTransaction()
64 | Folder.get(realm, 1).let { dbFolder ->
65 | Assert.assertNotNull(dbFolder)
66 | Assert.assertEquals(dbFolder?.name, TestGenerator.FOLDER_TITLE)
67 | }
68 | }
69 | }
70 |
71 | @Test
72 | fun testFeedInsert() {
73 | Realm.getDefaultInstance().use { realm ->
74 | realm.beginTransaction()
75 | TestGenerator.testFeed.insert(realm)
76 | realm.commitTransaction()
77 | Feed.get(realm, 1).let { feed ->
78 | Assert.assertNotNull(feed)
79 | Assert.assertEquals(feed?.name, TestGenerator.FEED_TITLE)
80 | }
81 | }
82 | }
83 |
84 | @Test
85 | fun testItemInsert() {
86 | Realm.getDefaultInstance().use { realm ->
87 | realm.beginTransaction()
88 | TestGenerator.testFeed.insert(realm)
89 | TestGenerator.testItem.insert(realm)
90 | realm.commitTransaction()
91 | realm.where- ().findFirst().let { item ->
92 | Assert.assertEquals(item?.id, 1)
93 | Assert.assertEquals(item?.title, TestGenerator.ITEM_TITLE)
94 | Assert.assertEquals(item?.body, TestGenerator.BODY)
95 | Assert.assertEquals(item?.author, TestGenerator.AUTHOR)
96 | Assert.assertNull(item?.enclosureLink)
97 | }
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/email/schaal/ocreader/ItemPagerActivityTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 | package email.schaal.ocreader
20 |
21 | import android.content.Intent
22 | import androidx.test.espresso.Espresso
23 | import androidx.test.espresso.action.ViewActions
24 | import androidx.test.espresso.matcher.ViewMatchers
25 | import androidx.test.ext.junit.runners.AndroidJUnit4
26 | import androidx.test.filters.LargeTest
27 | import androidx.test.rule.ActivityTestRule
28 | import email.schaal.ocreader.ItemPagerActivity
29 | import email.schaal.ocreader.database.model.Item
30 | import org.junit.Rule
31 | import org.junit.Test
32 | import org.junit.runner.RunWith
33 | import java.util.*
34 |
35 | /**
36 | * Created by daniel on 16.04.17.
37 | */
38 | @RunWith(AndroidJUnit4::class)
39 | @LargeTest
40 | class ItemPagerActivityTest {
41 | @Rule
42 | val activityTestRule = ActivityTestRule(ItemPagerActivity::class.java, true, false)
43 |
44 | @Test
45 | @Throws(Exception::class)
46 | fun testItemPagerActivity() {
47 | val intent = Intent(Intent.ACTION_VIEW)
48 | val items = ArrayList
- ()
49 | items.add(TestGenerator.getTestItem(1))
50 | items.add(TestGenerator.getTestItem(2))
51 | intent.putExtra("ARG_ITEMS", items)
52 | activityTestRule.launchActivity(intent)
53 | Espresso.onView(ViewMatchers.withId(R.id.container)).perform(ViewActions.swipeLeft())
54 | Espresso.onView(ViewMatchers.withId(R.id.container)).perform(ViewActions.swipeRight())
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/email/schaal/ocreader/TestGenerator.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader
2 |
3 | import email.schaal.ocreader.database.model.Feed
4 | import email.schaal.ocreader.database.model.Folder
5 | import email.schaal.ocreader.database.model.Item
6 | import java.util.*
7 |
8 | /**
9 | * Created by daniel on 14.10.16.
10 | */
11 | internal object TestGenerator {
12 | const val FOLDER_TITLE = "TestFolderTitle"
13 | const val FEED_TITLE = "TestFeedTitle"
14 | const val ITEM_TITLE = "TestItemTitle"
15 | const val BODY = "
TestBody
"
16 | const val AUTHOR = "TestAuthor"
17 |
18 | val testFolder: Folder
19 | get() {
20 | return Folder().apply {
21 | id = 1L
22 | name = FOLDER_TITLE
23 | }
24 | }
25 |
26 | val testFeed: Feed
27 | get() = getTestFeed(1)
28 |
29 | fun getTestFeed(id: Long): Feed {
30 | return Feed().apply {
31 | this.id = id
32 | folderId = 0L
33 | name = FEED_TITLE
34 | }
35 | }
36 |
37 | val testItem: Item
38 | get() = getTestItem(1)
39 |
40 | fun getTestItem(id: Long): Item {
41 | return Item().apply {
42 | this.id = id
43 | title = ITEM_TITLE
44 | body = BODY
45 | author = AUTHOR
46 | feedId = 1
47 | feed = testFeed
48 | lastModified = Date()
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | OCDebug
23 |
24 |
--------------------------------------------------------------------------------
/app/src/debug/res/xml-v25/shortcuts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
15 |
16 |
17 |
22 |
26 |
29 |
30 |
31 |
36 |
40 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/assets/font/crimsontextregular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schaal/ocreader/c0311a0ccbac01a930772a5d7d593b2367ae9ddc/app/src/main/assets/font/crimsontextregular.ttf
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schaal/ocreader/c0311a0ccbac01a930772a5d7d593b2367ae9ddc/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/OCReaderApplication.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 | package email.schaal.ocreader
20 |
21 | import android.app.Application
22 | import androidx.appcompat.app.AppCompatDelegate
23 | import androidx.preference.PreferenceManager
24 | import email.schaal.ocreader.database.Queries
25 | import email.schaal.ocreader.database.model.Item
26 |
27 | /**
28 | * Application base class to setup the singletons
29 | */
30 | class OCReaderApplication : Application() {
31 | override fun onCreate() {
32 | super.onCreate()
33 |
34 | val preferences = PreferenceManager.getDefaultSharedPreferences(this)
35 |
36 | preferences.edit().apply {
37 | // Migrate to apptoken
38 | if (preferences.contains(Preferences.PASSWORD.key)) {
39 | remove(Preferences.USERNAME.key)
40 | remove(Preferences.PASSWORD.key)
41 | remove(Preferences.URL.key)
42 | }
43 |
44 | // Migrate updatedAt to lastModified
45 | if (Preferences.SORT_FIELD.getString(preferences) == "updatedAt")
46 | putString(Preferences.SORT_FIELD.key, Item::lastModified.name)
47 |
48 | // Directly observer WorkManager
49 | remove("sync_running")
50 | }.apply()
51 |
52 | AppCompatDelegate.setDefaultNightMode(Preferences.getNightMode(preferences))
53 |
54 | Queries.init(this)
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/SettingsFragment.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader
2 |
3 | import android.os.Bundle
4 | import androidx.preference.Preference
5 | import androidx.preference.PreferenceFragmentCompat
6 | import androidx.preference.PreferenceManager
7 | import com.bumptech.glide.Glide
8 | import email.schaal.ocreader.database.Queries
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.runBlocking
11 |
12 | /**
13 | * Preference Fragment
14 | */
15 | class SettingsFragment : PreferenceFragmentCompat() {
16 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
17 | setPreferencesFromResource(R.xml.preferences, rootKey)
18 | findPreference("reset_database")?.apply {
19 | setOnPreferenceClickListener {
20 | Queries.resetDatabase()
21 | Glide.get(context).apply {
22 | runBlocking(Dispatchers.IO) {
23 | clearDiskCache()
24 | }
25 | clearMemory()
26 | }
27 | PreferenceManager.getDefaultSharedPreferences(requireContext())
28 | .edit()
29 | .putBoolean(Preferences.SYS_NEEDS_UPDATE_AFTER_SYNC.key, true)
30 | .apply()
31 | it.title = getString(R.string.database_was_reset)
32 | it.isEnabled = false
33 | true
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/APIv12Interface.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.api
2 |
3 | import email.schaal.ocreader.api.json.*
4 | import retrofit2.Response
5 | import retrofit2.http.*
6 |
7 | interface APIv12Interface {
8 | /* SERVER **/
9 | @GET("status")
10 | suspend fun status(): Status
11 |
12 | /** FOLDERS */
13 | @GET("folders")
14 | suspend fun folders(): Folders
15 |
16 | /** FEEDS */
17 | @GET("feeds")
18 | suspend fun feeds(): Feeds
19 |
20 | @POST("feeds")
21 | suspend fun createFeed(@Body feedMap: Map): Feeds
22 |
23 | @PUT("feeds/{feedId}/move")
24 | suspend fun moveFeed(@Path("feedId") feedId: Long, @Body folderIdMap: Map): Response
25 |
26 | @DELETE("feeds/{feedId}")
27 | suspend fun deleteFeed(@Path("feedId") feedId: Long): Response
28 |
29 | /** ITEMS */
30 | @GET("items")
31 | suspend fun items(
32 | @Query("batchSize") batchSize: Long,
33 | @Query("offset") offset: Long,
34 | @Query("type") type: Int,
35 | @Query("id") id: Long,
36 | @Query("getRead") getRead: Boolean,
37 | @Query("oldestFirst") oldestFirst: Boolean
38 | ): Items
39 |
40 | @GET("items/updated")
41 | suspend fun updatedItems(
42 | @Query("lastModified") lastModified: Long,
43 | @Query("type") type: Int,
44 | @Query("id") id: Long
45 | ): Items
46 |
47 | @PUT("items/read/multiple")
48 | suspend fun markItemsRead(@Body items: ItemIds): Response
49 |
50 | @PUT("items/unread/multiple")
51 | suspend fun markItemsUnread(@Body items: ItemIds): Response
52 |
53 | @PUT("items/star/multiple")
54 | suspend fun markItemsStarred(@Body itemMap: ItemMap): Response
55 |
56 | @PUT("items/unstar/multiple")
57 | suspend fun markItemsUnstarred(@Body itemMap: ItemMap): Response
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/CommonAPI.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.api
2 |
3 | import email.schaal.ocreader.api.json.APILevels
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 |
7 | interface CommonAPI {
8 | @GET(API.API_ROOT)
9 | suspend fun apiLevels(): Response
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/Level.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 | package email.schaal.ocreader.api
20 |
21 | import android.content.Context
22 | import email.schaal.ocreader.R
23 | import email.schaal.ocreader.http.HttpManager
24 |
25 | /**
26 | * Created by daniel on 26.05.17.
27 | */
28 | enum class Level(val level: String, val isSupported: Boolean) {
29 | V2("v2", false),
30 | V12("v1-2", true);
31 |
32 | companion object {
33 | fun getAPI(context: Context, level: Level, httpManager: HttpManager): API {
34 | return when (level) {
35 | V12 -> API(context, httpManager)
36 | V2 -> throw IllegalStateException(context.getString(R.string.error_not_compatible))
37 | }
38 | }
39 |
40 | operator fun get(level: String): Level? {
41 | for (supportedLevel in values()) {
42 | if (supportedLevel.level == level) return supportedLevel
43 | }
44 | return null
45 | }
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/OCSAPI.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.api
2 |
3 | import email.schaal.ocreader.database.model.User
4 | import retrofit2.http.GET
5 | import retrofit2.http.Headers
6 | import retrofit2.http.Path
7 |
8 | interface OCSAPI {
9 | @Headers("OCS-APIRequest: true")
10 | @GET("ocs/v1.php/cloud/users/{userId}?format=json")
11 | suspend fun user(@Path("userId") userId: String): User
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/APILevels.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.api.json
2 |
3 | import com.squareup.moshi.JsonClass
4 | import email.schaal.ocreader.api.Level
5 |
6 | /**
7 | * API response containing supported API levels
8 | */
9 | @JsonClass(generateAdapter = true)
10 | data class APILevels(
11 | val apiLevels: List = emptyList()
12 | ) {
13 | fun highestSupportedApi(): Level? {
14 | for (level in Level.values())
15 | if (level.isSupported && apiLevels.contains(level.level))
16 | return level
17 | return null
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/Collections.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.api.json
21 |
22 | import com.squareup.moshi.JsonClass
23 | import email.schaal.ocreader.database.model.Feed
24 | import email.schaal.ocreader.database.model.Folder
25 | import email.schaal.ocreader.database.model.Item
26 |
27 | /**
28 | * Class to deserialize the json response for feeds
29 | */
30 | @JsonClass(generateAdapter = true)
31 | class Feeds (
32 | val feeds: List,
33 | val starredCount: Int = 0,
34 | val newestItemId: Long? = null
35 | )
36 |
37 | /**
38 | * Class to deserialize the json response for folders
39 | */
40 | @JsonClass(generateAdapter = true)
41 | class Folders(val folders: List)
42 |
43 | /**
44 | * Class to deserialize the json response for items
45 | */
46 | @JsonClass(generateAdapter = true)
47 | class Items(val items: List- )
48 |
49 | /**
50 | * Aggregates item ids, used to mark multiple items as read
51 | */
52 | @JsonClass(generateAdapter = true)
53 | data class ItemIds(val items: List) {
54 | constructor(sourceItems: Iterable
- ) : this(sourceItems.map { it.id })
55 | }
56 |
57 | /**
58 | * Aggregates feedIds and guidHashes, used to mark multiple items as starred
59 | */
60 | @JsonClass(generateAdapter = true)
61 | data class ItemMap(val items: List) {
62 | @JsonClass(generateAdapter = true)
63 | data class MappedItem(val feedId: Long, val guidHash: String?)
64 |
65 | constructor(sourceItems: Iterable
- ) : this(sourceItems.map {
66 | MappedItem(it.feedId, it.guidHash)
67 | })
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/FeedJsonTypeAdapter.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.api.json
2 |
3 | import com.squareup.moshi.FromJson
4 | import com.squareup.moshi.JsonClass
5 | import com.squareup.moshi.ToJson
6 | import email.schaal.ocreader.database.model.Feed
7 | import email.schaal.ocreader.util.cleanString
8 | import java.util.*
9 |
10 | class FeedJsonTypeAdapter {
11 | @FromJson
12 | fun fromJson(jsonFeed: JsonFeed) : Feed {
13 | return Feed().apply {
14 | id = jsonFeed.id
15 | folderId = jsonFeed.folderId
16 | url = jsonFeed.url
17 | name = jsonFeed.title.cleanString()
18 | link = jsonFeed.link?.ifBlank { null }
19 | faviconLink = jsonFeed.faviconLink?.ifBlank { null }
20 | added = Date(jsonFeed.added * 1000)
21 | unreadCount = jsonFeed.unreadCount ?: 0
22 | ordering = jsonFeed.ordering
23 | pinned = jsonFeed.pinned
24 | updateErrorCount = jsonFeed.updateErrorCount ?: 0
25 | lastUpdateError = jsonFeed.lastUpdateError
26 | }
27 | }
28 |
29 | @ToJson
30 | fun toJson(feed: Feed) : Map {
31 | return mapOf(
32 | "id" to feed.id,
33 | "title" to feed.name
34 | )
35 | }
36 | }
37 |
38 | @JsonClass(generateAdapter = true)
39 | class JsonFeed(
40 | val id: Long,
41 | val folderId: Long?,
42 | val url: String,
43 | val title: String,
44 | val link: String?,
45 | val faviconLink: String?,
46 | val added: Long,
47 | val unreadCount: Int?,
48 | val ordering: Int,
49 | val pinned: Boolean,
50 | val updateErrorCount: Int?,
51 | val lastUpdateError: String?
52 | )
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/ItemJsonTypeAdapter.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.api.json
2 |
3 | import com.squareup.moshi.FromJson
4 | import com.squareup.moshi.JsonClass
5 | import com.squareup.moshi.ToJson
6 | import email.schaal.ocreader.database.model.Item
7 | import email.schaal.ocreader.util.cleanString
8 | import java.util.*
9 |
10 | class ItemJsonTypeAdapter {
11 | @FromJson
12 | fun fromJson(jsonItem: JsonItem) : Item {
13 | return Item().apply {
14 | id = jsonItem.id
15 | guid = jsonItem.guid
16 | guidHash = jsonItem.guidHash
17 | url = jsonItem.url
18 | title = jsonItem.title?.cleanString()
19 | author = jsonItem.author?.cleanString()
20 | pubDate = Date(jsonItem.pubDate * 1000)
21 | body = jsonItem.body
22 | enclosureLink = jsonItem.enclosureLink
23 | feedId = jsonItem.feedId
24 | unread = jsonItem.unread
25 | starred = jsonItem.starred
26 | lastModified = Date(jsonItem.lastModified * 1000)
27 | fingerprint = jsonItem.fingerprint
28 | contentHash = jsonItem.contentHash
29 | }
30 | }
31 |
32 | @ToJson
33 | fun toJson(item: Item) : Map {
34 | return mutableMapOf(
35 | "id" to item.id,
36 | "contentHash" to item.contentHash
37 | ). also {
38 | if(item.unreadChanged) it["isUnread"] = item.unread
39 | if(item.starredChanged) it["isStarred"] = item.starred
40 | }
41 | }
42 | }
43 |
44 | @JsonClass(generateAdapter = true)
45 | class JsonItem(
46 | val id: Long,
47 | val guid: String?,
48 | val guidHash: String?,
49 | val url: String?,
50 | val title: String?,
51 | val author: String?,
52 | val pubDate: Long,
53 | val body: String,
54 | val enclosureMime: String?,
55 | val enclosureLink: String?,
56 | val feedId: Long,
57 | val unread: Boolean,
58 | val starred: Boolean,
59 | val lastModified: Long,
60 | val fingerprint: String?,
61 | val contentHash: String?
62 | )
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/NewsError.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.api.json
2 |
3 | /**
4 | * Class to decode error json response
5 | */
6 | class NewsError {
7 | var message: String? = null
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/Status.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Daniel Schaal
3 | *
4 | * This file is part of OCReader.
5 | *
6 | * OCReader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * OCReader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with OCReader. If not, see .
18 | *
19 | */
20 | package email.schaal.ocreader.api.json
21 |
22 | import com.github.zafarkhaja.semver.Version
23 | import com.squareup.moshi.JsonClass
24 |
25 | /**
26 | * Encapsulates the JSON response for the status api call
27 | */
28 | @JsonClass(generateAdapter = true)
29 | class Status(val version: Version,
30 | val warnings: Map? = null) {
31 |
32 | val isImproperlyConfiguredCron: Boolean
33 | get() = warnings?.get("isImproperlyConfiguredCron") ?: false
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/TypeAdapters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.api.json
21 |
22 | import com.github.zafarkhaja.semver.Version
23 | import com.squareup.moshi.FromJson
24 | import com.squareup.moshi.ToJson
25 | import java.util.*
26 |
27 | class VersionTypeAdapter {
28 | @FromJson
29 | fun fromJson(versionString: String?): Version? {
30 | return Version.valueOf(versionString)
31 | }
32 |
33 | @ToJson
34 | fun toJson(version: Version): String {
35 | return version.toString()
36 | }
37 | }
38 |
39 | class DateTypeAdapter {
40 | @FromJson
41 | fun fromJson(timestamp: Long) : Date {
42 | return Date(timestamp * 1000)
43 | }
44 |
45 | @ToJson
46 | fun toJson(date: Date): Long {
47 | return date.time / 1000
48 | }
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/UserJsonTypeAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.api.json
21 |
22 | import com.squareup.moshi.FromJson
23 | import com.squareup.moshi.JsonClass
24 | import email.schaal.ocreader.database.model.User
25 |
26 | class UserJsonTypeAdapter {
27 | @FromJson
28 | fun fromJson(ocs: OCS): User {
29 | return User(ocs.ocs.data.id, ocs.ocs.data.displayname)
30 | }
31 | }
32 |
33 | @JsonClass(generateAdapter = true)
34 | class OCS (
35 | val ocs: JsonUserData
36 | )
37 |
38 | @JsonClass(generateAdapter = true)
39 | class JsonUserData (
40 | val data: JsonUser
41 | )
42 |
43 | @JsonClass(generateAdapter = true)
44 | class JsonUser (
45 | val id: String,
46 | val displayname: String
47 | )
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/api/json/v2/SyncResponse.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.api.json.v2
2 |
3 | import email.schaal.ocreader.database.model.Feed
4 | import email.schaal.ocreader.database.model.Folder
5 | import email.schaal.ocreader.database.model.Item
6 |
7 | /**
8 | * API response for sync call
9 | */
10 | class SyncResponse {
11 | var folders: List? = null
12 | var feeds: List? = null
13 | var items: List
- ? = null
14 |
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/FolderViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database
21 |
22 | import email.schaal.ocreader.database.model.Feed
23 | import email.schaal.ocreader.database.model.Folder
24 | import io.realm.kotlin.where
25 |
26 | class FolderViewModel: RealmViewModel() {
27 | val foldersLiveData = LiveRealmResults(Folder.getAll(realm, false))
28 | val feedsLiveData = LiveRealmResults(realm.where().sort(Feed::name.name).findAll())
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/LiveRealmResults.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Realm Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package email.schaal.ocreader.database
17 |
18 | import androidx.annotation.MainThread
19 | import androidx.lifecycle.MutableLiveData
20 | import io.realm.OrderedRealmCollectionChangeListener
21 | import io.realm.RealmModel
22 | import io.realm.RealmResults
23 |
24 | /**
25 | * This class represents a RealmResults wrapped inside a LiveData.
26 | *
27 | * Realm will always keep the RealmResults up-to-date whenever a change occurs on any thread,
28 | * and when that happens, the observer will be notified.
29 | *
30 | * The RealmResults will be observed until it is invalidated - meaning all local Realm instances on this thread are closed.
31 | *
32 | * @param the type of the RealmModel
33 | */
34 | class LiveRealmResults @MainThread constructor(results: RealmResults) : MutableLiveData
>() {
35 | private val results: RealmResults
36 | // The listener will notify the observers whenever a change occurs.
37 | // The results are modified in change. This could be expanded to also return the change set in a pair.
38 | private val listener = OrderedRealmCollectionChangeListener> { results, _ -> this@LiveRealmResults.value = results }
39 | // We should start observing and stop observing, depending on whether we have observers.
40 | /**
41 | * Starts observing the RealmResults, if it is still valid.
42 | */
43 | override fun onActive() {
44 | super.onActive()
45 | if (results.isValid) { // invalidated results can no longer be observed.
46 | results.addChangeListener(listener)
47 | value = results
48 | }
49 | }
50 |
51 | /**
52 | * Stops observing the RealmResults.
53 | */
54 | override fun onInactive() {
55 | super.onInactive()
56 | if (results.isValid) {
57 | results.removeChangeListener(listener)
58 | }
59 | }
60 |
61 | init {
62 | require(results.isValid) { "The provided RealmResults is no longer valid, the Realm instance it belongs to is closed. It can no longer be observed for changes." }
63 | this.results = results
64 | if (results.isLoaded) {
65 | // we should not notify observers when results aren't ready yet (async query).
66 | // however, synchronous query should be set explicitly.
67 | value = results
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/PagerViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database
21 |
22 | import email.schaal.ocreader.database.model.TemporaryFeed
23 |
24 | class PagerViewModel: RealmViewModel() {
25 | val pager = TemporaryFeed.getPagerTemporaryFeed(realm)
26 |
27 | fun updatePager() {
28 | TemporaryFeed.updatePagerTemporaryFeed(realm)
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/Queries.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015-2016 Daniel Schaal
3 | *
4 | * This file is part of OCReader.
5 | *
6 | * OCReader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * OCReader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with OCReader. If not, see .
18 | *
19 | */
20 | package email.schaal.ocreader.database
21 |
22 | import android.content.Context
23 | import android.util.Log
24 | import email.schaal.ocreader.database.model.*
25 | import io.realm.Realm
26 | import io.realm.RealmConfiguration
27 | import io.realm.exceptions.RealmPrimaryKeyConstraintException
28 | import io.realm.kotlin.createObject
29 | import io.realm.kotlin.where
30 |
31 | /**
32 | * Utility class containing some commonly used Queries for the Realm database.
33 | */
34 | object Queries {
35 | private val TAG = Queries::class.java.name
36 |
37 | const val SCHEMA_VERSION = 13L
38 |
39 | private val initialData = Realm.Transaction { realm: Realm ->
40 | realm.where().findAll().deleteAllFromRealm()
41 | realm.where- ().findAll().deleteAllFromRealm()
42 | realm.where().findAll().deleteAllFromRealm()
43 | realm.where().findAll().deleteAllFromRealm()
44 | try {
45 | realm.createObject(TemporaryFeed.LIST_ID)
46 | realm.createObject(TemporaryFeed.PAGER_ID)
47 | } catch (e: RealmPrimaryKeyConstraintException) {
48 | }
49 | }
50 |
51 | fun init(context: Context) {
52 | Realm.init(context)
53 | val realmConfiguration = RealmConfiguration.Builder()
54 | .schemaVersion(SCHEMA_VERSION)
55 | .deleteRealmIfMigrationNeeded()
56 | .initialData(initialData)
57 | .compactOnLaunch()
58 | .allowWritesOnUiThread(true)
59 | .build()
60 | Realm.setDefaultConfiguration(realmConfiguration)
61 |
62 | try {
63 | Realm.getDefaultInstance().use {
64 | if(it.isEmpty)
65 | it.executeTransaction(initialData)
66 | }
67 | } catch(e: Exception) {
68 | Log.e(TAG, "Failed to open realm db", e)
69 | }
70 | }
71 |
72 | fun resetDatabase() {
73 | Log.w(TAG, "Database will be reset")
74 | Realm.getDefaultInstance().use {
75 | it.executeTransaction(initialData)
76 | }
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/RealmViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database
21 |
22 | import android.util.Log
23 | import androidx.lifecycle.ViewModel
24 | import email.schaal.ocreader.database.model.Item
25 | import email.schaal.ocreader.database.model.Item.Companion.UNREAD
26 | import io.realm.Realm
27 | import io.realm.exceptions.RealmException
28 | import io.realm.kotlin.where
29 |
30 | open class RealmViewModel : ViewModel() {
31 | protected val realm: Realm by lazy { Realm.getDefaultInstance() }
32 |
33 | override fun onCleared() {
34 | Log.d(TAG, "onCleared called in ${this::class.simpleName}")
35 | realm.close()
36 | super.onCleared()
37 | }
38 |
39 | fun setItemUnread(unread: Boolean, vararg items: Item?) {
40 | realm.executeTransaction {
41 | try {
42 | for (item in items.filterNotNull()) { /* If the item has a fingerprint, mark all items with the same fingerprint
43 | as read
44 | */
45 | if (item.fingerprint == null) {
46 | item.unread = unread
47 | } else {
48 | val sameItems = it.where
- ()
49 | .equalTo(Item::fingerprint.name, item.fingerprint)
50 | .equalTo(UNREAD, !unread)
51 | .findAll()
52 | for (sameItem in sameItems) {
53 | sameItem.unread = unread
54 | }
55 | }
56 | }
57 | } catch (e: RealmException) {
58 | Log.e(TAG, "Failed to set item as unread", e)
59 | }
60 | }
61 | }
62 |
63 | fun setItemStarred(starred: Boolean, vararg items: Item?) {
64 | realm.executeTransaction {
65 | try {
66 | for (item in items.filterNotNull()) {
67 | item.starred = starred
68 | }
69 | } catch (e: RealmException) {
70 | Log.e(TAG, "Failed to set item as starred", e)
71 | }
72 | }
73 | }
74 |
75 | companion object {
76 | const val TAG = "RealmViewModel"
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/model/AllUnreadFolder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database.model
21 |
22 | import email.schaal.ocreader.R
23 | import io.realm.Realm
24 | import io.realm.Sort
25 | import io.realm.kotlin.where
26 |
27 | class AllUnreadFolder: SpecialFolder(R.string.unread_items) {
28 | companion object {
29 | const val ID: Long = -10
30 | }
31 |
32 | override fun treeItemId(): Long {
33 | return ID
34 | }
35 |
36 | override fun getIcon(): Int {
37 | return R.drawable.ic_feed_icon
38 | }
39 |
40 | override fun getCount(realm: Realm): Int {
41 | return realm.where().sum(Feed::unreadCount.name).toInt()
42 | }
43 |
44 | override fun getFeeds(realm: Realm, onlyUnread: Boolean): List {
45 | val query = realm.where()
46 | if(onlyUnread)
47 | query.greaterThan(Feed::unreadCount.name, 0)
48 | return query.sort(Feed::name.name, Sort.ASCENDING).findAll()
49 | }
50 |
51 | override fun getItems(realm: Realm, onlyUnread: Boolean): List
- {
52 | val query = realm.where
- ()
53 | if(onlyUnread)
54 | query.equalTo(Item.UNREAD, true)
55 | return query.distinct(Item::fingerprint.name).findAll()
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/model/FreshFolder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database.model
21 |
22 | import email.schaal.ocreader.R
23 | import io.realm.Realm
24 | import io.realm.kotlin.where
25 | import java.util.*
26 |
27 | class FreshFolder: SpecialFolder(R.string.fresh_items) {
28 | companion object {
29 | private const val MAX_ARTICLE_AGE = 24 * 60 * 60 * 1000
30 | const val ID: Long = -12
31 | }
32 |
33 | override fun treeItemId(): Long {
34 | return ID
35 | }
36 |
37 | override fun getIcon(): Int {
38 | return R.drawable.fresh
39 | }
40 |
41 | override fun getCount(realm: Realm): Int {
42 | return realm.where
- ()
43 | .equalTo(Item.UNREAD, true)
44 | .greaterThan(Item::pubDate.name, getDate())
45 | .count()
46 | .toInt()
47 | }
48 |
49 | override fun getFeeds(realm: Realm, onlyUnread: Boolean): List {
50 | val freshFeeds = mutableListOf()
51 | for(item: Item in getItems(realm, false)) {
52 | val feed = item.feed
53 | if(!freshFeeds.contains(feed) && feed != null)
54 | freshFeeds.add(feed)
55 | }
56 | return freshFeeds
57 | }
58 |
59 | override fun getItems(realm: Realm, onlyUnread: Boolean): List
- {
60 | return realm.where
- ()
61 | .equalTo(Item.UNREAD, true)
62 | .greaterThan(Item::pubDate.name, getDate())
63 | .findAll()
64 | }
65 |
66 | private fun getDate() : Date {
67 | return Date(System.currentTimeMillis() - MAX_ARTICLE_AGE)
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/model/Insertable.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database.model
21 |
22 | import io.realm.Realm
23 |
24 | interface Insertable {
25 | fun insert(realm: Realm)
26 | fun delete(realm: Realm)
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/model/SpecialFolder.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.database.model
2 |
3 | import android.content.Context
4 | import androidx.annotation.StringRes
5 |
6 | abstract class SpecialFolder(@StringRes private val name_res: Int): TreeItem {
7 | override fun treeItemName(context: Context): String {
8 | return context.getString(name_res)
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/model/StarredFolder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database.model
21 |
22 | import email.schaal.ocreader.R
23 | import io.realm.Realm
24 | import io.realm.Sort
25 | import io.realm.kotlin.where
26 |
27 | class StarredFolder: SpecialFolder(R.string.starred_items) {
28 | companion object {
29 | const val ID: Long = -11
30 | }
31 |
32 | override fun treeItemId(): Long {
33 | return ID
34 | }
35 |
36 | override fun getIcon(): Int {
37 | return R.drawable.ic_star_outline
38 | }
39 |
40 | override fun getCount(realm: Realm): Int {
41 | return realm.where
- ().equalTo(Item.STARRED, true).count().toInt()
42 | }
43 |
44 | override fun getFeeds(realm: Realm, onlyUnread: Boolean): List {
45 | return realm.where().greaterThan(Feed::starredCount.name, 0).sort(Feed::name.name, Sort.ASCENDING).findAll()
46 | }
47 |
48 | override fun getItems(realm: Realm, onlyUnread: Boolean): List
- {
49 | return realm.where
- ().equalTo(Item.STARRED, true).findAll()
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/model/TemporaryFeed.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Daniel Schaal
3 | *
4 | * This file is part of OCReader.
5 | *
6 | * OCReader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * OCReader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with OCReader. If not, see .
18 | *
19 | */
20 | package email.schaal.ocreader.database.model
21 |
22 | import io.realm.Realm
23 | import io.realm.RealmList
24 | import io.realm.RealmModel
25 | import io.realm.annotations.PrimaryKey
26 | import io.realm.annotations.RealmClass
27 | import io.realm.kotlin.where
28 |
29 | /**
30 | * TemporaryFeed allows to store the currently displayed Items.
31 | */
32 | @RealmClass
33 | open class TemporaryFeed() : RealmModel {
34 | @PrimaryKey
35 | var id: Long = 0
36 | var treeItemId: Long? = null
37 | var name: String = ""
38 | var items: RealmList
- ? = null
39 |
40 | constructor(id: Long) : this() {
41 | this.id = id
42 | }
43 |
44 | companion object {
45 | const val LIST_ID = 0
46 | const val PAGER_ID = 1
47 |
48 | fun getListTemporaryFeed(realm: Realm): TemporaryFeed? {
49 | return realm.where().equalTo(TemporaryFeed::id.name, LIST_ID).findFirst()
50 | }
51 |
52 | fun getPagerTemporaryFeed(realm: Realm): TemporaryFeed? {
53 | return realm.where().equalTo(TemporaryFeed::id.name, PAGER_ID).findFirst()
54 | }
55 |
56 | fun updatePagerTemporaryFeed(realm: Realm) {
57 | realm.executeTransaction { realm1: Realm ->
58 | val listTempFeed = getListTemporaryFeed(realm1)
59 | val pagerTempFeed = getPagerTemporaryFeed(realm1)
60 | for (item in realm1.where
- ().equalTo(Item::active.name, true).findAll()) {
61 | item.active = false
62 | }
63 | val listTempFeedItems = listTempFeed?.items
64 | if (listTempFeedItems != null) {
65 | for (item in listTempFeedItems) {
66 | item.active = true
67 | }
68 | }
69 | pagerTempFeed?.items = listTempFeedItems
70 | pagerTempFeed?.name = listTempFeed?.name ?: ""
71 | pagerTempFeed?.treeItemId = listTempFeed?.treeItemId ?: 0
72 | }
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/model/TreeItem.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database.model
21 |
22 | import android.content.Context
23 | import androidx.annotation.DrawableRes
24 | import io.realm.Realm
25 |
26 | interface TreeItem {
27 | fun treeItemId() : Long?
28 | fun treeItemName(context: Context): String
29 | @DrawableRes fun getIcon(): Int
30 | fun getCount(realm: Realm): Int
31 | fun getFeeds(realm: Realm, onlyUnread: Boolean = false): List
32 | fun getItems(realm: Realm, onlyUnread: Boolean = false): List
-
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/database/model/User.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.database.model
21 |
22 | import io.realm.Realm
23 | import io.realm.RealmModel
24 | import io.realm.RealmObject
25 | import io.realm.annotations.PrimaryKey
26 | import io.realm.annotations.RealmClass
27 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
28 |
29 | @RealmClass
30 | open class User(
31 | var userId: String = "",
32 | var displayName: String = "",
33 | ) : RealmModel, Insertable {
34 | @PrimaryKey var id = 0L
35 |
36 | override fun insert(realm: Realm) {
37 | realm.insertOrUpdate(this)
38 | }
39 |
40 | override fun delete(realm: Realm) {
41 | RealmObject.deleteFromRealm(this)
42 | }
43 |
44 | fun avatarUrl(baseUrl: String): String? {
45 | return baseUrl.toHttpUrlOrNull()?.resolve("avatar/${userId}/128")?.toString()
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/http/HttpManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015-2016 Daniel Schaal
3 | *
4 | * This file is part of OCReader.
5 | *
6 | * OCReader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * OCReader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with OCReader. If not, see .
18 | *
19 | */
20 | package email.schaal.ocreader.http
21 |
22 | import okhttp3.*
23 | import java.io.IOException
24 | import java.util.concurrent.TimeUnit
25 |
26 | /**
27 | * Utility class to setup the OkHttpClient and manage the credentials used to communicate with
28 | * the ownCloud instance.
29 | */
30 | class HttpManager(username: String, password: String, url: HttpUrl) {
31 | val client: OkHttpClient = OkHttpClient.Builder()
32 | .connectTimeout(20, TimeUnit.SECONDS)
33 | .readTimeout(3, TimeUnit.MINUTES)
34 | .addInterceptor(AuthorizationInterceptor())
35 | .build()
36 | val credentials: HostCredentials = HostCredentials(username, password, url)
37 |
38 | inner class HostCredentials constructor(username: String, password: String, url: HttpUrl) {
39 | val credentials: String = Credentials.basic(username, password)
40 | val rootUrl: HttpUrl = url
41 | }
42 |
43 | private inner class AuthorizationInterceptor : Interceptor {
44 | @Throws(IOException::class)
45 | override fun intercept(chain: Interceptor.Chain): Response {
46 | var request = chain.request()
47 | // only add Authorization header for urls on the configured owncloud host
48 | if (credentials.rootUrl.host == request.url.host) request = request.newBuilder()
49 | .addHeader("Authorization", credentials.credentials)
50 | .build()
51 | return chain.proceed(request)
52 | }
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/service/SyncType.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.service
2 |
3 | /**
4 | * Created by daniel on 02.04.17.
5 | */
6 | enum class SyncType(val action: String) {
7 | FULL_SYNC("email.schaal.ocreader.action.FULL_SYNC"),
8 | SYNC_CHANGES_ONLY("email.schaal.ocreader.action.SYNC_CHANGES_ONLY"),
9 | LOAD_MORE("email.schaal.ocreader.action.LOAD_MORE");
10 |
11 | companion object {
12 | operator fun get(action: String?): SyncType? = values().find { it.action == action }
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/service/SyncWorker.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.service
2 |
3 | import android.content.Context
4 | import androidx.lifecycle.LiveData
5 | import androidx.preference.PreferenceManager
6 | import androidx.work.*
7 | import email.schaal.ocreader.Preferences
8 | import email.schaal.ocreader.api.API
9 | import email.schaal.ocreader.util.LoginError
10 |
11 | class SyncWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
12 | companion object {
13 | const val KEY_SYNC_TYPE = "KEY_SYNC_TYPE"
14 | const val KEY_EXCEPTION = "KEY_EXCEPTION"
15 | const val WORK_ID = "WORK_ID_SYNC"
16 |
17 | fun sync(context: Context, syncType: SyncType): LiveData {
18 | val workManager = WorkManager.getInstance(context)
19 | val syncWork = OneTimeWorkRequestBuilder()
20 | .setInputData(workDataOf(KEY_SYNC_TYPE to syncType.action))
21 | .build()
22 | workManager.enqueueUniqueWork(WORK_ID, ExistingWorkPolicy.KEEP, syncWork)
23 | return workManager.getWorkInfoByIdLiveData(syncWork.id)
24 | }
25 |
26 | fun getLiveData(context: Context): LiveData
> {
27 | return WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(WORK_ID)
28 | }
29 | }
30 |
31 | override suspend fun doWork(): Result {
32 | val syncType: SyncType = SyncType[inputData.getString(KEY_SYNC_TYPE)] ?: SyncType.FULL_SYNC
33 |
34 | return try {
35 | API(applicationContext).sync(syncType)
36 | Result.success()
37 | } catch (e: Throwable) {
38 | e.printStackTrace()
39 | Result.failure(
40 | workDataOf(
41 | KEY_EXCEPTION to LoginError.getError(
42 | applicationContext, e).message
43 | )
44 | )
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/ui/loginflow/LoginFlowFragment.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.ui.loginflow
21 |
22 | import android.os.Bundle
23 | import android.view.LayoutInflater
24 | import android.view.View
25 | import android.view.ViewGroup
26 | import android.widget.TextView
27 | import androidx.fragment.app.Fragment
28 | import email.schaal.ocreader.R
29 | import email.schaal.ocreader.databinding.LoginFlowFragmentBinding
30 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
31 |
32 | class LoginFlowFragment : Fragment() {
33 | private lateinit var binding: LoginFlowFragmentBinding
34 |
35 | private var url: String? = null
36 |
37 | companion object {
38 | private const val ARG_URL = "URL"
39 |
40 | @JvmStatic
41 | fun newInstance(url: String? = null) =
42 | LoginFlowFragment().apply {
43 | url?.let {
44 | arguments = Bundle().apply {
45 | putString(ARG_URL, it)
46 | }
47 | }
48 | }
49 | }
50 |
51 | override fun onCreate(savedInstanceState: Bundle?) {
52 | super.onCreate(savedInstanceState)
53 | arguments?.let {
54 | url = it.getString(ARG_URL)
55 | }
56 | }
57 |
58 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
59 | savedInstanceState: Bundle?): View {
60 | binding = LoginFlowFragmentBinding.inflate(inflater, container, false)
61 | binding.inputUrl.setText(url, TextView.BufferType.EDITABLE)
62 | return binding.root
63 | }
64 |
65 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
66 | super.onViewCreated(view, savedInstanceState)
67 | binding.buttonLogin.setOnClickListener {
68 | // Check if inputUrl starts with a scheme (http or https)
69 | val urlString = binding.inputUrl.text?.let {
70 | if(!it.startsWith("http"))
71 | "https://${it}"
72 | else
73 | it
74 | }?.toString()
75 |
76 | val url = urlString
77 | ?.toHttpUrlOrNull()
78 | ?.newBuilder()
79 | ?.addPathSegments("index.php/login/flow")
80 |
81 | if(url != null) {
82 | parentFragmentManager.beginTransaction()
83 | .replace(R.id.container, LoginFlowWebViewFragment.newInstance(url.toString()))
84 | .commit()
85 | } else {
86 | binding.inputUrl.error = getString(R.string.error_incorrect_url)
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/util/ColorGenerator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2014 Amulya Khare
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 | package email.schaal.ocreader.util
25 |
26 | import androidx.annotation.ColorRes
27 | import email.schaal.ocreader.R
28 | import kotlin.math.abs
29 |
30 | /**
31 | * @author amulya
32 | */
33 | internal class ColorGenerator private constructor(private val mColors: List) {
34 | @ColorRes
35 | fun getColor(key: Any?): Int {
36 | return if (key != null) mColors[abs(key.hashCode()) % mColors.size] else mColors[0]
37 | }
38 |
39 | companion object {
40 | val MATERIAL = ColorGenerator(listOf(
41 | R.color.tdb_Red,
42 | R.color.tdb_Purple,
43 | R.color.tdb_Indigo,
44 | R.color.tdb_Blue,
45 | R.color.tdb_LightBlue,
46 | R.color.tdb_Cyan,
47 | R.color.tdb_Teal,
48 | R.color.tdb_Green,
49 | R.color.tdb_LightGreen,
50 | R.color.tdb_Lime,
51 | R.color.tdb_Amber,
52 | R.color.tdb_DeepOrange,
53 | R.color.tdb_Brown,
54 | R.color.tdb_BlueGrey
55 | ))
56 | }
57 |
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/util/FeedColors.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.util
2 |
3 | import androidx.annotation.ColorInt
4 | import androidx.palette.graphics.Palette
5 | import androidx.palette.graphics.Palette.Swatch
6 | import androidx.palette.graphics.Target
7 |
8 | /**
9 | * Generate text and background color for Feeds
10 | */
11 | class FeedColors(private val palette: Palette?) {
12 | enum class Type {
13 | TEXT, BACKGROUND
14 | }
15 |
16 | internal constructor(@ColorInt color: Int?) : this(
17 | color?.let { Swatch(it, 1) }
18 | ?.let { Palette.Builder(listOf(it)).addTarget(Target.MUTED).generate() })
19 |
20 | @ColorInt
21 | fun getColor(type: Type, @ColorInt defaultColor: Int): Int {
22 | return palette?.let {
23 | when (type) {
24 | Type.TEXT -> it.dominantSwatch
25 | Type.BACKGROUND -> it.mutedSwatch
26 | }
27 | }?.rgb ?: defaultColor
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/util/LoginError.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.util
2 |
3 | import android.content.Context
4 | import com.squareup.moshi.JsonClass
5 | import com.squareup.moshi.Moshi
6 | import email.schaal.ocreader.R
7 | import email.schaal.ocreader.api.API
8 | import retrofit2.HttpException
9 | import java.io.IOException
10 | import java.net.ConnectException
11 | import java.net.UnknownHostException
12 | import javax.net.ssl.SSLHandshakeException
13 |
14 | /**
15 | * Turn login errors into human-readable strings
16 | */
17 | class LoginError private constructor(val section: Section, val message: String, val throwable: Throwable? = null) {
18 | enum class Section {
19 | URL, USER, PASSWORD, NONE, UNKNOWN
20 | }
21 |
22 | @JsonClass(generateAdapter = true)
23 | internal class ErrorMessage(val message: String)
24 |
25 | constructor(message: String) : this(Section.NONE, message)
26 | private constructor(throwable: Throwable) : this(Section.UNKNOWN, throwable.message ?: "", throwable)
27 |
28 | companion object {
29 | private fun getHttpError(context: Context, code: Int, e: HttpException): LoginError {
30 | return when (code) {
31 | 401 -> LoginError(Section.USER, context.getString(R.string.error_access_forbidden))
32 | 403, 404 -> LoginError(Section.URL, context.getString(R.string.error_oc_not_found))
33 | 405 -> LoginError(Section.URL, context.getString(R.string.ncnews_too_old, API.MIN_VERSION.toString()))
34 | else -> {
35 | val message = e.response()?.errorBody()?.source()?.let { source ->
36 | Moshi.Builder().build().adapter(ErrorMessage::class.java).fromJson(source)?.message }
37 | LoginError("${context.getString(R.string.http_error, code)}${message.let { ": $it" }}")
38 | }
39 | }
40 | }
41 |
42 | fun getError(context: Context, t: Throwable): LoginError {
43 | return when (t) {
44 | is HttpException -> getHttpError(context, t.code(), t)
45 | is UnknownHostException -> LoginError(Section.URL, context.getString(R.string.error_unknown_host))
46 | is SSLHandshakeException -> LoginError(Section.URL, context.getString(R.string.untrusted_certificate))
47 | is ConnectException -> LoginError(Section.URL, context.getString(R.string.could_not_connect))
48 | is IOException -> return LoginError(Section.NONE, context.getString(R.string.ncnews_too_old, API.MIN_VERSION.toString()))
49 | else -> LoginError(t)
50 | }
51 | }
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/util/MyAppGlideModule.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.util
2 |
3 | import android.content.Context
4 | import com.bumptech.glide.GlideBuilder
5 | import com.bumptech.glide.annotation.GlideModule
6 | import com.bumptech.glide.module.AppGlideModule
7 | import com.bumptech.glide.request.RequestOptions
8 | import email.schaal.ocreader.R
9 |
10 | /**
11 | * Created by daniel on 7/15/17.
12 | */
13 | @GlideModule
14 | class MyAppGlideModule : AppGlideModule() {
15 | override fun applyOptions(context: Context, builder: GlideBuilder) {
16 | builder.setDefaultRequestOptions(RequestOptions.placeholderOf(R.drawable.ic_feed_icon))
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/util/StringUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Daniel Schaal
3 | *
4 | * This file is part of OCReader.
5 | *
6 | * OCReader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * OCReader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with OCReader. If not, see .
18 | *
19 | */
20 | @file:JvmName("StringUtils")
21 |
22 | package email.schaal.ocreader.util
23 |
24 | import android.content.Context
25 | import android.graphics.Color
26 | import android.text.format.DateUtils
27 | import androidx.core.text.HtmlCompat
28 | import email.schaal.ocreader.R
29 | import email.schaal.ocreader.database.model.Feed
30 | import okhttp3.HttpUrl
31 | import java.util.*
32 |
33 | fun String.htmlLink(href: String?) = if(href != null) "${this}" else this
34 |
35 | /**
36 | * Utility functions to handle Strings.
37 | */
38 | fun getByLine(context: Context, template: String, author: String?, feed: Feed?): String {
39 | return if (author != null && feed != null) {
40 | String.format(template, context.getString(R.string.by_author_on_feed, author, feed.name.htmlLink(feed.link)))
41 | } else if(author != null) {
42 | String.format(template, context.getString(R.string.by_author, author))
43 | } else if (feed != null) {
44 | String.format(template, context.getString(R.string.on_feed, feed.name.htmlLink(feed.link)))
45 | } else {
46 | ""
47 | }
48 | }
49 |
50 | fun Date.getTimeSpanString(endDate: Date = Date()): CharSequence {
51 | return DateUtils.getRelativeTimeSpanString(
52 | this.time,
53 | endDate.time,
54 | DateUtils.MINUTE_IN_MILLIS,
55 | DateUtils.FORMAT_ABBREV_ALL)
56 | }
57 |
58 | fun String.cleanString(): String {
59 | return HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
60 | }
61 |
62 | fun Int.asCssString(): String { // Use US locale so we always get a . as decimal separator for a valid css value
63 | return String.format(Locale.US, "rgba(%d,%d,%d,%.2f)",
64 | Color.red(this),
65 | Color.green(this),
66 | Color.blue(this),
67 | Color.alpha(this) / 255.0)
68 | }
69 |
70 | fun HttpUrl.buildBaseUrl(apiRoot: String): HttpUrl {
71 | return this
72 | .newBuilder()
73 | .addPathSegments(apiRoot)
74 | .build()
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/view/AddNewFeedDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.view
2 |
3 | import android.app.AlertDialog
4 | import android.app.Dialog
5 | import android.content.Context
6 | import android.os.Bundle
7 | import androidx.fragment.app.FragmentActivity
8 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
9 | import email.schaal.ocreader.ManageFeedsActivity
10 | import email.schaal.ocreader.R
11 | import email.schaal.ocreader.database.model.Feed
12 | import email.schaal.ocreader.databinding.FragmentAddNewFeedBinding
13 |
14 | /**
15 | * Display form to add new Feed
16 | */
17 | class AddNewFeedDialogFragment : BottomSheetDialogFragment() {
18 | private var listener: FeedManageListener? = null
19 | override fun onAttach(context: Context) {
20 | super.onAttach(context)
21 | listener = try {
22 | context as FeedManageListener
23 | } catch (e: ClassCastException) {
24 | throw ClassCastException("$context must implement FeedManageListener")
25 | }
26 | }
27 |
28 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
29 | val activity = requireActivity() as ManageFeedsActivity
30 |
31 | val arguments = requireArguments()
32 | val feedId = arguments.getLong(ARG_FEED_ID, -1)
33 | val newFeed = feedId < 0
34 |
35 | val binding = FragmentAddNewFeedBinding.inflate(activity.layoutInflater).apply {
36 | feedUrl.isEnabled = newFeed
37 | folder.adapter = activity.folderSpinnerAdapter
38 | feedUrl.setText(arguments.getString(ARG_URL))
39 | folder.setSelection(activity.folderSpinnerAdapter.getPosition(
40 | if (arguments.containsKey(ARG_FOLDER_ID)) arguments.getLong(ARG_FOLDER_ID) else null
41 | ))
42 | }
43 |
44 | return AlertDialog.Builder(activity).setTitle(if (newFeed) R.string.add_new_feed else R.string.edit_feed)
45 | .setPositiveButton(if (newFeed) R.string.add else R.string.save) { _, _ ->
46 | if (newFeed)
47 | listener?.addNewFeed(binding.feedUrl.text.toString(), binding.folder.selectedItemId, arguments.getBoolean(ARG_FINISH_AFTER_CLOSE, false))
48 | else
49 | listener?.changeFeed(feedId, binding.folder.selectedItemId)
50 | }
51 | .setView(binding.root)
52 | .create()
53 | }
54 |
55 | companion object {
56 | const val ARG_URL = "url"
57 | const val ARG_FOLDER_ID = "folder_id"
58 | const val ARG_FINISH_AFTER_CLOSE = "finish_after_close"
59 | private const val ARG_FEED_ID = "feed_id"
60 | /**
61 | * Show feed edit dialog
62 | * @param activity Activity to get the FragmentManager
63 | * @param feed feed to edit or add (id < 0 means add new feed, id >=0 means edit existing feed)
64 | * @param finishAfterClose should the activity be finished after operation was successful
65 | */
66 | fun show(activity: FragmentActivity, feed: Feed?, finishAfterClose: Boolean) {
67 | val dialogFragment = AddNewFeedDialogFragment()
68 |
69 | dialogFragment.arguments = Bundle().apply {
70 | putBoolean(ARG_FINISH_AFTER_CLOSE, finishAfterClose)
71 | feed?.let { feed ->
72 | putString(ARG_URL, feed.url)
73 | if (feed.id >= 0) {
74 | putLong(ARG_FEED_ID, feed.id)
75 | feed.folderId?.let { putLong(ARG_FOLDER_ID, it) }
76 | }
77 | }
78 | }
79 | dialogFragment.show(activity.supportFragmentManager, "newfeed")
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/view/DividerItemDecoration.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Daniel Schaal
3 | *
4 | * This file is part of OCReader.
5 | *
6 | * OCReader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * OCReader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with OCReader. If not, see .
18 | *
19 | */
20 | package email.schaal.ocreader.view
21 |
22 | import android.content.Context
23 | import android.graphics.Canvas
24 | import android.graphics.Rect
25 | import android.graphics.drawable.Drawable
26 | import android.view.View
27 | import androidx.annotation.DimenRes
28 | import androidx.core.content.res.use
29 | import androidx.recyclerview.widget.LinearLayoutManager
30 | import androidx.recyclerview.widget.RecyclerView
31 | import androidx.recyclerview.widget.RecyclerView.ItemDecoration
32 | import email.schaal.ocreader.R
33 |
34 | class DividerItemDecoration(context: Context, @DimenRes insetRes: Int) : ItemDecoration() {
35 | private val mDivider: Drawable? = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider)).use {
36 | it.getDrawable(0)
37 | }
38 | private val inset: Int = context.resources.getDimensionPixelSize(insetRes)
39 | private val size = mDivider?.intrinsicHeight ?: 0
40 | private val dividerRect = Rect(0, 0, 0, 0)
41 |
42 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
43 | state: RecyclerView.State) {
44 | super.getItemOffsets(outRect, view, parent, state)
45 | if (mDivider == null || parent.getChildLayoutPosition(view) < 1) {
46 | return
47 | }
48 | outRect.top = size
49 | }
50 |
51 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
52 | if (mDivider == null) return
53 |
54 | parent.layoutManager?.let { layoutManager ->
55 | val childCount = parent.childCount
56 | val paddingLeft = parent.paddingLeft
57 | val paddingLeftInset = paddingLeft + inset
58 | dividerRect.right = parent.width - parent.paddingRight
59 | loop@ for (i in 0 until childCount) {
60 | val child = parent.getChildAt(i)
61 | when (layoutManager.getItemViewType(child)) {
62 | R.id.viewtype_item -> if (i == childCount - 1 || i == childCount - 2 && layoutManager.getItemViewType(
63 | parent.getChildAt(i + 1)
64 | ) != R.id.viewtype_item
65 | ) dividerRect.left = paddingLeft else dividerRect.left = paddingLeftInset
66 | R.id.viewtype_empty, R.id.viewtype_loadmore -> dividerRect.left = paddingLeft
67 | R.id.viewtype_error -> continue@loop
68 | }
69 | dividerRect.top =
70 | child.bottom + (child.layoutParams as RecyclerView.LayoutParams).bottomMargin
71 | dividerRect.bottom = dividerRect.top + size
72 | mDivider.bounds = dividerRect
73 | mDivider.draw(c)
74 | }
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/view/FeedManageListener.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.view
2 |
3 | import email.schaal.ocreader.database.model.Feed
4 |
5 | /**
6 | * Callbacks for feed management
7 | */
8 | interface FeedManageListener {
9 | fun addNewFeed(url: String, folderId: Long, finishAfterAdd: Boolean)
10 | fun deleteFeed(feed: Feed)
11 | fun changeFeed(feedId: Long, folderId: Long)
12 | fun showFeedDialog(feed: Feed)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/view/FeedsAdapter.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.view
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import email.schaal.ocreader.R
8 | import email.schaal.ocreader.database.model.Feed
9 | import email.schaal.ocreader.databinding.ListFeedBinding
10 | import email.schaal.ocreader.util.FaviconLoader
11 | import email.schaal.ocreader.util.FaviconLoader.FeedColorsListener
12 | import email.schaal.ocreader.util.FeedColors
13 |
14 | /**
15 | * RecyclerView Adapter for Feeds
16 | */
17 | class FeedsAdapter(private val listener: FeedManageListener) : RecyclerView.Adapter() {
18 | var feeds: List = emptyList()
19 | set(value) {
20 | field = value
21 | notifyDataSetChanged()
22 | }
23 |
24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
25 | val binding = ListFeedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
26 | return FeedViewHolder(binding, listener)
27 | }
28 |
29 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
30 | if (holder is FeedViewHolder) {
31 | getItem(position)?.let { holder.bindFeed(it) }
32 | }
33 | }
34 |
35 | private fun getItem(position: Int): Feed? {
36 | return feeds[position]
37 | }
38 |
39 | override fun getItemId(index: Int): Long {
40 | return getItem(index)?.id ?: RecyclerView.NO_ID
41 | }
42 |
43 | override fun getItemViewType(position: Int): Int {
44 | return R.id.viewtype_item
45 | }
46 |
47 | /**
48 | * ViewHolder displaying a Feed
49 | */
50 | private class FeedViewHolder internal constructor(private val binding: ListFeedBinding, private val listener: FeedManageListener) : RecyclerView.ViewHolder(binding.root), FeedColorsListener {
51 | fun bindFeed(feed: Feed) {
52 | binding.textViewTitle.text = feed.name
53 | binding.textViewFolder.text = feed.getFolderTitle(itemView.context)
54 | binding.deleteFeed.setOnClickListener { listener.deleteFeed(feed) }
55 | if (feed.isConsideredFailed()) {
56 | binding.feedFailure.visibility = View.VISIBLE
57 | binding.feedFailure.text = feed.lastUpdateError
58 | } else {
59 | binding.feedFailure.visibility = View.GONE
60 | }
61 | itemView.setOnClickListener { listener.showFeedDialog(feed) }
62 | FaviconLoader.Builder(binding.imageviewFavicon).build().load(itemView.context, feed, this)
63 | }
64 |
65 | override fun onGenerated(feedColors: FeedColors) {}
66 | override fun onStart() {}
67 |
68 | }
69 |
70 | init {
71 | setHasStableIds(true)
72 | }
73 |
74 | override fun getItemCount(): Int {
75 | return feeds.size
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/view/FolderBottomSheetDialogFragment.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2019. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 | package email.schaal.ocreader.view
20 |
21 | import android.os.Bundle
22 | import android.view.LayoutInflater
23 | import android.view.View
24 | import android.view.ViewGroup
25 | import androidx.fragment.app.activityViewModels
26 | import androidx.lifecycle.Observer
27 | import androidx.preference.PreferenceManager
28 | import androidx.recyclerview.widget.LinearLayoutManager
29 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
30 | import email.schaal.ocreader.Preferences
31 | import email.schaal.ocreader.database.FeedViewModel
32 | import email.schaal.ocreader.database.FeedViewModel.FeedViewModelFactory
33 | import email.schaal.ocreader.database.model.AllUnreadFolder
34 | import email.schaal.ocreader.database.model.Folder
35 | import email.schaal.ocreader.database.model.TreeItem
36 | import email.schaal.ocreader.databinding.DialogFoldersBinding
37 | import email.schaal.ocreader.view.TreeItemsAdapter.TreeItemClickListener
38 |
39 | class FolderBottomSheetDialogFragment : BottomSheetDialogFragment() {
40 | private lateinit var binding: DialogFoldersBinding
41 | private val viewModel: FeedViewModel by activityViewModels { FeedViewModelFactory(requireContext()) }
42 | private lateinit var foldersAdapter: TreeItemsAdapter
43 | private var treeItemClickListener: TreeItemClickListener? = null
44 |
45 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
46 | binding = DialogFoldersBinding.inflate(inflater, container, true)
47 | return binding.root
48 | }
49 |
50 | fun setTreeItemClickListener(treeItemClickListener: TreeItemClickListener) {
51 | this.treeItemClickListener = object : TreeItemClickListener {
52 | override fun onTreeItemClick(treeItem: TreeItem) {
53 | treeItemClickListener.onTreeItemClick(treeItem)
54 | dismiss()
55 | }
56 | }
57 | }
58 |
59 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
60 | super.onViewCreated(view, savedInstanceState)
61 | foldersAdapter = TreeItemsAdapter(requireContext(), viewModel.folders.value, treeItemClickListener, viewModel.topFolders)
62 | foldersAdapter.setSelectedTreeItemId(viewModel.selectedTreeItem.value?.treeItemId() ?: AllUnreadFolder.ID)
63 |
64 | viewModel.updateFolders(Preferences.SHOW_ONLY_UNREAD.getBoolean(PreferenceManager.getDefaultSharedPreferences(requireContext())))
65 | viewModel.folders.observe(this, Observer {
66 | folders: List? -> foldersAdapter.updateTreeItems(folders)
67 | })
68 |
69 | binding.recyclerViewFolders.adapter = foldersAdapter
70 | binding.recyclerViewFolders.layoutManager = LinearLayoutManager(requireContext())
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/view/FolderSpinnerAdapter.kt:
--------------------------------------------------------------------------------
1 | package email.schaal.ocreader.view
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.BaseAdapter
7 | import android.widget.TextView
8 | import androidx.annotation.LayoutRes
9 | import email.schaal.ocreader.R
10 | import email.schaal.ocreader.database.model.Folder
11 |
12 | /**
13 | * Adapter for Spinner to display Folders
14 | */
15 | class FolderSpinnerAdapter(context: Context, private var folders: List = emptyList()) : BaseAdapter() {
16 | private val rootFolder: String = context.getString(R.string.root_folder)
17 |
18 | override fun getCount(): Int {
19 | return folders.size + 1
20 | }
21 |
22 | fun updateFolders(folders: List) {
23 | this.folders = folders
24 | notifyDataSetChanged()
25 | }
26 |
27 | override fun getItem(position: Int): Any {
28 | return if (position == 0) rootFolder else folders[position - 1].name
29 | }
30 |
31 | override fun getItemId(position: Int): Long {
32 | return if (position == 0) 0 else (folders[position - 1].id ?: 0)
33 | }
34 |
35 | override fun getItemViewType(position: Int): Int {
36 | return if (position == 0) VIEW_TYPE_NONE else VIEW_TYPE_FOLDER
37 | }
38 |
39 | override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
40 | return getSpinnerView(position, convertView, parent, R.layout.spinner_folder_dropdown)
41 | }
42 |
43 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
44 | return getSpinnerView(position, convertView, parent, R.layout.spinner_folder)
45 | }
46 |
47 | private fun getSpinnerView(position: Int, convertView: View?, parent: ViewGroup, @LayoutRes layout: Int): View {
48 | val view = convertView ?: View.inflate(parent.context, layout, null)
49 | (view as TextView).text = getItem(position) as String
50 | return view
51 | }
52 |
53 | override fun hasStableIds(): Boolean {
54 | return true
55 | }
56 |
57 | fun getPosition(folderId: Long?): Int {
58 | if (folderId == 0L || folderId == null) return 0
59 | var i = 1
60 | while (i - 1 < folders.size) {
61 | if (folders[i - 1].id == folderId) {
62 | return i
63 | }
64 | i++
65 | }
66 | return -1
67 | }
68 |
69 | companion object {
70 | private const val VIEW_TYPE_NONE = 0
71 | private const val VIEW_TYPE_FOLDER = 1
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/email/schaal/ocreader/view/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2020. Daniel Schaal
3 | *
4 | * This file is part of ocreader.
5 | *
6 | * ocreader is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * ocreader is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with Foobar. If not, see .
18 | */
19 |
20 | package email.schaal.ocreader.view
21 |
22 | import androidx.lifecycle.MutableLiveData
23 | import androidx.lifecycle.ViewModel
24 |
25 | class LoginViewModel : ViewModel() {
26 | val credentialsLiveData: MutableLiveData