Github version
This commit is contained in:
		
							
								
								
									
										73
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										73
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,73 +0,0 @@ | |||||||
| # Introduction |  | ||||||
|  |  | ||||||
| ### Hey you ! |  | ||||||
|  |  | ||||||
| Thank you for wanting to help. Even the smallest things can help this project become better. |  | ||||||
|  |  | ||||||
| Please read the guidelines before contributing, and follow them (or try to) when contributing. |  | ||||||
|  |  | ||||||
| ### What you can do to help. |  | ||||||
|  |  | ||||||
| There are many ways to contribute to this project, you could [translate the app](https://crowdin.com/project/readerforselfoss), report bugs, request missing features, suggest enhancements and changes to existing ones. You also can improve the README with useful tips that could help the other users. |  | ||||||
|  |  | ||||||
| You can fork the repository, and [help me solve some issues](https://github.com/aminecmi/ReaderforSelfoss/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://github.com/aminecmi/ReaderforSelfoss/issues) |  | ||||||
|  |  | ||||||
| ### What I can't help you with. |  | ||||||
|  |  | ||||||
| Please, don't use the issue tracker for anything related to [Selfoss itself](https://github.com/SSilence/selfoss). The app calls the api provided by Selfoss, and can't help with solving issues with your Selfoss instance. |  | ||||||
|  |  | ||||||
| Always check if the web version of your instance is working. |  | ||||||
|  |  | ||||||
| # Some rules |  | ||||||
| ### Bug reports/Feature request |  | ||||||
|  |  | ||||||
| * Always search before reporting an issue or asking for a feature to avoid duplicates. |  | ||||||
| * Include your unique user id. It's displayed on the debug settings page. (You can tap it, it'll be copied to your clipboard) |  | ||||||
| * Include every other useful details (app version, phone model, Android version and screenshots when possible). |  | ||||||
| * Avoid bumping non-fatal issues, or feature requests. I'll try to fix them as soon as possible, and try to prioritize the requests. (You may wan to use the [reactions](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) for that) |  | ||||||
|  |  | ||||||
| ### Pull requests |  | ||||||
|  |  | ||||||
| * Don't create a PR for translations. See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654) for an explanation why. |  | ||||||
| * Please ask before starting to work on an issue. I may be working on it, or someone else could be doing so. |  | ||||||
| * Each pull request should implement **ONE** feature or bugfix. Keep in mind that you can submit as many PR as you want. |  | ||||||
| * Your code must be simple and clear enough to avoid using comments to explain what it does. |  | ||||||
| * Follow the used coding style [the android koding style](https://android.github.io/kotlin-guides/style.html) ([some idoms for reference](http://kotlinlang.org/docs/reference/idioms.html)) with more to come. |  | ||||||
| * Try as much as possible to write a test for your feature, and if you do so, run it, and make it work. |  | ||||||
| * Always check your changes and discard the ones that are irrelevant to your feature or bugfix. |  | ||||||
| * Have meaningful commit messages. |  | ||||||
| * Always reference the issue you are working on in your PR description. |  | ||||||
| * Be willing to accept criticism on your PRs (as I am on mine). |  | ||||||
| * Remember that PR review can take time. |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Install Selfoss (if you don't have an instance) |  | ||||||
|  |  | ||||||
| I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it. |  | ||||||
|  |  | ||||||
| All the details to need are [here](https://selfoss.aditu.de/). |  | ||||||
|  |  | ||||||
| # Build the project |  | ||||||
|  |  | ||||||
| You can directly import this project into IntellIJ/Android Studio. |  | ||||||
|  |  | ||||||
| You'll have to: |  | ||||||
|  |  | ||||||
| - Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples) |  | ||||||
|  |  | ||||||
|     - appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.** |  | ||||||
|  |  | ||||||
| ### Examples: |  | ||||||
| #### Inside ~/.gradle/gradle.properties |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| appLoginUrl="URL" # It can be empty. |  | ||||||
| appLoginUsername="LOGIN" # It can be empty. |  | ||||||
| appLoginPassword="PASS" # It can be empty. |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### As gradle parameters |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| ./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS" |  | ||||||
| ``` |  | ||||||
							
								
								
									
										32
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,32 +0,0 @@ | |||||||
| ### Prerequisites |  | ||||||
|  |  | ||||||
| * [ ] Are you running the latest version? |  | ||||||
| * [ ] Did you check for an existing issue ? |  | ||||||
| * [ ] Are you reporting to the correct repository? |  | ||||||
| * [ ] Did you perform a cursory search? |  | ||||||
| * [ ] Did you read the `CONTRIBUTING` guide ? |  | ||||||
|  |  | ||||||
| ### Description |  | ||||||
|  |  | ||||||
| [Description of the bug or feature] |  | ||||||
|  |  | ||||||
| ### Steps to Reproduce |  | ||||||
|  |  | ||||||
| 1. [First Step] |  | ||||||
| 2. [Second Step] |  | ||||||
| 3. [and so on...] |  | ||||||
|  |  | ||||||
| **Expected behavior:** [What you expected to happen] |  | ||||||
|  |  | ||||||
| **Actual behavior:** [What actually happened] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Screenshots (optional) |  | ||||||
|  |  | ||||||
| `...` |  | ||||||
|  |  | ||||||
| ### Device |  | ||||||
|  |  | ||||||
| - Device (manufacturer, model ...) |  | ||||||
| - OS (Android Version, ROM/Stock, Rooted/not, mods...) |  | ||||||
| - App version _(See Prerequisites)_ |  | ||||||
							
								
								
									
										14
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,14 +0,0 @@ | |||||||
| ## Types of changes |  | ||||||
|  |  | ||||||
| - [ ] I have read the **CONTRIBUTING** document. |  | ||||||
| - [ ] My code follows the code style of this project. |  | ||||||
| - [ ] I have updated the documentation accordingly. |  | ||||||
| - [ ] I have added tests to cover my changes. |  | ||||||
| - [ ] All new and existing tests passed. |  | ||||||
| - [ ] This is **NOT** translation related. (See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654)) |  | ||||||
|  |  | ||||||
| This closes issue #XXX |  | ||||||
|  |  | ||||||
| This is implements feature #YYY |  | ||||||
|  |  | ||||||
| This finishes chore #ZZZ |  | ||||||
							
								
								
									
										562
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										562
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,562 +0,0 @@ | |||||||
| **1.7.x** |  | ||||||
|  |  | ||||||
| - Hiding tags with 0 articles |  | ||||||
|  |  | ||||||
| - Fixed issue with basic auth and images loading |  | ||||||
|  |  | ||||||
| - Added the ability to justify or left align the reader text |  | ||||||
|  |  | ||||||
| - Fixed #251 |  | ||||||
|  |  | ||||||
| - Added experimental issue to set a default timeout. Should work for #238. |  | ||||||
|  |  | ||||||
| - Closing #220. |  | ||||||
|  |  | ||||||
| - Start of #238. "Add a quick shortcut to open the app on offline mode ?" |  | ||||||
|  |  | ||||||
| - Closes #216. Issue with selfoss version 2.19. |  | ||||||
|  |  | ||||||
| - Closes #179. Sync of read/unread/star/unstar items on background task or on app reload with network available. |  | ||||||
|  |  | ||||||
| - Closes #33. Background sync with settings. |  | ||||||
|  |  | ||||||
| - Closing #1. Initial article caching. |  | ||||||
|  |  | ||||||
| - Closing #228 by removing the list action bar. Action buttons are exclusively on the card view from now on. |  | ||||||
|  |  | ||||||
| - Closing #38. Only doing api calls on network available. |  | ||||||
|  |  | ||||||
| - Closing #298 and #287. Issues with Listview rendering |  | ||||||
|  |  | ||||||
| - Closing #290. Fixing back button issue in Settings |  | ||||||
|  |  | ||||||
| - Closing #300. Fixing issues when displaying some special characters. |  | ||||||
|  |  | ||||||
| - Closing #310. Some feeds don't have icons nor thumbnails. |  | ||||||
|  |  | ||||||
| - Closing #178. Expending images on tap. |  | ||||||
|  |  | ||||||
| - Closing #323. Old issue with textview not having the right color. |  | ||||||
|  |  | ||||||
| - Closing #324. Svg images loading crashes the app. |  | ||||||
|  |  | ||||||
| - Closing #322. App crashed because of svg images. |  | ||||||
|  |  | ||||||
| - Closing #236. New sources can be added in Selfoss 2.19. |  | ||||||
|  |  | ||||||
| - Closing #397 and #355. Tag and Sources filters are now exclusive. |  | ||||||
|  |  | ||||||
| - Dropped support for android 4, the last version supporting it is v1721030811 |  | ||||||
|  |  | ||||||
| - Added ability to scroll articles up and down using the volume keys #400 |  | ||||||
|  |  | ||||||
| **1.6.x** |  | ||||||
|  |  | ||||||
| - Handling hidden tags. |  | ||||||
|  |  | ||||||
| - Fixed pre-lolipop issue with automatic theme changes. |  | ||||||
|  |  | ||||||
| - Removed all Build config things. |  | ||||||
|  |  | ||||||
| - Removed firebase and fabric. |  | ||||||
|  |  | ||||||
| - Added Acra for optional crash reporting and error logging. |  | ||||||
|  |  | ||||||
| - Dynamic themes ! |  | ||||||
|  |  | ||||||
| - Strings cleaning. |  | ||||||
|  |  | ||||||
| - Versions updates. |  | ||||||
|  |  | ||||||
| - Fixes #215, #208. |  | ||||||
|  |  | ||||||
| - Fixes #328. |  | ||||||
|  |  | ||||||
| **1.5.7.x** |  | ||||||
|  |  | ||||||
| - Added confirmation to the mark as read and update menues. |  | ||||||
|  |  | ||||||
| - Add to favorites from article viewer. |  | ||||||
|  |  | ||||||
| - Added an option to use a webview in the article viewer (see #149) |  | ||||||
|  |  | ||||||
| - Fixes (#151 #152 #155 #157 #160 #174) and more. |  | ||||||
|  |  | ||||||
| - New year fixes !!! |  | ||||||
|  |  | ||||||
| - Changed page indicator position as it was overlaping content. |  | ||||||
|  |  | ||||||
| - Now using slack instead of gitter. |  | ||||||
|  |  | ||||||
| - Moved completely to a webview to fix #161. |  | ||||||
|  |  | ||||||
| - Fixed typos in French ( Thanks @aancel ) |  | ||||||
|  |  | ||||||
| - Updated the Contribution guide about translations. |  | ||||||
|  |  | ||||||
| - Better handling for articles update. (See #169) |  | ||||||
|  |  | ||||||
| - Ability to change the article viewer content font size (see #153) |  | ||||||
|  |  | ||||||
| - Versions updates * 2. |  | ||||||
|  |  | ||||||
| - Added padding to the recyclerview. |  | ||||||
|  |  | ||||||
| **1.5.5.x (didn't last long) AND 1.5.6.x** |  | ||||||
|  |  | ||||||
| - Toolbar in reader activity. |  | ||||||
|  |  | ||||||
| - Marking items as read on scroll (with settings to enable/disable). |  | ||||||
|  |  | ||||||
| - Swapped the title and subtitle in the article viewer. |  | ||||||
|  |  | ||||||
| - Added an animation to the viewpager. |  | ||||||
|  |  | ||||||
| - Completed Dutch, Indonesian and Portuguese translations ! |  | ||||||
|  |  | ||||||
| - Fixed #142, #144, #147. |  | ||||||
|  |  | ||||||
| - Changed versions handling. |  | ||||||
|  |  | ||||||
| - Removed indonesian english as it was causing issues with the english version of the app. |  | ||||||
|  |  | ||||||
| **1.5.4.22** |  | ||||||
|  |  | ||||||
| - You can now scroll through the loaded articles ! |  | ||||||
|  |  | ||||||
| **1.5.4.21** |  | ||||||
|  |  | ||||||
| - Spanish translation and some Indonesian ! |  | ||||||
|  |  | ||||||
| **1.5.4.20** |  | ||||||
|  |  | ||||||
| - Turkish translation ! |  | ||||||
|  |  | ||||||
| **1.5.4.19** |  | ||||||
|  |  | ||||||
| - Fixed an issue with crowdin configuration (and its translations) |  | ||||||
|  |  | ||||||
| **1.5.4.18** |  | ||||||
|  |  | ||||||
| - Typo fix. |  | ||||||
|  |  | ||||||
| - The real last infinite scroll bug fix. |  | ||||||
|  |  | ||||||
| - Simplified Chinese translation ! |  | ||||||
|  |  | ||||||
| **1.5.4.17** |  | ||||||
|  |  | ||||||
| - Fixed the last bug with infinite scroll. |  | ||||||
|  |  | ||||||
| **1.5.4.16** |  | ||||||
|  |  | ||||||
| - Fixing list view displaying issues. |  | ||||||
|  |  | ||||||
| - Endless scroll is not in beta anymore. |  | ||||||
|  |  | ||||||
| **1.5.4.15** |  | ||||||
|  |  | ||||||
| - Fixed an issue with the sources list. |  | ||||||
|  |  | ||||||
| **1.5.4.14** |  | ||||||
|  |  | ||||||
| - Fixing infinite scroll trying to load more items when there are no more. |  | ||||||
|  |  | ||||||
| **1.5.4.13** |  | ||||||
|  |  | ||||||
| - Displaying the right number of items. |  | ||||||
|  |  | ||||||
| - Fixing infinite scroll remaining issues. Should be stable enough. |  | ||||||
|  |  | ||||||
| **1.5.4.12** |  | ||||||
|  |  | ||||||
| - Fixed fab and toolbar issue (#113) |  | ||||||
|  |  | ||||||
| - Fixed links clickable (#114) |  | ||||||
|  |  | ||||||
| - Changed the link colors in the article viewer |  | ||||||
|  |  | ||||||
| **1.5.4.11** |  | ||||||
|  |  | ||||||
| - Hiding FABs on scroll. |  | ||||||
|  |  | ||||||
| - Closing #109 (code cleaning) |  | ||||||
|  |  | ||||||
| - Hiding fabs on scroll (#101) |  | ||||||
|  |  | ||||||
| **1.5.4.10** |  | ||||||
|  |  | ||||||
| - Displaying a loader when "reading more" in the article viewer. |  | ||||||
|  |  | ||||||
| - Displaying the thumbnail instead of icon on the article viewer. |  | ||||||
|  |  | ||||||
| - Scrolling to top when loading content with the "read more" button. |  | ||||||
|  |  | ||||||
| **1.5.4.09** |  | ||||||
|  |  | ||||||
| - Using the kotlin wrapper for the material drawer (see #98 for more details). |  | ||||||
|  |  | ||||||
| - Updated support libraries |  | ||||||
|  |  | ||||||
| - Changed the Floating Action Button to the support library version. |  | ||||||
|  |  | ||||||
| - New reader activity action bar #103. |  | ||||||
|  |  | ||||||
| **1.5.4.08** |  | ||||||
|  |  | ||||||
| - Thanks @jrafaelsantana for translating the whole app in Brazilian Portuguese. |  | ||||||
|  |  | ||||||
| **1.5.4.07** |  | ||||||
|  |  | ||||||
| - Loading more items on swipe too. |  | ||||||
|  |  | ||||||
| - Fixed popup menu style. User may need to reselect the theme. |  | ||||||
|  |  | ||||||
| - Disabled reporting marking items as read if there isn't an issue. |  | ||||||
|  |  | ||||||
| **1.5.4.05/06** |  | ||||||
|  |  | ||||||
| - Translation fix. |  | ||||||
|  |  | ||||||
| **1.5.4.04** |  | ||||||
|  |  | ||||||
| - Fixing an issue with marking items as read (something related to an old version of selfoss). |  | ||||||
|  |  | ||||||
| **1.5.4.03** |  | ||||||
|  |  | ||||||
| - Trying to fix some issue with pre-launch reports. Reverted because it seems to be related to the dev console side. |  | ||||||
|  |  | ||||||
| **1.5.4.02** |  | ||||||
|  |  | ||||||
| - Fixing full height cards issue. |  | ||||||
|  |  | ||||||
| **1.5.4.01** |  | ||||||
|  |  | ||||||
| - Removed the "apk downloaded from outside of playstore" message. |  | ||||||
|  |  | ||||||
| - Versions update. |  | ||||||
|  |  | ||||||
| - HTML viewer version update. It should fix an issue with images. |  | ||||||
|  |  | ||||||
| - Some code cleaning. |  | ||||||
|  |  | ||||||
| **1.5.4.00** |  | ||||||
|  |  | ||||||
| - Added issue reporting from within the app. |  | ||||||
|  |  | ||||||
| **1.5.3.06** |  | ||||||
|  |  | ||||||
| - Fixed infinite scroll not working. |  | ||||||
|  |  | ||||||
| - Fixed logs not working. |  | ||||||
|  |  | ||||||
| - Temporary workaround handling opening invalid urls. Waiting to solve #83. |  | ||||||
|  |  | ||||||
| **1.5.3.05** |  | ||||||
|  |  | ||||||
| - Fixed an issue on older versions of Android. |  | ||||||
|  |  | ||||||
| - Libs update. |  | ||||||
|  |  | ||||||
| **1.5.3.04** |  | ||||||
|  |  | ||||||
| - Crowdin translations |  | ||||||
|  |  | ||||||
| **1.5.3.03** |  | ||||||
|  |  | ||||||
| - Libs updates. |  | ||||||
|  |  | ||||||
| - Translation fix. |  | ||||||
|  |  | ||||||
| **1.5.3.01/02** |  | ||||||
|  |  | ||||||
| - Added translation link to the settings page. |  | ||||||
|  |  | ||||||
| - Added the translation link to the README. |  | ||||||
|  |  | ||||||
| **1.5.3.00** |  | ||||||
|  |  | ||||||
| - (BETA) Added pull from bottom to load more pages of results. May be buggy. |  | ||||||
|  |  | ||||||
| **1.5.2.18/19** |  | ||||||
|  |  | ||||||
| - APK minification finally working. That means less space taken ! |  | ||||||
| - Added an option to log every API call. |  | ||||||
|  |  | ||||||
| **1.5.2.17** |  | ||||||
|  |  | ||||||
| - Source code and tracker links weren't being set, and updated the contributing doc. |  | ||||||
|  |  | ||||||
| **1.5.2.15/16** |  | ||||||
|  |  | ||||||
| - Adding an account header on the lateral drawer. |  | ||||||
|  |  | ||||||
| - The account header is only displayed when the setting is enabled. |  | ||||||
|  |  | ||||||
| **1.5.2.13/14** |  | ||||||
|  |  | ||||||
| - Updated glide. |  | ||||||
|  |  | ||||||
| - Loading images from self signed certificate now working. |  | ||||||
|  |  | ||||||
| **1.5.2.12** |  | ||||||
|  |  | ||||||
| - Self signed certificates are now working for loading data. Image are not loading yet. |  | ||||||
|  |  | ||||||
| **1.5.2.11** |  | ||||||
|  |  | ||||||
| - Added a random unique identifier to be used in the logs. |  | ||||||
|  |  | ||||||
| **1.5.2.08/09/10** |  | ||||||
|  |  | ||||||
| - Added settable logs for reading articles problems. |  | ||||||
|  |  | ||||||
| **1.5.2.07** |  | ||||||
|  |  | ||||||
| - Added the ability to choose the number of items loaded (the maximum value is 200 and is imposed by the selfoss api) |  | ||||||
|  |  | ||||||
| **1.5.2.06** |  | ||||||
|  |  | ||||||
| - Fix problem introduced in 1.5.2.04. SVG file not working on older versions of android. |  | ||||||
|  |  | ||||||
| **1.5.2.05** |  | ||||||
|  |  | ||||||
| - Versions updates |  | ||||||
|  |  | ||||||
| **1.5.2.04** |  | ||||||
|  |  | ||||||
| - Reverted to the old icon. |  | ||||||
|  |  | ||||||
| - Better icon for the intro activity. |  | ||||||
|  |  | ||||||
| - Updated gradle version. |  | ||||||
|  |  | ||||||
| **1.5.2.03** |  | ||||||
|  |  | ||||||
| - Added the ability to accept self signed certificates. (Needs more testing) |  | ||||||
|  |  | ||||||
| **1.5.2.02** |  | ||||||
|  |  | ||||||
| - Added optional login option. |  | ||||||
|  |  | ||||||
| **1.5.2.01** |  | ||||||
|  |  | ||||||
| - New (Better) Icon ! |  | ||||||
|  |  | ||||||
| **1.5.2.0** |  | ||||||
|  |  | ||||||
| - New Icon ! |  | ||||||
|  |  | ||||||
| **1.5.1.9/10/11** |  | ||||||
|  |  | ||||||
| - Hiding the unread badge when marking all items as read. |  | ||||||
|  |  | ||||||
| **1.5.1.8** |  | ||||||
|  |  | ||||||
| - Fixes and libs updates. |  | ||||||
|  |  | ||||||
| **1.5.1.7** |  | ||||||
|  |  | ||||||
| - Bug fixes. |  | ||||||
|  |  | ||||||
| - Code cleaning |  | ||||||
|  |  | ||||||
| **1.5.1.6** |  | ||||||
|  |  | ||||||
| - Added back the badges after it was fixed on the library side. |  | ||||||
|  |  | ||||||
| **1.5.1.5** |  | ||||||
|  |  | ||||||
| - THEMES !!!! For now, the app has predefined themes. You can ask for new ones until I make them dynamic. |  | ||||||
|  |  | ||||||
| **1.5.1.3/4** |  | ||||||
|  |  | ||||||
| - Fixes introduces by the previous alpha (1.5.1.2) |  | ||||||
|  |  | ||||||
| **1.5.1.2** |  | ||||||
|  |  | ||||||
| - Added testing to the CI. |  | ||||||
|  |  | ||||||
| - Code cleaning |  | ||||||
|  |  | ||||||
| - Display the pull to refresh loader on api call |  | ||||||
|  |  | ||||||
| - Fixes : |  | ||||||
|  |  | ||||||
|   - Can't pull down to refresh on first launch |  | ||||||
|  |  | ||||||
|   - Recurring crash because of the url |  | ||||||
|  |  | ||||||
|   - Couldn't open some urls because of missing "http" |  | ||||||
|  |  | ||||||
|   - Adding a source with invalid url would crash |  | ||||||
|  |  | ||||||
|  |  | ||||||
| **1.5.1.1** |  | ||||||
|  |  | ||||||
| - Fixed an issue when trying to add a source without being logged in. |  | ||||||
|  |  | ||||||
| - Reloading drawer tags badges on slide to refresh. |  | ||||||
|  |  | ||||||
| **1.5.1** |  | ||||||
|  |  | ||||||
| - Added a drawer for filtering sources and tags. |  | ||||||
|  |  | ||||||
| - You can now search for items from the toolbar. |  | ||||||
|  |  | ||||||
| **1.5.0.2** |  | ||||||
|  |  | ||||||
| - If the content in the article viewer is empty, the article will open in a custom tab. |  | ||||||
|  |  | ||||||
| - Added a share button, and an "open in browser" button to the bottom of the article viewer. |  | ||||||
|  |  | ||||||
| - Updated custom tab code. |  | ||||||
|  |  | ||||||
| **1.5.0.1** |  | ||||||
|  |  | ||||||
| - The release APK wasn't working at all. |  | ||||||
|  |  | ||||||
| **1.5.0.0** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - The app is now open source ! And rewritten in Kotlin ! |  | ||||||
|  |  | ||||||
| **1.4.0.9** |  | ||||||
|  |  | ||||||
| _Fixes_ |  | ||||||
|  |  | ||||||
| - Fixes and missing translations. |  | ||||||
|  |  | ||||||
| **1.4.0.8** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - Added setting for full height and fixed height cards size. |  | ||||||
|  |  | ||||||
| _Fixed_ |  | ||||||
|  |  | ||||||
| - Action Bar color now matches the primary color on the recent apps screen. |  | ||||||
|  |  | ||||||
| - Added a bottom margin to de article viewer content |  | ||||||
|  |  | ||||||
| - Multiple fixes for the new article viewer. |  | ||||||
|  |  | ||||||
| **1.4.0.7** |  | ||||||
|  |  | ||||||
| _Fixed_ |  | ||||||
|  |  | ||||||
| - Disable swipe to hide from other "tabs" and avoid badges problems |  | ||||||
|  |  | ||||||
| - Fixed a bug with the new Article viewer with some displaying fixes |  | ||||||
|  |  | ||||||
|  |  | ||||||
| **1.4.0.6** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - Added the ability to use http authentication (Basic and Digest) |  | ||||||
|  |  | ||||||
| _Fixed_ |  | ||||||
|  |  | ||||||
| - Fixed gitter link |  | ||||||
|  |  | ||||||
| - Change the article viewer because the other was causing crashes |  | ||||||
|  |  | ||||||
| **1.4.0.5** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - Added an intro to the app. |  | ||||||
|  |  | ||||||
| - Added the ability to test the app without a Selfoss instance. |  | ||||||
|  |  | ||||||
| **1.4.0.4** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - Added the ability to have a github build. If the apk is a Github build, check for update and ask the user to download it (directly from the github page). |  | ||||||
|  |  | ||||||
| _Changes_ |  | ||||||
|  |  | ||||||
| - The apk stating that the app wasn't installed from the store is only displayed on start. |  | ||||||
|  |  | ||||||
| **1.4.0.3** |  | ||||||
|  |  | ||||||
| _Fixed_ |  | ||||||
|  |  | ||||||
| - Fixed boolean problem. |  | ||||||
|  |  | ||||||
| **1.4.0.2** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - The app is available in Dutch ! |  | ||||||
|  |  | ||||||
| _Fixed_ |  | ||||||
|  |  | ||||||
| - Fixed a bug with the articles states. |  | ||||||
|  |  | ||||||
| **1.4.0.1** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - You can now help me translate the app ! There will be a dialog displayed the first time you open the app, and the link will still be available from the settings page. |  | ||||||
|  |  | ||||||
| _Changes_ |  | ||||||
|  |  | ||||||
| - Changed the custom tabs color to dark orange to fix the wrong title color. |  | ||||||
|  |  | ||||||
| _Fixes_ |  | ||||||
|  |  | ||||||
| - The badges now are shown even if the tab is selected. |  | ||||||
|  |  | ||||||
| - Fixed feeds not reloading on app resume (caused by 1.4.0.0 changes). |  | ||||||
|  |  | ||||||
| **1.4.0.0** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - Added a setting to enable/disable the article viewer when the internal browser is enabled. |  | ||||||
|  |  | ||||||
| - Added peek to the card view. |  | ||||||
|  |  | ||||||
| - Text drawable if no icon. |  | ||||||
|  |  | ||||||
|  |  | ||||||
| _Changes_ |  | ||||||
|  |  | ||||||
| - Changed the external browser setting to internal browser and handled the change on first open. |  | ||||||
|  |  | ||||||
| - Some text changes. |  | ||||||
|  |  | ||||||
| - Better animations handling on slow networks. |  | ||||||
|  |  | ||||||
| ... |  | ||||||
|  |  | ||||||
| **1.3.3.5** |  | ||||||
|  |  | ||||||
| _New_ |  | ||||||
|  |  | ||||||
| - Added tab bar badges with settings to display them. |  | ||||||
|  |  | ||||||
| - Added invites. |  | ||||||
|  |  | ||||||
| _Fixes_ |  | ||||||
|  |  | ||||||
| - Fixed a typo. |  | ||||||
|  |  | ||||||
| _Updates_ |  | ||||||
|  |  | ||||||
| - Updated support library to 10.2.0. |  | ||||||
|  |  | ||||||
| - Updated firebase to 10.2.0. |  | ||||||
|  |  | ||||||
| - Updated article_viewer to 0.20.1. |  | ||||||
|  |  | ||||||
| - Updated bottom-bar to 2.1.1. |  | ||||||
|  |  | ||||||
|  |  | ||||||
| **1.3.3.4** |  | ||||||
|  |  | ||||||
| ... |  | ||||||
							
								
								
									
										674
									
								
								LICENSE.md
									
									
									
									
									
								
							
							
						
						
									
										674
									
								
								LICENSE.md
									
									
									
									
									
								
							| @@ -1,674 +0,0 @@ | |||||||
|                     GNU GENERAL PUBLIC LICENSE |  | ||||||
|                        Version 3, 29 June 2007 |  | ||||||
|  |  | ||||||
|  Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> |  | ||||||
|  Everyone is permitted to copy and distribute verbatim copies |  | ||||||
|  of this license document, but changing it is not allowed. |  | ||||||
|  |  | ||||||
|                             Preamble |  | ||||||
|  |  | ||||||
|   The GNU General Public License is a free, copyleft license for |  | ||||||
| software and other kinds of works. |  | ||||||
|  |  | ||||||
|   The licenses for most software and other practical works are designed |  | ||||||
| to take away your freedom to share and change the works.  By contrast, |  | ||||||
| the GNU General Public License is intended to guarantee your freedom to |  | ||||||
| share and change all versions of a program--to make sure it remains free |  | ||||||
| software for all its users.  We, the Free Software Foundation, use the |  | ||||||
| GNU General Public License for most of our software; it applies also to |  | ||||||
| any other work released this way by its authors.  You can apply it to |  | ||||||
| your programs, too. |  | ||||||
|  |  | ||||||
|   When we speak of free software, we are referring to freedom, not |  | ||||||
| price.  Our General Public Licenses are designed to make sure that you |  | ||||||
| have the freedom to distribute copies of free software (and charge for |  | ||||||
| them if you wish), that you receive source code or can get it if you |  | ||||||
| want it, that you can change the software or use pieces of it in new |  | ||||||
| free programs, and that you know you can do these things. |  | ||||||
|  |  | ||||||
|   To protect your rights, we need to prevent others from denying you |  | ||||||
| these rights or asking you to surrender the rights.  Therefore, you have |  | ||||||
| certain responsibilities if you distribute copies of the software, or if |  | ||||||
| you modify it: responsibilities to respect the freedom of others. |  | ||||||
|  |  | ||||||
|   For example, if you distribute copies of such a program, whether |  | ||||||
| gratis or for a fee, you must pass on to the recipients the same |  | ||||||
| freedoms that you received.  You must make sure that they, too, receive |  | ||||||
| or can get the source code.  And you must show them these terms so they |  | ||||||
| know their rights. |  | ||||||
|  |  | ||||||
|   Developers that use the GNU GPL protect your rights with two steps: |  | ||||||
| (1) assert copyright on the software, and (2) offer you this License |  | ||||||
| giving you legal permission to copy, distribute and/or modify it. |  | ||||||
|  |  | ||||||
|   For the developers' and authors' protection, the GPL clearly explains |  | ||||||
| that there is no warranty for this free software.  For both users' and |  | ||||||
| authors' sake, the GPL requires that modified versions be marked as |  | ||||||
| changed, so that their problems will not be attributed erroneously to |  | ||||||
| authors of previous versions. |  | ||||||
|  |  | ||||||
|   Some devices are designed to deny users access to install or run |  | ||||||
| modified versions of the software inside them, although the manufacturer |  | ||||||
| can do so.  This is fundamentally incompatible with the aim of |  | ||||||
| protecting users' freedom to change the software.  The systematic |  | ||||||
| pattern of such abuse occurs in the area of products for individuals to |  | ||||||
| use, which is precisely where it is most unacceptable.  Therefore, we |  | ||||||
| have designed this version of the GPL to prohibit the practice for those |  | ||||||
| products.  If such problems arise substantially in other domains, we |  | ||||||
| stand ready to extend this provision to those domains in future versions |  | ||||||
| of the GPL, as needed to protect the freedom of users. |  | ||||||
|  |  | ||||||
|   Finally, every program is threatened constantly by software patents. |  | ||||||
| States should not allow patents to restrict development and use of |  | ||||||
| software on general-purpose computers, but in those that do, we wish to |  | ||||||
| avoid the special danger that patents applied to a free program could |  | ||||||
| make it effectively proprietary.  To prevent this, the GPL assures that |  | ||||||
| patents cannot be used to render the program non-free. |  | ||||||
|  |  | ||||||
|   The precise terms and conditions for copying, distribution and |  | ||||||
| modification follow. |  | ||||||
|  |  | ||||||
|                        TERMS AND CONDITIONS |  | ||||||
|  |  | ||||||
|   0. Definitions. |  | ||||||
|  |  | ||||||
|   "This License" refers to version 3 of the GNU General Public License. |  | ||||||
|  |  | ||||||
|   "Copyright" also means copyright-like laws that apply to other kinds of |  | ||||||
| works, such as semiconductor masks. |  | ||||||
|  |  | ||||||
|   "The Program" refers to any copyrightable work licensed under this |  | ||||||
| License.  Each licensee is addressed as "you".  "Licensees" and |  | ||||||
| "recipients" may be individuals or organizations. |  | ||||||
|  |  | ||||||
|   To "modify" a work means to copy from or adapt all or part of the work |  | ||||||
| in a fashion requiring copyright permission, other than the making of an |  | ||||||
| exact copy.  The resulting work is called a "modified version" of the |  | ||||||
| earlier work or a work "based on" the earlier work. |  | ||||||
|  |  | ||||||
|   A "covered work" means either the unmodified Program or a work based |  | ||||||
| on the Program. |  | ||||||
|  |  | ||||||
|   To "propagate" a work means to do anything with it that, without |  | ||||||
| permission, would make you directly or secondarily liable for |  | ||||||
| infringement under applicable copyright law, except executing it on a |  | ||||||
| computer or modifying a private copy.  Propagation includes copying, |  | ||||||
| distribution (with or without modification), making available to the |  | ||||||
| public, and in some countries other activities as well. |  | ||||||
|  |  | ||||||
|   To "convey" a work means any kind of propagation that enables other |  | ||||||
| parties to make or receive copies.  Mere interaction with a user through |  | ||||||
| a computer network, with no transfer of a copy, is not conveying. |  | ||||||
|  |  | ||||||
|   An interactive user interface displays "Appropriate Legal Notices" |  | ||||||
| to the extent that it includes a convenient and prominently visible |  | ||||||
| feature that (1) displays an appropriate copyright notice, and (2) |  | ||||||
| tells the user that there is no warranty for the work (except to the |  | ||||||
| extent that warranties are provided), that licensees may convey the |  | ||||||
| work under this License, and how to view a copy of this License.  If |  | ||||||
| the interface presents a list of user commands or options, such as a |  | ||||||
| menu, a prominent item in the list meets this criterion. |  | ||||||
|  |  | ||||||
|   1. Source Code. |  | ||||||
|  |  | ||||||
|   The "source code" for a work means the preferred form of the work |  | ||||||
| for making modifications to it.  "Object code" means any non-source |  | ||||||
| form of a work. |  | ||||||
|  |  | ||||||
|   A "Standard Interface" means an interface that either is an official |  | ||||||
| standard defined by a recognized standards body, or, in the case of |  | ||||||
| interfaces specified for a particular programming language, one that |  | ||||||
| is widely used among developers working in that language. |  | ||||||
|  |  | ||||||
|   The "System Libraries" of an executable work include anything, other |  | ||||||
| than the work as a whole, that (a) is included in the normal form of |  | ||||||
| packaging a Major Component, but which is not part of that Major |  | ||||||
| Component, and (b) serves only to enable use of the work with that |  | ||||||
| Major Component, or to implement a Standard Interface for which an |  | ||||||
| implementation is available to the public in source code form.  A |  | ||||||
| "Major Component", in this context, means a major essential component |  | ||||||
| (kernel, window system, and so on) of the specific operating system |  | ||||||
| (if any) on which the executable work runs, or a compiler used to |  | ||||||
| produce the work, or an object code interpreter used to run it. |  | ||||||
|  |  | ||||||
|   The "Corresponding Source" for a work in object code form means all |  | ||||||
| the source code needed to generate, install, and (for an executable |  | ||||||
| work) run the object code and to modify the work, including scripts to |  | ||||||
| control those activities.  However, it does not include the work's |  | ||||||
| System Libraries, or general-purpose tools or generally available free |  | ||||||
| programs which are used unmodified in performing those activities but |  | ||||||
| which are not part of the work.  For example, Corresponding Source |  | ||||||
| includes interface definition files associated with source files for |  | ||||||
| the work, and the source code for shared libraries and dynamically |  | ||||||
| linked subprograms that the work is specifically designed to require, |  | ||||||
| such as by intimate data communication or control flow between those |  | ||||||
| subprograms and other parts of the work. |  | ||||||
|  |  | ||||||
|   The Corresponding Source need not include anything that users |  | ||||||
| can regenerate automatically from other parts of the Corresponding |  | ||||||
| Source. |  | ||||||
|  |  | ||||||
|   The Corresponding Source for a work in source code form is that |  | ||||||
| same work. |  | ||||||
|  |  | ||||||
|   2. Basic Permissions. |  | ||||||
|  |  | ||||||
|   All rights granted under this License are granted for the term of |  | ||||||
| copyright on the Program, and are irrevocable provided the stated |  | ||||||
| conditions are met.  This License explicitly affirms your unlimited |  | ||||||
| permission to run the unmodified Program.  The output from running a |  | ||||||
| covered work is covered by this License only if the output, given its |  | ||||||
| content, constitutes a covered work.  This License acknowledges your |  | ||||||
| rights of fair use or other equivalent, as provided by copyright law. |  | ||||||
|  |  | ||||||
|   You may make, run and propagate covered works that you do not |  | ||||||
| convey, without conditions so long as your license otherwise remains |  | ||||||
| in force.  You may convey covered works to others for the sole purpose |  | ||||||
| of having them make modifications exclusively for you, or provide you |  | ||||||
| with facilities for running those works, provided that you comply with |  | ||||||
| the terms of this License in conveying all material for which you do |  | ||||||
| not control copyright.  Those thus making or running the covered works |  | ||||||
| for you must do so exclusively on your behalf, under your direction |  | ||||||
| and control, on terms that prohibit them from making any copies of |  | ||||||
| your copyrighted material outside their relationship with you. |  | ||||||
|  |  | ||||||
|   Conveying under any other circumstances is permitted solely under |  | ||||||
| the conditions stated below.  Sublicensing is not allowed; section 10 |  | ||||||
| makes it unnecessary. |  | ||||||
|  |  | ||||||
|   3. Protecting Users' Legal Rights From Anti-Circumvention Law. |  | ||||||
|  |  | ||||||
|   No covered work shall be deemed part of an effective technological |  | ||||||
| measure under any applicable law fulfilling obligations under article |  | ||||||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or |  | ||||||
| similar laws prohibiting or restricting circumvention of such |  | ||||||
| measures. |  | ||||||
|  |  | ||||||
|   When you convey a covered work, you waive any legal power to forbid |  | ||||||
| circumvention of technological measures to the extent such circumvention |  | ||||||
| is effected by exercising rights under this License with respect to |  | ||||||
| the covered work, and you disclaim any intention to limit operation or |  | ||||||
| modification of the work as a means of enforcing, against the work's |  | ||||||
| users, your or third parties' legal rights to forbid circumvention of |  | ||||||
| technological measures. |  | ||||||
|  |  | ||||||
|   4. Conveying Verbatim Copies. |  | ||||||
|  |  | ||||||
|   You may convey verbatim copies of the Program's source code as you |  | ||||||
| receive it, in any medium, provided that you conspicuously and |  | ||||||
| appropriately publish on each copy an appropriate copyright notice; |  | ||||||
| keep intact all notices stating that this License and any |  | ||||||
| non-permissive terms added in accord with section 7 apply to the code; |  | ||||||
| keep intact all notices of the absence of any warranty; and give all |  | ||||||
| recipients a copy of this License along with the Program. |  | ||||||
|  |  | ||||||
|   You may charge any price or no price for each copy that you convey, |  | ||||||
| and you may offer support or warranty protection for a fee. |  | ||||||
|  |  | ||||||
|   5. Conveying Modified Source Versions. |  | ||||||
|  |  | ||||||
|   You may convey a work based on the Program, or the modifications to |  | ||||||
| produce it from the Program, in the form of source code under the |  | ||||||
| terms of section 4, provided that you also meet all of these conditions: |  | ||||||
|  |  | ||||||
|     a) The work must carry prominent notices stating that you modified |  | ||||||
|     it, and giving a relevant date. |  | ||||||
|  |  | ||||||
|     b) The work must carry prominent notices stating that it is |  | ||||||
|     released under this License and any conditions added under section |  | ||||||
|     7.  This requirement modifies the requirement in section 4 to |  | ||||||
|     "keep intact all notices". |  | ||||||
|  |  | ||||||
|     c) You must license the entire work, as a whole, under this |  | ||||||
|     License to anyone who comes into possession of a copy.  This |  | ||||||
|     License will therefore apply, along with any applicable section 7 |  | ||||||
|     additional terms, to the whole of the work, and all its parts, |  | ||||||
|     regardless of how they are packaged.  This License gives no |  | ||||||
|     permission to license the work in any other way, but it does not |  | ||||||
|     invalidate such permission if you have separately received it. |  | ||||||
|  |  | ||||||
|     d) If the work has interactive user interfaces, each must display |  | ||||||
|     Appropriate Legal Notices; however, if the Program has interactive |  | ||||||
|     interfaces that do not display Appropriate Legal Notices, your |  | ||||||
|     work need not make them do so. |  | ||||||
|  |  | ||||||
|   A compilation of a covered work with other separate and independent |  | ||||||
| works, which are not by their nature extensions of the covered work, |  | ||||||
| and which are not combined with it such as to form a larger program, |  | ||||||
| in or on a volume of a storage or distribution medium, is called an |  | ||||||
| "aggregate" if the compilation and its resulting copyright are not |  | ||||||
| used to limit the access or legal rights of the compilation's users |  | ||||||
| beyond what the individual works permit.  Inclusion of a covered work |  | ||||||
| in an aggregate does not cause this License to apply to the other |  | ||||||
| parts of the aggregate. |  | ||||||
|  |  | ||||||
|   6. Conveying Non-Source Forms. |  | ||||||
|  |  | ||||||
|   You may convey a covered work in object code form under the terms |  | ||||||
| of sections 4 and 5, provided that you also convey the |  | ||||||
| machine-readable Corresponding Source under the terms of this License, |  | ||||||
| in one of these ways: |  | ||||||
|  |  | ||||||
|     a) Convey the object code in, or embodied in, a physical product |  | ||||||
|     (including a physical distribution medium), accompanied by the |  | ||||||
|     Corresponding Source fixed on a durable physical medium |  | ||||||
|     customarily used for software interchange. |  | ||||||
|  |  | ||||||
|     b) Convey the object code in, or embodied in, a physical product |  | ||||||
|     (including a physical distribution medium), accompanied by a |  | ||||||
|     written offer, valid for at least three years and valid for as |  | ||||||
|     long as you offer spare parts or customer support for that product |  | ||||||
|     model, to give anyone who possesses the object code either (1) a |  | ||||||
|     copy of the Corresponding Source for all the software in the |  | ||||||
|     product that is covered by this License, on a durable physical |  | ||||||
|     medium customarily used for software interchange, for a price no |  | ||||||
|     more than your reasonable cost of physically performing this |  | ||||||
|     conveying of source, or (2) access to copy the |  | ||||||
|     Corresponding Source from a network server at no charge. |  | ||||||
|  |  | ||||||
|     c) Convey individual copies of the object code with a copy of the |  | ||||||
|     written offer to provide the Corresponding Source.  This |  | ||||||
|     alternative is allowed only occasionally and noncommercially, and |  | ||||||
|     only if you received the object code with such an offer, in accord |  | ||||||
|     with subsection 6b. |  | ||||||
|  |  | ||||||
|     d) Convey the object code by offering access from a designated |  | ||||||
|     place (gratis or for a charge), and offer equivalent access to the |  | ||||||
|     Corresponding Source in the same way through the same place at no |  | ||||||
|     further charge.  You need not require recipients to copy the |  | ||||||
|     Corresponding Source along with the object code.  If the place to |  | ||||||
|     copy the object code is a network server, the Corresponding Source |  | ||||||
|     may be on a different server (operated by you or a third party) |  | ||||||
|     that supports equivalent copying facilities, provided you maintain |  | ||||||
|     clear directions next to the object code saying where to find the |  | ||||||
|     Corresponding Source.  Regardless of what server hosts the |  | ||||||
|     Corresponding Source, you remain obligated to ensure that it is |  | ||||||
|     available for as long as needed to satisfy these requirements. |  | ||||||
|  |  | ||||||
|     e) Convey the object code using peer-to-peer transmission, provided |  | ||||||
|     you inform other peers where the object code and Corresponding |  | ||||||
|     Source of the work are being offered to the general public at no |  | ||||||
|     charge under subsection 6d. |  | ||||||
|  |  | ||||||
|   A separable portion of the object code, whose source code is excluded |  | ||||||
| from the Corresponding Source as a System Library, need not be |  | ||||||
| included in conveying the object code work. |  | ||||||
|  |  | ||||||
|   A "User Product" is either (1) a "consumer product", which means any |  | ||||||
| tangible personal property which is normally used for personal, family, |  | ||||||
| or household purposes, or (2) anything designed or sold for incorporation |  | ||||||
| into a dwelling.  In determining whether a product is a consumer product, |  | ||||||
| doubtful cases shall be resolved in favor of coverage.  For a particular |  | ||||||
| product received by a particular user, "normally used" refers to a |  | ||||||
| typical or common use of that class of product, regardless of the status |  | ||||||
| of the particular user or of the way in which the particular user |  | ||||||
| actually uses, or expects or is expected to use, the product.  A product |  | ||||||
| is a consumer product regardless of whether the product has substantial |  | ||||||
| commercial, industrial or non-consumer uses, unless such uses represent |  | ||||||
| the only significant mode of use of the product. |  | ||||||
|  |  | ||||||
|   "Installation Information" for a User Product means any methods, |  | ||||||
| procedures, authorization keys, or other information required to install |  | ||||||
| and execute modified versions of a covered work in that User Product from |  | ||||||
| a modified version of its Corresponding Source.  The information must |  | ||||||
| suffice to ensure that the continued functioning of the modified object |  | ||||||
| code is in no case prevented or interfered with solely because |  | ||||||
| modification has been made. |  | ||||||
|  |  | ||||||
|   If you convey an object code work under this section in, or with, or |  | ||||||
| specifically for use in, a User Product, and the conveying occurs as |  | ||||||
| part of a transaction in which the right of possession and use of the |  | ||||||
| User Product is transferred to the recipient in perpetuity or for a |  | ||||||
| fixed term (regardless of how the transaction is characterized), the |  | ||||||
| Corresponding Source conveyed under this section must be accompanied |  | ||||||
| by the Installation Information.  But this requirement does not apply |  | ||||||
| if neither you nor any third party retains the ability to install |  | ||||||
| modified object code on the User Product (for example, the work has |  | ||||||
| been installed in ROM). |  | ||||||
|  |  | ||||||
|   The requirement to provide Installation Information does not include a |  | ||||||
| requirement to continue to provide support service, warranty, or updates |  | ||||||
| for a work that has been modified or installed by the recipient, or for |  | ||||||
| the User Product in which it has been modified or installed.  Access to a |  | ||||||
| network may be denied when the modification itself materially and |  | ||||||
| adversely affects the operation of the network or violates the rules and |  | ||||||
| protocols for communication across the network. |  | ||||||
|  |  | ||||||
|   Corresponding Source conveyed, and Installation Information provided, |  | ||||||
| in accord with this section must be in a format that is publicly |  | ||||||
| documented (and with an implementation available to the public in |  | ||||||
| source code form), and must require no special password or key for |  | ||||||
| unpacking, reading or copying. |  | ||||||
|  |  | ||||||
|   7. Additional Terms. |  | ||||||
|  |  | ||||||
|   "Additional permissions" are terms that supplement the terms of this |  | ||||||
| License by making exceptions from one or more of its conditions. |  | ||||||
| Additional permissions that are applicable to the entire Program shall |  | ||||||
| be treated as though they were included in this License, to the extent |  | ||||||
| that they are valid under applicable law.  If additional permissions |  | ||||||
| apply only to part of the Program, that part may be used separately |  | ||||||
| under those permissions, but the entire Program remains governed by |  | ||||||
| this License without regard to the additional permissions. |  | ||||||
|  |  | ||||||
|   When you convey a copy of a covered work, you may at your option |  | ||||||
| remove any additional permissions from that copy, or from any part of |  | ||||||
| it.  (Additional permissions may be written to require their own |  | ||||||
| removal in certain cases when you modify the work.)  You may place |  | ||||||
| additional permissions on material, added by you to a covered work, |  | ||||||
| for which you have or can give appropriate copyright permission. |  | ||||||
|  |  | ||||||
|   Notwithstanding any other provision of this License, for material you |  | ||||||
| add to a covered work, you may (if authorized by the copyright holders of |  | ||||||
| that material) supplement the terms of this License with terms: |  | ||||||
|  |  | ||||||
|     a) Disclaiming warranty or limiting liability differently from the |  | ||||||
|     terms of sections 15 and 16 of this License; or |  | ||||||
|  |  | ||||||
|     b) Requiring preservation of specified reasonable legal notices or |  | ||||||
|     author attributions in that material or in the Appropriate Legal |  | ||||||
|     Notices displayed by works containing it; or |  | ||||||
|  |  | ||||||
|     c) Prohibiting misrepresentation of the origin of that material, or |  | ||||||
|     requiring that modified versions of such material be marked in |  | ||||||
|     reasonable ways as different from the original version; or |  | ||||||
|  |  | ||||||
|     d) Limiting the use for publicity purposes of names of licensors or |  | ||||||
|     authors of the material; or |  | ||||||
|  |  | ||||||
|     e) Declining to grant rights under trademark law for use of some |  | ||||||
|     trade names, trademarks, or service marks; or |  | ||||||
|  |  | ||||||
|     f) Requiring indemnification of licensors and authors of that |  | ||||||
|     material by anyone who conveys the material (or modified versions of |  | ||||||
|     it) with contractual assumptions of liability to the recipient, for |  | ||||||
|     any liability that these contractual assumptions directly impose on |  | ||||||
|     those licensors and authors. |  | ||||||
|  |  | ||||||
|   All other non-permissive additional terms are considered "further |  | ||||||
| restrictions" within the meaning of section 10.  If the Program as you |  | ||||||
| received it, or any part of it, contains a notice stating that it is |  | ||||||
| governed by this License along with a term that is a further |  | ||||||
| restriction, you may remove that term.  If a license document contains |  | ||||||
| a further restriction but permits relicensing or conveying under this |  | ||||||
| License, you may add to a covered work material governed by the terms |  | ||||||
| of that license document, provided that the further restriction does |  | ||||||
| not survive such relicensing or conveying. |  | ||||||
|  |  | ||||||
|   If you add terms to a covered work in accord with this section, you |  | ||||||
| must place, in the relevant source files, a statement of the |  | ||||||
| additional terms that apply to those files, or a notice indicating |  | ||||||
| where to find the applicable terms. |  | ||||||
|  |  | ||||||
|   Additional terms, permissive or non-permissive, may be stated in the |  | ||||||
| form of a separately written license, or stated as exceptions; |  | ||||||
| the above requirements apply either way. |  | ||||||
|  |  | ||||||
|   8. Termination. |  | ||||||
|  |  | ||||||
|   You may not propagate or modify a covered work except as expressly |  | ||||||
| provided under this License.  Any attempt otherwise to propagate or |  | ||||||
| modify it is void, and will automatically terminate your rights under |  | ||||||
| this License (including any patent licenses granted under the third |  | ||||||
| paragraph of section 11). |  | ||||||
|  |  | ||||||
|   However, if you cease all violation of this License, then your |  | ||||||
| license from a particular copyright holder is reinstated (a) |  | ||||||
| provisionally, unless and until the copyright holder explicitly and |  | ||||||
| finally terminates your license, and (b) permanently, if the copyright |  | ||||||
| holder fails to notify you of the violation by some reasonable means |  | ||||||
| prior to 60 days after the cessation. |  | ||||||
|  |  | ||||||
|   Moreover, your license from a particular copyright holder is |  | ||||||
| reinstated permanently if the copyright holder notifies you of the |  | ||||||
| violation by some reasonable means, this is the first time you have |  | ||||||
| received notice of violation of this License (for any work) from that |  | ||||||
| copyright holder, and you cure the violation prior to 30 days after |  | ||||||
| your receipt of the notice. |  | ||||||
|  |  | ||||||
|   Termination of your rights under this section does not terminate the |  | ||||||
| licenses of parties who have received copies or rights from you under |  | ||||||
| this License.  If your rights have been terminated and not permanently |  | ||||||
| reinstated, you do not qualify to receive new licenses for the same |  | ||||||
| material under section 10. |  | ||||||
|  |  | ||||||
|   9. Acceptance Not Required for Having Copies. |  | ||||||
|  |  | ||||||
|   You are not required to accept this License in order to receive or |  | ||||||
| run a copy of the Program.  Ancillary propagation of a covered work |  | ||||||
| occurring solely as a consequence of using peer-to-peer transmission |  | ||||||
| to receive a copy likewise does not require acceptance.  However, |  | ||||||
| nothing other than this License grants you permission to propagate or |  | ||||||
| modify any covered work.  These actions infringe copyright if you do |  | ||||||
| not accept this License.  Therefore, by modifying or propagating a |  | ||||||
| covered work, you indicate your acceptance of this License to do so. |  | ||||||
|  |  | ||||||
|   10. Automatic Licensing of Downstream Recipients. |  | ||||||
|  |  | ||||||
|   Each time you convey a covered work, the recipient automatically |  | ||||||
| receives a license from the original licensors, to run, modify and |  | ||||||
| propagate that work, subject to this License.  You are not responsible |  | ||||||
| for enforcing compliance by third parties with this License. |  | ||||||
|  |  | ||||||
|   An "entity transaction" is a transaction transferring control of an |  | ||||||
| organization, or substantially all assets of one, or subdividing an |  | ||||||
| organization, or merging organizations.  If propagation of a covered |  | ||||||
| work results from an entity transaction, each party to that |  | ||||||
| transaction who receives a copy of the work also receives whatever |  | ||||||
| licenses to the work the party's predecessor in interest had or could |  | ||||||
| give under the previous paragraph, plus a right to possession of the |  | ||||||
| Corresponding Source of the work from the predecessor in interest, if |  | ||||||
| the predecessor has it or can get it with reasonable efforts. |  | ||||||
|  |  | ||||||
|   You may not impose any further restrictions on the exercise of the |  | ||||||
| rights granted or affirmed under this License.  For example, you may |  | ||||||
| not impose a license fee, royalty, or other charge for exercise of |  | ||||||
| rights granted under this License, and you may not initiate litigation |  | ||||||
| (including a cross-claim or counterclaim in a lawsuit) alleging that |  | ||||||
| any patent claim is infringed by making, using, selling, offering for |  | ||||||
| sale, or importing the Program or any portion of it. |  | ||||||
|  |  | ||||||
|   11. Patents. |  | ||||||
|  |  | ||||||
|   A "contributor" is a copyright holder who authorizes use under this |  | ||||||
| License of the Program or a work on which the Program is based.  The |  | ||||||
| work thus licensed is called the contributor's "contributor version". |  | ||||||
|  |  | ||||||
|   A contributor's "essential patent claims" are all patent claims |  | ||||||
| owned or controlled by the contributor, whether already acquired or |  | ||||||
| hereafter acquired, that would be infringed by some manner, permitted |  | ||||||
| by this License, of making, using, or selling its contributor version, |  | ||||||
| but do not include claims that would be infringed only as a |  | ||||||
| consequence of further modification of the contributor version.  For |  | ||||||
| purposes of this definition, "control" includes the right to grant |  | ||||||
| patent sublicenses in a manner consistent with the requirements of |  | ||||||
| this License. |  | ||||||
|  |  | ||||||
|   Each contributor grants you a non-exclusive, worldwide, royalty-free |  | ||||||
| patent license under the contributor's essential patent claims, to |  | ||||||
| make, use, sell, offer for sale, import and otherwise run, modify and |  | ||||||
| propagate the contents of its contributor version. |  | ||||||
|  |  | ||||||
|   In the following three paragraphs, a "patent license" is any express |  | ||||||
| agreement or commitment, however denominated, not to enforce a patent |  | ||||||
| (such as an express permission to practice a patent or covenant not to |  | ||||||
| sue for patent infringement).  To "grant" such a patent license to a |  | ||||||
| party means to make such an agreement or commitment not to enforce a |  | ||||||
| patent against the party. |  | ||||||
|  |  | ||||||
|   If you convey a covered work, knowingly relying on a patent license, |  | ||||||
| and the Corresponding Source of the work is not available for anyone |  | ||||||
| to copy, free of charge and under the terms of this License, through a |  | ||||||
| publicly available network server or other readily accessible means, |  | ||||||
| then you must either (1) cause the Corresponding Source to be so |  | ||||||
| available, or (2) arrange to deprive yourself of the benefit of the |  | ||||||
| patent license for this particular work, or (3) arrange, in a manner |  | ||||||
| consistent with the requirements of this License, to extend the patent |  | ||||||
| license to downstream recipients.  "Knowingly relying" means you have |  | ||||||
| actual knowledge that, but for the patent license, your conveying the |  | ||||||
| covered work in a country, or your recipient's use of the covered work |  | ||||||
| in a country, would infringe one or more identifiable patents in that |  | ||||||
| country that you have reason to believe are valid. |  | ||||||
|  |  | ||||||
|   If, pursuant to or in connection with a single transaction or |  | ||||||
| arrangement, you convey, or propagate by procuring conveyance of, a |  | ||||||
| covered work, and grant a patent license to some of the parties |  | ||||||
| receiving the covered work authorizing them to use, propagate, modify |  | ||||||
| or convey a specific copy of the covered work, then the patent license |  | ||||||
| you grant is automatically extended to all recipients of the covered |  | ||||||
| work and works based on it. |  | ||||||
|  |  | ||||||
|   A patent license is "discriminatory" if it does not include within |  | ||||||
| the scope of its coverage, prohibits the exercise of, or is |  | ||||||
| conditioned on the non-exercise of one or more of the rights that are |  | ||||||
| specifically granted under this License.  You may not convey a covered |  | ||||||
| work if you are a party to an arrangement with a third party that is |  | ||||||
| in the business of distributing software, under which you make payment |  | ||||||
| to the third party based on the extent of your activity of conveying |  | ||||||
| the work, and under which the third party grants, to any of the |  | ||||||
| parties who would receive the covered work from you, a discriminatory |  | ||||||
| patent license (a) in connection with copies of the covered work |  | ||||||
| conveyed by you (or copies made from those copies), or (b) primarily |  | ||||||
| for and in connection with specific products or compilations that |  | ||||||
| contain the covered work, unless you entered into that arrangement, |  | ||||||
| or that patent license was granted, prior to 28 March 2007. |  | ||||||
|  |  | ||||||
|   Nothing in this License shall be construed as excluding or limiting |  | ||||||
| any implied license or other defenses to infringement that may |  | ||||||
| otherwise be available to you under applicable patent law. |  | ||||||
|  |  | ||||||
|   12. No Surrender of Others' Freedom. |  | ||||||
|  |  | ||||||
|   If conditions are imposed on you (whether by court order, agreement or |  | ||||||
| otherwise) that contradict the conditions of this License, they do not |  | ||||||
| excuse you from the conditions of this License.  If you cannot convey a |  | ||||||
| covered work so as to satisfy simultaneously your obligations under this |  | ||||||
| License and any other pertinent obligations, then as a consequence you may |  | ||||||
| not convey it at all.  For example, if you agree to terms that obligate you |  | ||||||
| to collect a royalty for further conveying from those to whom you convey |  | ||||||
| the Program, the only way you could satisfy both those terms and this |  | ||||||
| License would be to refrain entirely from conveying the Program. |  | ||||||
|  |  | ||||||
|   13. Use with the GNU Affero General Public License. |  | ||||||
|  |  | ||||||
|   Notwithstanding any other provision of this License, you have |  | ||||||
| permission to link or combine any covered work with a work licensed |  | ||||||
| under version 3 of the GNU Affero General Public License into a single |  | ||||||
| combined work, and to convey the resulting work.  The terms of this |  | ||||||
| License will continue to apply to the part which is the covered work, |  | ||||||
| but the special requirements of the GNU Affero General Public License, |  | ||||||
| section 13, concerning interaction through a network will apply to the |  | ||||||
| combination as such. |  | ||||||
|  |  | ||||||
|   14. Revised Versions of this License. |  | ||||||
|  |  | ||||||
|   The Free Software Foundation may publish revised and/or new versions of |  | ||||||
| the GNU General Public License from time to time.  Such new versions will |  | ||||||
| be similar in spirit to the present version, but may differ in detail to |  | ||||||
| address new problems or concerns. |  | ||||||
|  |  | ||||||
|   Each version is given a distinguishing version number.  If the |  | ||||||
| Program specifies that a certain numbered version of the GNU General |  | ||||||
| Public License "or any later version" applies to it, you have the |  | ||||||
| option of following the terms and conditions either of that numbered |  | ||||||
| version or of any later version published by the Free Software |  | ||||||
| Foundation.  If the Program does not specify a version number of the |  | ||||||
| GNU General Public License, you may choose any version ever published |  | ||||||
| by the Free Software Foundation. |  | ||||||
|  |  | ||||||
|   If the Program specifies that a proxy can decide which future |  | ||||||
| versions of the GNU General Public License can be used, that proxy's |  | ||||||
| public statement of acceptance of a version permanently authorizes you |  | ||||||
| to choose that version for the Program. |  | ||||||
|  |  | ||||||
|   Later license versions may give you additional or different |  | ||||||
| permissions.  However, no additional obligations are imposed on any |  | ||||||
| author or copyright holder as a result of your choosing to follow a |  | ||||||
| later version. |  | ||||||
|  |  | ||||||
|   15. Disclaimer of Warranty. |  | ||||||
|  |  | ||||||
|   THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY |  | ||||||
| APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT |  | ||||||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY |  | ||||||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, |  | ||||||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |  | ||||||
| PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM |  | ||||||
| IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF |  | ||||||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. |  | ||||||
|  |  | ||||||
|   16. Limitation of Liability. |  | ||||||
|  |  | ||||||
|   IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |  | ||||||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |  | ||||||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY |  | ||||||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE |  | ||||||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF |  | ||||||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD |  | ||||||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), |  | ||||||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |  | ||||||
| SUCH DAMAGES. |  | ||||||
|  |  | ||||||
|   17. Interpretation of Sections 15 and 16. |  | ||||||
|  |  | ||||||
|   If the disclaimer of warranty and limitation of liability provided |  | ||||||
| above cannot be given local legal effect according to their terms, |  | ||||||
| reviewing courts shall apply local law that most closely approximates |  | ||||||
| an absolute waiver of all civil liability in connection with the |  | ||||||
| Program, unless a warranty or assumption of liability accompanies a |  | ||||||
| copy of the Program in return for a fee. |  | ||||||
|  |  | ||||||
|                      END OF TERMS AND CONDITIONS |  | ||||||
|  |  | ||||||
|             How to Apply These Terms to Your New Programs |  | ||||||
|  |  | ||||||
|   If you develop a new program, and you want it to be of the greatest |  | ||||||
| possible use to the public, the best way to achieve this is to make it |  | ||||||
| free software which everyone can redistribute and change under these terms. |  | ||||||
|  |  | ||||||
|   To do so, attach the following notices to the program.  It is safest |  | ||||||
| to attach them to the start of each source file to most effectively |  | ||||||
| state the exclusion of warranty; and each file should have at least |  | ||||||
| the "copyright" line and a pointer to where the full notice is found. |  | ||||||
|  |  | ||||||
|     {one line to give the program's name and a brief idea of what it does.} |  | ||||||
|     Copyright (C) {year}  {name of author} |  | ||||||
|  |  | ||||||
|     This program is free software: you can redistribute it and/or modify |  | ||||||
|     it under the terms of the GNU General Public License as published by |  | ||||||
|     the Free Software Foundation, either version 3 of the License, or |  | ||||||
|     (at your option) any later version. |  | ||||||
|  |  | ||||||
|     This program is distributed in the hope that it will be useful, |  | ||||||
|     but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|     GNU General Public License for more details. |  | ||||||
|  |  | ||||||
|     You should have received a copy of the GNU General Public License |  | ||||||
|     along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  |  | ||||||
| Also add information on how to contact you by electronic and paper mail. |  | ||||||
|  |  | ||||||
|   If the program does terminal interaction, make it output a short |  | ||||||
| notice like this when it starts in an interactive mode: |  | ||||||
|  |  | ||||||
|     {project}  Copyright (C) {year}  {fullname} |  | ||||||
|     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |  | ||||||
|     This is free software, and you are welcome to redistribute it |  | ||||||
|     under certain conditions; type `show c' for details. |  | ||||||
|  |  | ||||||
| The hypothetical commands `show w' and `show c' should show the appropriate |  | ||||||
| parts of the General Public License.  Of course, your program's commands |  | ||||||
| might be different; for a GUI interface, you would use an "about box". |  | ||||||
|  |  | ||||||
|   You should also get your employer (if you work as a programmer) or school, |  | ||||||
| if any, to sign a "copyright disclaimer" for the program, if necessary. |  | ||||||
| For more information on this, and how to apply and follow the GNU GPL, see |  | ||||||
| <http://www.gnu.org/licenses/>. |  | ||||||
|  |  | ||||||
|   The GNU General Public License does not permit incorporating your program |  | ||||||
| into proprietary programs.  If your program is a subroutine library, you |  | ||||||
| may consider it more useful to permit linking proprietary applications with |  | ||||||
| the library.  If this is what you want to do, use the GNU Lesser General |  | ||||||
| Public License instead of this License.  But first, please read |  | ||||||
| <http://www.gnu.org/philosophy/why-not-lgpl.html>. |  | ||||||
| @@ -1 +1,3 @@ | |||||||
| # Project moved to https://github.com/aminecmi/ReaderforSelfoss-multiplatform | # Multiplatform version [here](https://gitea.amine-louveau.fr/Amine_L/ReaderForSelfoss-multiplatform) | ||||||
|  |  | ||||||
|  | # Original moved  [here](https://gitea.amine-louveau.fr/Amine_L/ReaderforSelfoss) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +0,0 @@ | |||||||
| /build |  | ||||||
							
								
								
									
										172
									
								
								app/build.gradle
									
									
									
									
									
								
							
							
						
						
									
										172
									
								
								app/build.gradle
									
									
									
									
									
								
							| @@ -1,172 +0,0 @@ | |||||||
| buildscript { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| def gitVersion() { |  | ||||||
|     def process |  | ||||||
|     def maybeTagOfCurrentCommit = 'git describe --contains HEAD'.execute() |  | ||||||
|     if (maybeTagOfCurrentCommit.text.isEmpty()) { |  | ||||||
|         println "No tag on current commit. Will take the latest one." |  | ||||||
|         process = "git for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1".execute() |  | ||||||
|     } else { |  | ||||||
|         println "Tag found on current commit" |  | ||||||
|         process = 'git describe --contains HEAD'.execute() |  | ||||||
|     } |  | ||||||
|     return process.text.replaceAll("'", "").substring(1).replaceAll("\\.", "").trim() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| def versionCodeFromGit() { |  | ||||||
|     println "version code " + gitVersion() |  | ||||||
|     return gitVersion().toInteger() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| def versionNameFromGit() { |  | ||||||
|     println "version name " + gitVersion() |  | ||||||
|     return gitVersion() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| apply plugin: 'com.android.application' |  | ||||||
|  |  | ||||||
| apply plugin: 'kotlin-android' |  | ||||||
|  |  | ||||||
| apply plugin: 'kotlin-kapt' |  | ||||||
|  |  | ||||||
| android { |  | ||||||
|     compileOptions { |  | ||||||
|         // Flag to enable support for the new language APIs |  | ||||||
|         coreLibraryDesugaringEnabled true |  | ||||||
|  |  | ||||||
|         sourceCompatibility JavaVersion.VERSION_1_8 |  | ||||||
|         targetCompatibility JavaVersion.VERSION_1_8 |  | ||||||
|     } |  | ||||||
|     compileSdkVersion 31 |  | ||||||
|     buildToolsVersion '31.0.0' |  | ||||||
|     buildFeatures { |  | ||||||
|         viewBinding true |  | ||||||
|     } |  | ||||||
|     defaultConfig { |  | ||||||
|         applicationId "apps.amine.bou.readerforselfoss" |  | ||||||
|         minSdkVersion 21 |  | ||||||
|         targetSdkVersion 31 |  | ||||||
|         versionCode versionCodeFromGit() |  | ||||||
|         versionName versionNameFromGit() |  | ||||||
|  |  | ||||||
|         // Enabling multidex support. |  | ||||||
|         multiDexEnabled true |  | ||||||
|         lintOptions { |  | ||||||
|             abortOnError true |  | ||||||
|             disable 'InvalidPackage' |  | ||||||
|         } |  | ||||||
|         vectorDrawables.useSupportLibrary = true |  | ||||||
|  |  | ||||||
|         // tests |  | ||||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |  | ||||||
|  |  | ||||||
|         javaCompileOptions { |  | ||||||
|             annotationProcessorOptions { |  | ||||||
|                 arguments = ["room.schemaLocation": |  | ||||||
|                                      "$projectDir/schemas".toString()] |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     buildTypes { |  | ||||||
|         release { |  | ||||||
|             minifyEnabled true |  | ||||||
|             shrinkResources true |  | ||||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), |  | ||||||
|                     'proguard-rules.pro' |  | ||||||
|         } |  | ||||||
|         debug { |  | ||||||
|             buildConfigField "String", "LOGIN_URL", appLoginUrl |  | ||||||
|             buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword |  | ||||||
|             buildConfigField "String", "LOGIN_USERNAME", appLoginUsername |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     flavorDimensions "build" |  | ||||||
|     productFlavors { |  | ||||||
|         githubConfig { |  | ||||||
|             versionNameSuffix '-github' |  | ||||||
|             dimension "build" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     kotlinOptions { |  | ||||||
|         jvmTarget = '1.8' |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dependencies { |  | ||||||
|     implementation 'androidx.preference:preference-ktx:1.1.1' |  | ||||||
|  |  | ||||||
|     // Testing |  | ||||||
|     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02' |  | ||||||
|     androidTestImplementation 'androidx.test:runner:1.3.1-alpha02' |  | ||||||
|     // Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource |  | ||||||
|     androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0-alpha02' |  | ||||||
|     // Espresso-intents for validation and stubbing of Intents |  | ||||||
|     androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0-alpha02' |  | ||||||
|     implementation fileTree(include: ['*.jar'], dir: 'libs') |  | ||||||
|  |  | ||||||
|     // Android Support |  | ||||||
|     implementation 'androidx.appcompat:appcompat:1.4.1' |  | ||||||
|     implementation 'com.google.android.material:material:1.5.0' |  | ||||||
|     implementation 'androidx.recyclerview:recyclerview:1.3.0-alpha01' |  | ||||||
|     implementation "androidx.legacy:legacy-support-v4:$android_version" |  | ||||||
|     implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02' |  | ||||||
|     implementation 'androidx.browser:browser:1.4.0' |  | ||||||
|     implementation "androidx.cardview:cardview:$android_version" |  | ||||||
|     implementation 'androidx.annotation:annotation:1.3.0' |  | ||||||
|     implementation 'androidx.work:work-runtime-ktx:2.7.1' |  | ||||||
|     implementation 'androidx.constraintlayout:constraintlayout:2.1.3' |  | ||||||
|     implementation 'org.jsoup:jsoup:1.14.3' |  | ||||||
|  |  | ||||||
|     coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") |  | ||||||
|  |  | ||||||
|     //multidex |  | ||||||
|     implementation 'androidx.multidex:multidex:2.0.1' |  | ||||||
|  |  | ||||||
|     // About |  | ||||||
|     implementation 'com.mikepenz:aboutlibraries-core:8.9.4' |  | ||||||
|     implementation 'com.mikepenz:aboutlibraries:8.9.4' |  | ||||||
|     implementation "com.mikepenz:aboutlibraries-definitions:8.9.4" |  | ||||||
|  |  | ||||||
|     // Async |  | ||||||
|     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' |  | ||||||
|  |  | ||||||
|     // Retrofit + http logging + okhttp |  | ||||||
|     implementation 'com.squareup.retrofit2:retrofit:2.9.0' |  | ||||||
|     implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3' |  | ||||||
|     implementation 'com.squareup.retrofit2:converter-gson:2.9.0' |  | ||||||
|     implementation 'com.burgstaller:okhttp-digest:2.5' |  | ||||||
|  |  | ||||||
|     // Material-ish things |  | ||||||
|     implementation 'com.ashokvarma.android:bottom-navigation-bar:2.2.0' |  | ||||||
|     implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' |  | ||||||
|  |  | ||||||
|     // glide |  | ||||||
|     kapt 'com.github.bumptech.glide:compiler:4.11.0' |  | ||||||
|     implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1' |  | ||||||
|  |  | ||||||
|     // Drawer |  | ||||||
|     implementation 'com.mikepenz:materialdrawer:8.4.5' |  | ||||||
|  |  | ||||||
|     // Themes |  | ||||||
|     implementation 'com.52inc:scoops:1.0.0' |  | ||||||
|     implementation 'com.jaredrummler:colorpicker:1.1.0' |  | ||||||
|     implementation 'com.github.rubensousa:floatingtoolbar:1.5.1' |  | ||||||
|  |  | ||||||
|     // Pager |  | ||||||
|     implementation 'me.relex:circleindicator:2.1.6' |  | ||||||
|     implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" |  | ||||||
|  |  | ||||||
|     //PhotoView |  | ||||||
|     implementation 'com.github.chrisbanes:PhotoView:2.3.0' |  | ||||||
|  |  | ||||||
|     implementation 'androidx.core:core-ktx:1.7.0' |  | ||||||
|  |  | ||||||
|     implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0' |  | ||||||
|     implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0' |  | ||||||
|  |  | ||||||
|     implementation "androidx.room:room-ktx:2.4.0-beta01" |  | ||||||
|     kapt "androidx.room:room-compiler:2.4.0-beta01" |  | ||||||
|  |  | ||||||
|     implementation "android.arch.work:work-runtime-ktx:$work_version" |  | ||||||
| } |  | ||||||
							
								
								
									
										65
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										65
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							| @@ -1,65 +0,0 @@ | |||||||
| # Add project specific ProGuard rules here. |  | ||||||
| # By default, the flags in this file are appended to flags specified |  | ||||||
| # in /home/amine/apps/android-sdk-linux/tools/proguard/proguard-android.txt |  | ||||||
| # You can edit the include path and order by changing the proguardFiles |  | ||||||
| # directive in build.gradle. |  | ||||||
| # |  | ||||||
| # For more details, see |  | ||||||
| #   http://developer.android.com/guide/developing/tools/proguard.html |  | ||||||
|  |  | ||||||
| # Add any project specific keep options here: |  | ||||||
|  |  | ||||||
| # If your project uses WebView with JS, uncomment the following |  | ||||||
| # and specify the fully qualified class name to the JavaScript interface |  | ||||||
| # class: |  | ||||||
| #-keepclassmembers class fqcn.of.javascript.interface.for.webview { |  | ||||||
| #   public *; |  | ||||||
| #} |  | ||||||
|  |  | ||||||
| # Uncomment this to preserve the line number information for |  | ||||||
| # debugging stack traces. |  | ||||||
| #-keepattributes SourceFile,LineNumberTable |  | ||||||
|  |  | ||||||
| # If you keep the line number information, uncomment this to |  | ||||||
| # hide the original source file name. |  | ||||||
| #-renamesourcefileattribute SourceFile |  | ||||||
|  |  | ||||||
| #About libraries |  | ||||||
| -keep class .R |  | ||||||
| -keep class **.R$* { |  | ||||||
|     <fields>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| -dontwarn okio.** |  | ||||||
| -dontwarn retrofit2.Platform$Java8 |  | ||||||
| -keep class retrofit.** { *; } |  | ||||||
| -keepclasseswithmembers class * { |  | ||||||
|     @retrofit.http.* <methods>; |  | ||||||
| } |  | ||||||
| -keepattributes *Annotation*,Signature |  | ||||||
| -keepattributes Exceptions |  | ||||||
| -dontwarn okio.** |  | ||||||
| -dontwarn javax.annotation.Nullable |  | ||||||
| -dontwarn javax.annotation.ParametersAreNonnullByDefault |  | ||||||
|  |  | ||||||
|  |  | ||||||
| #Bottom bar lib |  | ||||||
| -dontwarn com.roughike.bottombar.** |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # self signed glidemodule |  | ||||||
| -keep public class * implements com.bumptech.glide.module.GlideModule |  | ||||||
| -keep public class * extends com.bumptech.glide.AppGlideModule |  | ||||||
| -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { |  | ||||||
|   **[] $VALUES; |  | ||||||
|   public *; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| -dontwarn com.anupcowkur.reservoir.** |  | ||||||
|  |  | ||||||
| -dontwarn javax.annotation.** |  | ||||||
|  |  | ||||||
| -keep class android.support.v7.widget.SearchView { *; } |  | ||||||
|  |  | ||||||
| # maybe remove later ? |  | ||||||
| -keep class * extends androidx.fragment.app.Fragment |  | ||||||
| @@ -1,96 +0,0 @@ | |||||||
| { |  | ||||||
|   "formatVersion": 1, |  | ||||||
|   "database": { |  | ||||||
|     "version": 1, |  | ||||||
|     "identityHash": "08ca537d7ac9d4dd216e8e395d70801a", |  | ||||||
|     "entities": [ |  | ||||||
|       { |  | ||||||
|         "tableName": "tags", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tag", |  | ||||||
|             "columnName": "tag", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "color", |  | ||||||
|             "columnName": "color", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "tag" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "sources", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "title", |  | ||||||
|             "columnName": "title", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tags", |  | ||||||
|             "columnName": "tags", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "spout", |  | ||||||
|             "columnName": "spout", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "error", |  | ||||||
|             "columnName": "error", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "icon", |  | ||||||
|             "columnName": "icon", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       } |  | ||||||
|     ], |  | ||||||
|     "views": [], |  | ||||||
|     "setupQueries": [ |  | ||||||
|       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", |  | ||||||
|       "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"08ca537d7ac9d4dd216e8e395d70801a\")" |  | ||||||
|     ] |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,176 +0,0 @@ | |||||||
| { |  | ||||||
|   "formatVersion": 1, |  | ||||||
|   "database": { |  | ||||||
|     "version": 2, |  | ||||||
|     "identityHash": "6fa6944b04100d68eab61039876a8804", |  | ||||||
|     "entities": [ |  | ||||||
|       { |  | ||||||
|         "tableName": "tags", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tag", |  | ||||||
|             "columnName": "tag", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "color", |  | ||||||
|             "columnName": "color", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "tag" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "sources", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "title", |  | ||||||
|             "columnName": "title", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tags", |  | ||||||
|             "columnName": "tags", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "spout", |  | ||||||
|             "columnName": "spout", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "error", |  | ||||||
|             "columnName": "error", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "icon", |  | ||||||
|             "columnName": "icon", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "items", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "datetime", |  | ||||||
|             "columnName": "datetime", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "title", |  | ||||||
|             "columnName": "title", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "content", |  | ||||||
|             "columnName": "content", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "starred", |  | ||||||
|             "columnName": "starred", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "thumbnail", |  | ||||||
|             "columnName": "thumbnail", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "icon", |  | ||||||
|             "columnName": "icon", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "link", |  | ||||||
|             "columnName": "link", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "sourcetitle", |  | ||||||
|             "columnName": "sourcetitle", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tags", |  | ||||||
|             "columnName": "tags", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       } |  | ||||||
|     ], |  | ||||||
|     "views": [], |  | ||||||
|     "setupQueries": [ |  | ||||||
|       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", |  | ||||||
|       "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6fa6944b04100d68eab61039876a8804\")" |  | ||||||
|     ] |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,226 +0,0 @@ | |||||||
| { |  | ||||||
|   "formatVersion": 1, |  | ||||||
|   "database": { |  | ||||||
|     "version": 3, |  | ||||||
|     "identityHash": "7ad9c4961992c13b670128485ebb3efc", |  | ||||||
|     "entities": [ |  | ||||||
|       { |  | ||||||
|         "tableName": "tags", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tag", |  | ||||||
|             "columnName": "tag", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "color", |  | ||||||
|             "columnName": "color", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "tag" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "sources", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "title", |  | ||||||
|             "columnName": "title", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tags", |  | ||||||
|             "columnName": "tags", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "spout", |  | ||||||
|             "columnName": "spout", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "error", |  | ||||||
|             "columnName": "error", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "icon", |  | ||||||
|             "columnName": "icon", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "items", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "datetime", |  | ||||||
|             "columnName": "datetime", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "title", |  | ||||||
|             "columnName": "title", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "content", |  | ||||||
|             "columnName": "content", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "starred", |  | ||||||
|             "columnName": "starred", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "thumbnail", |  | ||||||
|             "columnName": "thumbnail", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "icon", |  | ||||||
|             "columnName": "icon", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "link", |  | ||||||
|             "columnName": "link", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "sourcetitle", |  | ||||||
|             "columnName": "sourcetitle", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tags", |  | ||||||
|             "columnName": "tags", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "actions", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "articleId", |  | ||||||
|             "columnName": "articleid", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "read", |  | ||||||
|             "columnName": "read", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "starred", |  | ||||||
|             "columnName": "starred", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unstarred", |  | ||||||
|             "columnName": "unstarred", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": true |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       } |  | ||||||
|     ], |  | ||||||
|     "views": [], |  | ||||||
|     "setupQueries": [ |  | ||||||
|       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", |  | ||||||
|       "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"7ad9c4961992c13b670128485ebb3efc\")" |  | ||||||
|     ] |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,226 +0,0 @@ | |||||||
| { |  | ||||||
|   "formatVersion": 1, |  | ||||||
|   "database": { |  | ||||||
|     "version": 4, |  | ||||||
|     "identityHash": "9cf8b03d32f80dfd58160599a1df197d", |  | ||||||
|     "entities": [ |  | ||||||
|       { |  | ||||||
|         "tableName": "tags", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tag", |  | ||||||
|             "columnName": "tag", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "color", |  | ||||||
|             "columnName": "color", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "tag" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "sources", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "title", |  | ||||||
|             "columnName": "title", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tags", |  | ||||||
|             "columnName": "tags", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "spout", |  | ||||||
|             "columnName": "spout", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "error", |  | ||||||
|             "columnName": "error", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "icon", |  | ||||||
|             "columnName": "icon", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "items", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "datetime", |  | ||||||
|             "columnName": "datetime", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "title", |  | ||||||
|             "columnName": "title", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "content", |  | ||||||
|             "columnName": "content", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "starred", |  | ||||||
|             "columnName": "starred", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "thumbnail", |  | ||||||
|             "columnName": "thumbnail", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": false |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "icon", |  | ||||||
|             "columnName": "icon", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": false |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "link", |  | ||||||
|             "columnName": "link", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "sourcetitle", |  | ||||||
|             "columnName": "sourcetitle", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "tags", |  | ||||||
|             "columnName": "tags", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": false |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "tableName": "actions", |  | ||||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)", |  | ||||||
|         "fields": [ |  | ||||||
|           { |  | ||||||
|             "fieldPath": "id", |  | ||||||
|             "columnName": "id", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "articleId", |  | ||||||
|             "columnName": "articleid", |  | ||||||
|             "affinity": "TEXT", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "read", |  | ||||||
|             "columnName": "read", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unread", |  | ||||||
|             "columnName": "unread", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "starred", |  | ||||||
|             "columnName": "starred", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "fieldPath": "unstarred", |  | ||||||
|             "columnName": "unstarred", |  | ||||||
|             "affinity": "INTEGER", |  | ||||||
|             "notNull": true |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "primaryKey": { |  | ||||||
|           "columnNames": [ |  | ||||||
|             "id" |  | ||||||
|           ], |  | ||||||
|           "autoGenerate": true |  | ||||||
|         }, |  | ||||||
|         "indices": [], |  | ||||||
|         "foreignKeys": [] |  | ||||||
|       } |  | ||||||
|     ], |  | ||||||
|     "views": [], |  | ||||||
|     "setupQueries": [ |  | ||||||
|       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", |  | ||||||
|       "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9cf8b03d32f80dfd58160599a1df197d\")" |  | ||||||
|     ] |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| // TODO: test source adding |  | ||||||
| @@ -1,100 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import androidx.test.InstrumentationRegistry |  | ||||||
| import androidx.test.espresso.Espresso.onView |  | ||||||
| import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu |  | ||||||
| import androidx.test.espresso.action.ViewActions.click |  | ||||||
| import androidx.test.espresso.action.ViewActions.closeSoftKeyboard |  | ||||||
| import androidx.test.espresso.action.ViewActions.pressKey |  | ||||||
| import androidx.test.espresso.action.ViewActions.typeText |  | ||||||
| import androidx.test.espresso.assertion.ViewAssertions.matches |  | ||||||
| import androidx.test.espresso.intent.Intents |  | ||||||
| import androidx.test.espresso.intent.Intents.intended |  | ||||||
| import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.isDisplayed |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withContentDescription |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withId |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withText |  | ||||||
| import androidx.test.rule.ActivityTestRule |  | ||||||
| import androidx.test.runner.AndroidJUnit4 |  | ||||||
| import android.view.KeyEvent |  | ||||||
| import androidx.test.espresso.matcher.RootMatchers.isDialog |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import org.junit.After |  | ||||||
| import org.junit.Before |  | ||||||
| import org.junit.Rule |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith |  | ||||||
|  |  | ||||||
| @RunWith(AndroidJUnit4::class) |  | ||||||
| class HomeActivityEspressoTest { |  | ||||||
|     lateinit var context: Context |  | ||||||
|  |  | ||||||
|     @Rule @JvmField |  | ||||||
|     val rule = ActivityTestRule(HomeActivity::class.java, true, false) |  | ||||||
|  |  | ||||||
|     @Before |  | ||||||
|     fun clearData() { |  | ||||||
|         context = InstrumentationRegistry.getInstrumentation().targetContext |  | ||||||
|  |  | ||||||
|         val editor = |  | ||||||
|                 context |  | ||||||
|                         .getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|                         .edit() |  | ||||||
|         editor.clear() |  | ||||||
|  |  | ||||||
|         editor.putString("url", BuildConfig.LOGIN_URL) |  | ||||||
|         editor.putString("login", BuildConfig.LOGIN_USERNAME) |  | ||||||
|         editor.putString("password", BuildConfig.LOGIN_PASSWORD) |  | ||||||
|  |  | ||||||
|         editor.commit() |  | ||||||
|  |  | ||||||
|         Intents.init() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun menuItems() { |  | ||||||
|  |  | ||||||
|         rule.launchActivity(Intent()) |  | ||||||
|  |  | ||||||
|         onView( |  | ||||||
|                 withMenu( |  | ||||||
|                         id = R.id.action_search, |  | ||||||
|                         titleId = R.string.menu_home_search |  | ||||||
|                 ) |  | ||||||
|         ).perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.search_bar)).check(matches(isDisplayed())) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.search_src_text)).perform( |  | ||||||
|                 typeText("android"), |  | ||||||
|                 pressKey(KeyEvent.KEYCODE_SEARCH), |  | ||||||
|                 closeSoftKeyboard() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         onView(withContentDescription(R.string.abc_toolbar_collapse_description)).perform(click()) |  | ||||||
|  |  | ||||||
|         openActionBarOverflowOrOptionsMenu(context) |  | ||||||
|  |  | ||||||
|         onView(withMenu(id = R.id.refresh, titleId = R.string.menu_home_refresh)) |  | ||||||
|                 .perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withText(android.R.string.ok)) |  | ||||||
|             .inRoot(isDialog()).check(matches(isDisplayed())).perform(click()) |  | ||||||
|  |  | ||||||
|         openActionBarOverflowOrOptionsMenu(context) |  | ||||||
|  |  | ||||||
|         onView(withText(R.string.action_disconnect)).perform(click()) |  | ||||||
|  |  | ||||||
|         intended(hasComponent(LoginActivity::class.java.name)) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // TODO: test articles opening and actions for cards and lists |  | ||||||
|  |  | ||||||
|     @After |  | ||||||
|     fun releaseIntents() { |  | ||||||
|         Intents.release() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,178 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import androidx.test.InstrumentationRegistry |  | ||||||
| import androidx.test.espresso.Espresso.onView |  | ||||||
| import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu |  | ||||||
| import androidx.test.espresso.action.ViewActions.click |  | ||||||
| import androidx.test.espresso.action.ViewActions.closeSoftKeyboard |  | ||||||
| import androidx.test.espresso.action.ViewActions.pressBack |  | ||||||
| import androidx.test.espresso.action.ViewActions.typeText |  | ||||||
| import androidx.test.espresso.assertion.ViewAssertions.matches |  | ||||||
| import androidx.test.espresso.intent.Intents |  | ||||||
| import androidx.test.espresso.intent.Intents.intended |  | ||||||
| import androidx.test.espresso.intent.Intents.times |  | ||||||
| import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.isRoot |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withId |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withText |  | ||||||
| import androidx.test.rule.ActivityTestRule |  | ||||||
| import androidx.test.runner.AndroidJUnit4 |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import com.mikepenz.aboutlibraries.ui.LibsActivity |  | ||||||
| import org.junit.After |  | ||||||
| import org.junit.Before |  | ||||||
| import org.junit.Rule |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith |  | ||||||
|  |  | ||||||
| @RunWith(AndroidJUnit4::class) |  | ||||||
| class LoginActivityEspressoTest { |  | ||||||
|  |  | ||||||
|     @Rule @JvmField |  | ||||||
|     val rule = ActivityTestRule(LoginActivity::class.java, true, false) |  | ||||||
|  |  | ||||||
|     private lateinit var context: Context |  | ||||||
|     private lateinit var url: String |  | ||||||
|     private lateinit var username: String |  | ||||||
|     private lateinit var password: String |  | ||||||
|  |  | ||||||
|     @Before |  | ||||||
|     fun setUp() { |  | ||||||
|         context = InstrumentationRegistry.getInstrumentation().targetContext |  | ||||||
|         val editor = |  | ||||||
|                 context |  | ||||||
|                         .getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|                         .edit() |  | ||||||
|         editor.clear() |  | ||||||
|         editor.commit() |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         url = BuildConfig.LOGIN_URL |  | ||||||
|         username = BuildConfig.LOGIN_USERNAME |  | ||||||
|         password = BuildConfig.LOGIN_PASSWORD |  | ||||||
|  |  | ||||||
|         Intents.init() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun menuItems() { |  | ||||||
|  |  | ||||||
|         rule.launchActivity(Intent()) |  | ||||||
|  |  | ||||||
|         openActionBarOverflowOrOptionsMenu(context) |  | ||||||
|  |  | ||||||
|         onView(withText(R.string.action_about)).perform(click()) |  | ||||||
|  |  | ||||||
|         intended(hasComponent(LibsActivity::class.java.name), times(1)) |  | ||||||
|  |  | ||||||
|         onView(isRoot()).perform(pressBack()) |  | ||||||
|  |  | ||||||
|         intended(hasComponent(LoginActivity::class.java.name)) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun wrongLoginUrl() { |  | ||||||
|         rule.launchActivity(Intent()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.loginProgress)) |  | ||||||
|                 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.urlView)).perform(click()).perform(typeText("WRONGURL")) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.signInButton)).perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.urlView)).check(matches(isHintOrErrorEnabled())) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // TODO: Add tests for multiple false urls with dialog |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun emptyAuthData() { |  | ||||||
|  |  | ||||||
|         rule.launchActivity(Intent()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.withLogin)).perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.signInButton)).perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.loginView)).check(matches(isHintOrErrorEnabled())) |  | ||||||
|         onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled())) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.loginView)).perform(click()).perform( |  | ||||||
|                 typeText(username), |  | ||||||
|                 closeSoftKeyboard() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled())) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.signInButton)).perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.passwordView)).check( |  | ||||||
|                 matches( |  | ||||||
|                         isHintOrErrorEnabled() |  | ||||||
|                 ) |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun wrongAuthData() { |  | ||||||
|  |  | ||||||
|         rule.launchActivity(Intent()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.withLogin)).perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.loginView)).perform(click()).perform( |  | ||||||
|                 typeText(username), |  | ||||||
|                 closeSoftKeyboard() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.passwordView)).perform(click()).perform( |  | ||||||
|                 typeText("WRONGPASS"), |  | ||||||
|                 closeSoftKeyboard() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.signInButton)).perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.urlView)).check(matches(isHintOrErrorEnabled())) |  | ||||||
|         onView(withId(R.id.loginView)).check(matches(isHintOrErrorEnabled())) |  | ||||||
|         onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled())) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun workingAuth() { |  | ||||||
|  |  | ||||||
|         rule.launchActivity(Intent()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.withLogin)).perform(click()) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.loginView)).perform(click()).perform( |  | ||||||
|                 typeText(username), |  | ||||||
|                 closeSoftKeyboard() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.passwordView)).perform(click()).perform( |  | ||||||
|                 typeText(password), |  | ||||||
|                 closeSoftKeyboard() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         onView(withId(R.id.signInButton)).perform(click()) |  | ||||||
|  |  | ||||||
|         Thread.sleep(2000) |  | ||||||
|         intended(hasComponent(HomeActivity::class.java.name)) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @After |  | ||||||
|     fun releaseIntents() { |  | ||||||
|         Intents.release() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,79 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.content.SharedPreferences |  | ||||||
| import android.preference.PreferenceManager |  | ||||||
| import androidx.test.InstrumentationRegistry.getInstrumentation |  | ||||||
| import androidx.test.espresso.intent.Intents |  | ||||||
| import androidx.test.espresso.intent.Intents.intended |  | ||||||
| import androidx.test.espresso.intent.Intents.times |  | ||||||
| import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent |  | ||||||
| import androidx.test.rule.ActivityTestRule |  | ||||||
| import androidx.test.runner.AndroidJUnit4 |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import org.junit.After |  | ||||||
|  |  | ||||||
| import org.junit.Before |  | ||||||
| import org.junit.Rule |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith |  | ||||||
|  |  | ||||||
| @RunWith(AndroidJUnit4::class) |  | ||||||
| class MainActivityEspressoTest { |  | ||||||
|  |  | ||||||
|     lateinit var intent: Intent |  | ||||||
|     lateinit var preferencesEditor: SharedPreferences.Editor |  | ||||||
|     private lateinit var url: String |  | ||||||
|     private lateinit var username: String |  | ||||||
|     private lateinit var password: String |  | ||||||
|  |  | ||||||
|     @Rule @JvmField |  | ||||||
|     val rule = ActivityTestRule(MainActivity::class.java, true, false) |  | ||||||
|  |  | ||||||
|     @Before |  | ||||||
|     fun setUp() { |  | ||||||
|         intent = Intent() |  | ||||||
|         val context = getInstrumentation().targetContext |  | ||||||
|  |  | ||||||
|         // create a SharedPreferences editor |  | ||||||
|         preferencesEditor = context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE).edit() |  | ||||||
|  |  | ||||||
|         url = BuildConfig.LOGIN_URL |  | ||||||
|         username = BuildConfig.LOGIN_USERNAME |  | ||||||
|         password = BuildConfig.LOGIN_PASSWORD |  | ||||||
|  |  | ||||||
|         Intents.init() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun checkFirstOpenLaunchesIntro() { |  | ||||||
|         preferencesEditor.putString("url", "") |  | ||||||
|         preferencesEditor.putString("password", "") |  | ||||||
|         preferencesEditor.putString("login", "") |  | ||||||
|         preferencesEditor.commit() |  | ||||||
|  |  | ||||||
|         rule.launchActivity(intent) |  | ||||||
|  |  | ||||||
|         intended(hasComponent(LoginActivity::class.java.name)) |  | ||||||
|         intended(hasComponent(HomeActivity::class.java.name), times(0)) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun checkNotFirstOpenLaunchesLogin() { |  | ||||||
|         preferencesEditor.putString("url", url) |  | ||||||
|         preferencesEditor.putString("password", password) |  | ||||||
|         preferencesEditor.putString("login", username) |  | ||||||
|         preferencesEditor.commit() |  | ||||||
|  |  | ||||||
|         rule.launchActivity(intent) |  | ||||||
|  |  | ||||||
|         intended(hasComponent(MainActivity::class.java.name)) |  | ||||||
|         intended(hasComponent(HomeActivity::class.java.name)) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @After |  | ||||||
|     fun releaseIntents() { |  | ||||||
|         Intents.release() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers |  | ||||||
| import android.view.View |  | ||||||
| import android.widget.EditText |  | ||||||
| import org.hamcrest.Description |  | ||||||
| import org.hamcrest.Matcher |  | ||||||
| import org.hamcrest.Matchers |  | ||||||
| import org.hamcrest.TypeSafeMatcher |  | ||||||
|  |  | ||||||
| fun isHintOrErrorEnabled(): Matcher<View> = |  | ||||||
|         object : TypeSafeMatcher<View>() { |  | ||||||
|             override fun describeTo(description: Description?) { |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             override fun matchesSafely(item: View?): Boolean { |  | ||||||
|                 if (item !is EditText) { |  | ||||||
|                     return false |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return item.error.isNotEmpty() |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
| fun withMenu(id: Int, titleId: Int): Matcher<View> = |  | ||||||
|         Matchers.anyOf( |  | ||||||
|                 ViewMatchers.withId(id), |  | ||||||
|                 ViewMatchers.withText(titleId) |  | ||||||
|         ) |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import org.junit.Test |  | ||||||
|  |  | ||||||
| class DateUtilsTest { |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun parseDateV4() { |  | ||||||
|  |  | ||||||
|         Config.apiVersion = 4 |  | ||||||
|         val dateString = "2013-04-07T13:43:00+01:00" |  | ||||||
|  |  | ||||||
|         val milliseconds = parseDate(dateString).toEpochMilli() |  | ||||||
|         val correctMilliseconds : Long = 1365338580000 |  | ||||||
|  |  | ||||||
|         assert(milliseconds == correctMilliseconds) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun parseDateV1() { |  | ||||||
|         Config.apiVersion = 0 |  | ||||||
|         val dateString = "2013-04-07 13:43:00" |  | ||||||
|  |  | ||||||
|         val milliseconds = parseDate(dateString).toEpochMilli() |  | ||||||
|         val correctMilliseconds = 1365342180000 |  | ||||||
|  |  | ||||||
|         assert(milliseconds == correctMilliseconds) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,87 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|     package="apps.amine.bou.readerforselfoss"> |  | ||||||
|  |  | ||||||
|     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> |  | ||||||
|     <uses-permission android:name="android.permission.INTERNET" /> |  | ||||||
|  |  | ||||||
|     <application |  | ||||||
|         android:name=".MyApp" |  | ||||||
|         android:allowBackup="true" |  | ||||||
|         android:icon="@mipmap/ic_launcher" |  | ||||||
|         android:label="@string/app_name" |  | ||||||
|         android:supportsRtl="true" |  | ||||||
|         android:networkSecurityConfig="@xml/network_security_config" |  | ||||||
|         android:theme="@style/NoBar"> |  | ||||||
|         <activity |  | ||||||
|             android:name=".MainActivity" |  | ||||||
|             android:theme="@style/SplashTheme" |  | ||||||
|             android:exported="true"> |  | ||||||
|             <intent-filter> |  | ||||||
|                 <action android:name="android.intent.action.MAIN" /> |  | ||||||
|                 <category android:name="android.intent.category.LAUNCHER" /> |  | ||||||
|             </intent-filter> |  | ||||||
|  |  | ||||||
|             <meta-data |  | ||||||
|                 android:name="android.app.shortcuts" |  | ||||||
|                 android:resource="@xml/shortcuts" /> |  | ||||||
|         </activity> |  | ||||||
|         <activity |  | ||||||
|             android:name=".LoginActivity" |  | ||||||
|             android:label="@string/title_activity_login"> |  | ||||||
|         </activity> |  | ||||||
|         <activity android:name=".HomeActivity"> |  | ||||||
|         </activity> |  | ||||||
|         <activity |  | ||||||
|             android:name=".settings.SettingsActivity" |  | ||||||
|             android:label="@string/title_activity_settings" |  | ||||||
|             android:parentActivityName=".HomeActivity"> |  | ||||||
|             <meta-data |  | ||||||
|                 android:name="android.support.PARENT_ACTIVITY" |  | ||||||
|                 android:value=".HomeActivity" /> |  | ||||||
|         </activity> |  | ||||||
|         <activity |  | ||||||
|             android:name=".SourcesActivity" |  | ||||||
|             android:parentActivityName=".HomeActivity"> |  | ||||||
|             <meta-data |  | ||||||
|                 android:name="android.support.PARENT_ACTIVITY" |  | ||||||
|                 android:value=".HomeActivity" /> |  | ||||||
|         </activity> |  | ||||||
|         <activity |  | ||||||
|             android:name=".AddSourceActivity" |  | ||||||
|             android:parentActivityName=".SourcesActivity" |  | ||||||
|             android:exported="true"> |  | ||||||
|             <meta-data |  | ||||||
|                 android:name="android.support.PARENT_ACTIVITY" |  | ||||||
|                 android:value=".SourcesActivity" /> |  | ||||||
|  |  | ||||||
|             <intent-filter> |  | ||||||
|                 <action android:name="android.intent.action.SEND" /> |  | ||||||
|                 <category android:name="android.intent.category.DEFAULT" /> |  | ||||||
|                 <data android:mimeType="text/plain" /> |  | ||||||
|             </intent-filter> |  | ||||||
|         </activity> |  | ||||||
|         <activity |  | ||||||
|             android:name=".ReaderActivity"> |  | ||||||
|         </activity> |  | ||||||
|         <activity |  | ||||||
|             android:name=".ImageActivity"> |  | ||||||
|         </activity> |  | ||||||
|  |  | ||||||
|         <meta-data |  | ||||||
|             android:name="apps.amine.bou.readerforselfoss.utils.glide.SelfSignedGlideModule" |  | ||||||
|             android:value="GlideModule" /> |  | ||||||
|  |  | ||||||
|         <meta-data android:name="android.webkit.WebView.MetricsOptOut" |  | ||||||
|             android:value="true" /> |  | ||||||
|  |  | ||||||
|         <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" |  | ||||||
|             android:value="true" /> |  | ||||||
|  |  | ||||||
|         <meta-data android:name="android.max_aspect" android:value="2.1" /> |  | ||||||
|         <meta-data |  | ||||||
|             android:name="preloaded_fonts" |  | ||||||
|             android:resource="@array/preloaded_fonts" /> |  | ||||||
|     </application> |  | ||||||
|  |  | ||||||
| </manifest> |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 20 KiB | 
| @@ -1,268 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Bundle |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import androidx.constraintlayout.widget.ConstraintLayout |  | ||||||
| import androidx.appcompat.app.AppCompatActivity |  | ||||||
| import android.view.View |  | ||||||
| import android.widget.AdapterView |  | ||||||
| import android.widget.ArrayAdapter |  | ||||||
| import android.widget.EditText |  | ||||||
| import android.widget.ProgressBar |  | ||||||
| import android.widget.Spinner |  | ||||||
| import android.widget.TextView |  | ||||||
| import android.widget.Toast |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Spout |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.AppColors |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.Toppings |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid |  | ||||||
| import com.ftinc.scoop.Scoop |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Callback |  | ||||||
| import retrofit2.Response |  | ||||||
| import android.graphics.PorterDuff |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.ActivityAddSourceBinding |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AddSourceActivity : AppCompatActivity() { |  | ||||||
|  |  | ||||||
|     private var mSpoutsValue: String? = null |  | ||||||
|     private lateinit var api: SelfossApi |  | ||||||
|  |  | ||||||
|     private lateinit var appColors: AppColors |  | ||||||
|     private lateinit var binding: ActivityAddSourceBinding |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         appColors = AppColors(this@AddSourceActivity) |  | ||||||
|  |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         binding = ActivityAddSourceBinding.inflate(layoutInflater) |  | ||||||
|         val view = binding.root |  | ||||||
|  |  | ||||||
|         setContentView(view) |  | ||||||
|  |  | ||||||
|         val scoop = Scoop.getInstance() |  | ||||||
|         scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar) |  | ||||||
|         scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value) |  | ||||||
|  |  | ||||||
|         val drawable = binding.nameInput.background |  | ||||||
|         drawable.setTint(appColors.colorAccent) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         // TODO: clean |  | ||||||
|         binding.nameInput.background = drawable |  | ||||||
|  |  | ||||||
|         val drawable1 = binding.sourceUri.background |  | ||||||
|         drawable1.setTint(appColors.colorAccent) |  | ||||||
|  |  | ||||||
|         binding.sourceUri.background = drawable1 |  | ||||||
|  |  | ||||||
|         val drawable2 = binding.tags.background |  | ||||||
|         drawable2.setTint(appColors.colorAccent) |  | ||||||
|  |  | ||||||
|         binding.tags.background = drawable2 |  | ||||||
|  |  | ||||||
|         setSupportActionBar(binding.toolbar) |  | ||||||
|         supportActionBar?.setDisplayHomeAsUpEnabled(true) |  | ||||||
|         supportActionBar?.setDisplayShowHomeEnabled(true) |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             val prefs = PreferenceManager.getDefaultSharedPreferences(this) |  | ||||||
|             val settings = |  | ||||||
|                 getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|             api = SelfossApi( |  | ||||||
|                 this, |  | ||||||
|                 this@AddSourceActivity, |  | ||||||
|                 settings.getBoolean("isSelfSignedCert", false), |  | ||||||
|                 prefs.getString("api_timeout", "-1")!!.toLong() |  | ||||||
|             ) |  | ||||||
|         } catch (e: IllegalArgumentException) { |  | ||||||
|             mustLoginToAddSource() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput) |  | ||||||
|  |  | ||||||
|         binding.saveBtn.setTextColor(appColors.colorAccent) |  | ||||||
|  |  | ||||||
|         binding.saveBtn.setOnClickListener { |  | ||||||
|             handleSaveSource(binding.tags, binding.nameInput.text.toString(), binding.sourceUri.text.toString(), api) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onResume() { |  | ||||||
|         super.onResume() |  | ||||||
|         val config = Config(this) |  | ||||||
|  |  | ||||||
|         if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) { |  | ||||||
|             mustLoginToAddSource() |  | ||||||
|         } else { |  | ||||||
|             handleSpoutsSpinner(binding.spoutsSpinner, api, binding.progress, binding.formContainer) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun handleSpoutsSpinner( |  | ||||||
|         spoutsSpinner: Spinner, |  | ||||||
|         api: SelfossApi?, |  | ||||||
|         mProgress: ProgressBar, |  | ||||||
|         formContainer: ConstraintLayout |  | ||||||
|     ) { |  | ||||||
|         val spoutsKV = HashMap<String, String>() |  | ||||||
|         spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { |  | ||||||
|             override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) { |  | ||||||
|                 if (view != null) { |  | ||||||
|                     val spoutName = (view as TextView).text.toString() |  | ||||||
|                     mSpoutsValue = spoutsKV[spoutName] |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             override fun onNothingSelected(adapterView: AdapterView<*>) { |  | ||||||
|                 mSpoutsValue = null |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var items: Map<String, Spout> |  | ||||||
|         api!!.spouts().enqueue(object : Callback<Map<String, Spout>> { |  | ||||||
|             override fun onResponse( |  | ||||||
|                 call: Call<Map<String, Spout>>, |  | ||||||
|                 response: Response<Map<String, Spout>> |  | ||||||
|             ) { |  | ||||||
|                 if (response.body() != null) { |  | ||||||
|                     items = response.body()!! |  | ||||||
|  |  | ||||||
|                     val itemsStrings = items.map { it.value.name } |  | ||||||
|                     for ((key, value) in items) { |  | ||||||
|                         spoutsKV[value.name] = key |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     mProgress.visibility = View.GONE |  | ||||||
|                     formContainer.visibility = View.VISIBLE |  | ||||||
|  |  | ||||||
|                     val spinnerArrayAdapter = |  | ||||||
|                         ArrayAdapter( |  | ||||||
|                             this@AddSourceActivity, |  | ||||||
|                             android.R.layout.simple_spinner_item, |  | ||||||
|                             itemsStrings |  | ||||||
|                         ) |  | ||||||
|                     spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) |  | ||||||
|                     spoutsSpinner.adapter = spinnerArrayAdapter |  | ||||||
|                 } else { |  | ||||||
|                     handleProblemWithSpouts() |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) { |  | ||||||
|                 handleProblemWithSpouts() |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             private fun handleProblemWithSpouts() { |  | ||||||
|                 Toast.makeText( |  | ||||||
|                     this@AddSourceActivity, |  | ||||||
|                     R.string.cant_get_spouts, |  | ||||||
|                     Toast.LENGTH_SHORT |  | ||||||
|                 ).show() |  | ||||||
|                 mProgress.visibility = View.GONE |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun maybeGetDetailsFromIntentSharing( |  | ||||||
|         intent: Intent, |  | ||||||
|         sourceUri: EditText, |  | ||||||
|         nameInput: EditText |  | ||||||
|     ) { |  | ||||||
|         if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) { |  | ||||||
|             sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT)) |  | ||||||
|             nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE)) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun mustLoginToAddSource() { |  | ||||||
|         Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show() |  | ||||||
|         val i = Intent(this, LoginActivity::class.java) |  | ||||||
|         startActivity(i) |  | ||||||
|         finish() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) { |  | ||||||
|  |  | ||||||
|         val sourceDetailsUnavailable = |  | ||||||
|             title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty() |  | ||||||
|  |  | ||||||
|         when { |  | ||||||
|             sourceDetailsUnavailable -> { |  | ||||||
|                 Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show() |  | ||||||
|             } |  | ||||||
|             PreferenceManager.getDefaultSharedPreferences(this).getInt("apiVersionMajor", 0) > 1 -> { |  | ||||||
|                 val tagList = tags.text.toString().split(",").map { it.trim() } |  | ||||||
|                 api.createSourceApi2( |  | ||||||
|                     title, |  | ||||||
|                     url, |  | ||||||
|                     mSpoutsValue!!, |  | ||||||
|                     tagList, |  | ||||||
|                     "" |  | ||||||
|                 ).enqueue(object : Callback<SuccessResponse> { |  | ||||||
|                     override fun onResponse( |  | ||||||
|                         call: Call<SuccessResponse>, |  | ||||||
|                         response: Response<SuccessResponse> |  | ||||||
|                     ) { |  | ||||||
|                         if (response.body() != null && response.body()!!.isSuccess) { |  | ||||||
|                             finish() |  | ||||||
|                         } else { |  | ||||||
|                             Toast.makeText( |  | ||||||
|                                 this@AddSourceActivity, |  | ||||||
|                                 R.string.cant_create_source, |  | ||||||
|                                 Toast.LENGTH_SHORT |  | ||||||
|                             ).show() |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { |  | ||||||
|                         Toast.makeText( |  | ||||||
|                             this@AddSourceActivity, |  | ||||||
|                             R.string.cant_create_source, |  | ||||||
|                             Toast.LENGTH_SHORT |  | ||||||
|                         ).show() |  | ||||||
|                     } |  | ||||||
|                 }) |  | ||||||
|             } |  | ||||||
|             else -> { |  | ||||||
|                 api.createSource( |  | ||||||
|                     title, |  | ||||||
|                     url, |  | ||||||
|                     mSpoutsValue!!, |  | ||||||
|                     tags.text.toString(), |  | ||||||
|                     "" |  | ||||||
|                 ).enqueue(object : Callback<SuccessResponse> { |  | ||||||
|                     override fun onResponse( |  | ||||||
|                         call: Call<SuccessResponse>, |  | ||||||
|                         response: Response<SuccessResponse> |  | ||||||
|                     ) { |  | ||||||
|                         if (response.body() != null && response.body()!!.isSuccess) { |  | ||||||
|                             finish() |  | ||||||
|                         } else { |  | ||||||
|                             Toast.makeText( |  | ||||||
|                                 this@AddSourceActivity, |  | ||||||
|                                 R.string.cant_create_source, |  | ||||||
|                                 Toast.LENGTH_SHORT |  | ||||||
|                             ).show() |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { |  | ||||||
|                         Toast.makeText( |  | ||||||
|                             this@AddSourceActivity, |  | ||||||
|                             R.string.cant_create_source, |  | ||||||
|                             Toast.LENGTH_SHORT |  | ||||||
|                         ).show() |  | ||||||
|                     } |  | ||||||
|                 }) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,53 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.MenuItem |  | ||||||
| import androidx.appcompat.app.AppCompatActivity |  | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| import androidx.fragment.app.FragmentActivity |  | ||||||
| import androidx.viewpager2.adapter.FragmentStateAdapter |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.fragments.ImageFragment |  | ||||||
|  |  | ||||||
| class ImageActivity : AppCompatActivity() { |  | ||||||
|     private lateinit var allImages : ArrayList<String> |  | ||||||
|     private var position : Int = 0 |  | ||||||
|  |  | ||||||
|     private lateinit var binding: ActivityImageBinding |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         binding = ActivityImageBinding.inflate(layoutInflater) |  | ||||||
|         val view = binding.root |  | ||||||
|  |  | ||||||
|         setContentView(view) |  | ||||||
|  |  | ||||||
|         setSupportActionBar(binding.toolBar) |  | ||||||
|         supportActionBar?.setDisplayShowTitleEnabled(false) |  | ||||||
|         supportActionBar?.setDisplayHomeAsUpEnabled(true) |  | ||||||
|  |  | ||||||
|         allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String> |  | ||||||
|         position = intent.getIntExtra("position", 0) |  | ||||||
|  |  | ||||||
|         binding.pager.adapter = ScreenSlidePagerAdapter(this) |  | ||||||
|         binding.pager.setCurrentItem(position, false) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { |  | ||||||
|         when (item.itemId) { |  | ||||||
|             android.R.id.home -> { |  | ||||||
|                 onBackPressed() |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return super.onOptionsItemSelected(item) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { |  | ||||||
|  |  | ||||||
|         override fun getItemCount(): Int = allImages.size |  | ||||||
|  |  | ||||||
|         override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position]) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,294 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.animation.Animator |  | ||||||
| import android.animation.AnimatorListenerAdapter |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.content.SharedPreferences |  | ||||||
| import android.os.Bundle |  | ||||||
| import androidx.appcompat.app.AlertDialog |  | ||||||
| import androidx.appcompat.app.AppCompatActivity |  | ||||||
| import android.text.TextUtils |  | ||||||
| import android.view.Menu |  | ||||||
| import android.view.MenuItem |  | ||||||
| import android.view.View |  | ||||||
| import android.view.inputmethod.EditorInfo |  | ||||||
| import android.widget.TextView |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.ActivityLoginBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.AppColors |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible |  | ||||||
| import com.mikepenz.aboutlibraries.LibsBuilder |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Callback |  | ||||||
| import retrofit2.Response |  | ||||||
|  |  | ||||||
| class LoginActivity : AppCompatActivity() { |  | ||||||
|  |  | ||||||
|     private var inValidCount: Int = 0 |  | ||||||
|     private var isWithSelfSignedCert = false |  | ||||||
|     private var isWithLogin = false |  | ||||||
|     private var isWithHTTPLogin = false |  | ||||||
|  |  | ||||||
|     private lateinit var settings: SharedPreferences |  | ||||||
|     private lateinit var editor: SharedPreferences.Editor |  | ||||||
|     private lateinit var userIdentifier: String |  | ||||||
|     private lateinit var appColors: AppColors |  | ||||||
|     private lateinit var binding: ActivityLoginBinding |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         appColors = AppColors(this@LoginActivity) |  | ||||||
|  |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         binding = ActivityLoginBinding.inflate(layoutInflater) |  | ||||||
|         val view = binding.root |  | ||||||
|  |  | ||||||
|         setContentView(view) |  | ||||||
|  |  | ||||||
|         setSupportActionBar(binding.toolbar) |  | ||||||
|  |  | ||||||
|         handleBaseUrlFail() |  | ||||||
|  |  | ||||||
|         settings = PreferenceManager.getDefaultSharedPreferences(applicationContext) |  | ||||||
|         userIdentifier = settings.getString("unique_id", "")!! |  | ||||||
|  |  | ||||||
|         editor = settings.edit() |  | ||||||
|  |  | ||||||
|         if (settings.getString("url", "")!!.isNotEmpty()) { |  | ||||||
|             goToMain() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         handleActions() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun handleActions() { |  | ||||||
|  |  | ||||||
|         binding.withSelfhostedCert.setOnCheckedChangeListener { _, b -> |  | ||||||
|             isWithSelfSignedCert = !isWithSelfSignedCert |  | ||||||
|             val visi: Int = if (b) View.VISIBLE else View.GONE |  | ||||||
|  |  | ||||||
|             binding.warningText.visibility = visi |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         binding.passwordView.setOnEditorActionListener( |  | ||||||
|             TextView.OnEditorActionListener { _, id, _ -> |  | ||||||
|                 if (id == R.id.loginView || id == EditorInfo.IME_NULL) { |  | ||||||
|                     attemptLogin() |  | ||||||
|                     return@OnEditorActionListener true |  | ||||||
|                 } |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         binding.signInButton.setOnClickListener { attemptLogin() } |  | ||||||
|  |  | ||||||
|         binding.withLogin.setOnCheckedChangeListener { _, b -> |  | ||||||
|             isWithLogin = !isWithLogin |  | ||||||
|             val visi: Int = if (b) View.VISIBLE else View.GONE |  | ||||||
|  |  | ||||||
|             binding.loginView.visibility = visi |  | ||||||
|             binding.passwordView.visibility = visi |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         binding.withHttpLogin.setOnCheckedChangeListener { _, b -> |  | ||||||
|             isWithHTTPLogin = !isWithHTTPLogin |  | ||||||
|             val visi: Int = if (b) View.VISIBLE else View.GONE |  | ||||||
|  |  | ||||||
|             binding.httpLoginView.visibility = visi |  | ||||||
|             binding.httpPasswordView.visibility = visi |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun handleBaseUrlFail() { |  | ||||||
|         if (intent.getBooleanExtra("baseUrlFail", false)) { |  | ||||||
|             val alertDialog = AlertDialog.Builder(this).create() |  | ||||||
|             alertDialog.setTitle(getString(R.string.warning_wrong_url)) |  | ||||||
|             alertDialog.setMessage(getString(R.string.base_url_error)) |  | ||||||
|             alertDialog.setButton( |  | ||||||
|                 AlertDialog.BUTTON_NEUTRAL, |  | ||||||
|                 "OK" |  | ||||||
|             ) { dialog, _ -> dialog.dismiss() } |  | ||||||
|             alertDialog.show() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun goToMain() { |  | ||||||
|         val intent = Intent(this, HomeActivity::class.java) |  | ||||||
|         startActivity(intent) |  | ||||||
|         finish() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun attemptLogin() { |  | ||||||
|  |  | ||||||
|         // Reset errors. |  | ||||||
|         binding.urlView.error = null |  | ||||||
|         binding.loginView.error = null |  | ||||||
|         binding.httpLoginView.error = null |  | ||||||
|         binding.passwordView.error = null |  | ||||||
|         binding.httpPasswordView.error = null |  | ||||||
|  |  | ||||||
|         // Store values at the time of the login attempt. |  | ||||||
|         val url = binding.urlView.text.toString() |  | ||||||
|         val login = binding.loginView.text.toString() |  | ||||||
|         val httpLogin = binding.httpLoginView.text.toString() |  | ||||||
|         val password = binding.passwordView.text.toString() |  | ||||||
|         val httpPassword = binding.httpPasswordView.text.toString() |  | ||||||
|  |  | ||||||
|         var cancel = false |  | ||||||
|         var focusView: View? = null |  | ||||||
|  |  | ||||||
|         if (!url.isBaseUrlValid(this@LoginActivity)) { |  | ||||||
|             binding.urlView.error = getString(R.string.login_url_problem) |  | ||||||
|             focusView = binding.urlView |  | ||||||
|             cancel = true |  | ||||||
|             inValidCount++ |  | ||||||
|             if (inValidCount == 3) { |  | ||||||
|                 val alertDialog = AlertDialog.Builder(this).create() |  | ||||||
|                 alertDialog.setTitle(getString(R.string.warning_wrong_url)) |  | ||||||
|                 alertDialog.setMessage(getString(R.string.text_wrong_url)) |  | ||||||
|                 alertDialog.setButton( |  | ||||||
|                     AlertDialog.BUTTON_NEUTRAL, |  | ||||||
|                     "OK" |  | ||||||
|                 ) { dialog, _ -> dialog.dismiss() } |  | ||||||
|                 alertDialog.show() |  | ||||||
|                 inValidCount = 0 |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (isWithLogin) { |  | ||||||
|             if (TextUtils.isEmpty(password)) { |  | ||||||
|                 binding.passwordView.error = getString(R.string.error_invalid_password) |  | ||||||
|                 focusView = binding.passwordView |  | ||||||
|                 cancel = true |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (TextUtils.isEmpty(login)) { |  | ||||||
|                 binding.loginView.error = getString(R.string.error_field_required) |  | ||||||
|                 focusView = binding.loginView |  | ||||||
|                 cancel = true |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (isWithHTTPLogin) { |  | ||||||
|             if (TextUtils.isEmpty(httpPassword)) { |  | ||||||
|                 binding.httpPasswordView.error = getString(R.string.error_invalid_password) |  | ||||||
|                 focusView = binding.httpPasswordView |  | ||||||
|                 cancel = true |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (TextUtils.isEmpty(httpLogin)) { |  | ||||||
|                 binding.httpLoginView.error = getString(R.string.error_field_required) |  | ||||||
|                 focusView = binding.httpLoginView |  | ||||||
|                 cancel = true |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (cancel) { |  | ||||||
|             focusView?.requestFocus() |  | ||||||
|         } else { |  | ||||||
|             showProgress(true) |  | ||||||
|  |  | ||||||
|             editor.putString("url", url) |  | ||||||
|             editor.putString("login", login) |  | ||||||
|             editor.putString("httpUserName", httpLogin) |  | ||||||
|             editor.putString("password", password) |  | ||||||
|             editor.putString("httpPassword", httpPassword) |  | ||||||
|             editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert) |  | ||||||
|             editor.apply() |  | ||||||
|  |  | ||||||
|             val api = SelfossApi( |  | ||||||
|                 this, |  | ||||||
|                 this@LoginActivity, |  | ||||||
|                 isWithSelfSignedCert, |  | ||||||
|                 -1L |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) { |  | ||||||
|                 api.login().enqueue(object : Callback<SuccessResponse> { |  | ||||||
|                     private fun preferenceError(t: Throwable) { |  | ||||||
|                         editor.remove("url") |  | ||||||
|                         editor.remove("login") |  | ||||||
|                         editor.remove("httpUserName") |  | ||||||
|                         editor.remove("password") |  | ||||||
|                         editor.remove("httpPassword") |  | ||||||
|                         editor.apply() |  | ||||||
|                         binding.urlView.error = getString(R.string.wrong_infos) |  | ||||||
|                         binding.loginView.error = getString(R.string.wrong_infos) |  | ||||||
|                         binding.passwordView.error = getString(R.string.wrong_infos) |  | ||||||
|                         binding.httpLoginView.error = getString(R.string.wrong_infos) |  | ||||||
|                         binding.httpPasswordView.error = getString(R.string.wrong_infos) |  | ||||||
|                         showProgress(false) |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     override fun onResponse( |  | ||||||
|                         call: Call<SuccessResponse>, |  | ||||||
|                         response: Response<SuccessResponse> |  | ||||||
|                     ) { |  | ||||||
|                         if (response.body() != null && response.body()!!.isSuccess) { |  | ||||||
|                             goToMain() |  | ||||||
|                         } else { |  | ||||||
|                             preferenceError(Exception("No response body...")) |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { |  | ||||||
|                         preferenceError(t) |  | ||||||
|                     } |  | ||||||
|                 }) |  | ||||||
|             } else { |  | ||||||
|                 showProgress(false) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun showProgress(show: Boolean) { |  | ||||||
|         val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime) |  | ||||||
|  |  | ||||||
|         binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE |  | ||||||
|         binding.loginForm |  | ||||||
|             .animate() |  | ||||||
|             .setDuration(shortAnimTime.toLong()) |  | ||||||
|             .alpha( |  | ||||||
|                 if (show) 0F else 1F |  | ||||||
|             ).setListener(object : AnimatorListenerAdapter() { |  | ||||||
|             override fun onAnimationEnd(animation: Animator) { |  | ||||||
|                 binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE |  | ||||||
|         binding.loginProgress |  | ||||||
|             .animate() |  | ||||||
|             .setDuration(shortAnimTime.toLong()) |  | ||||||
|             .alpha( |  | ||||||
|                 if (show) 1F else 0F |  | ||||||
|             ).setListener(object : AnimatorListenerAdapter() { |  | ||||||
|             override fun onAnimationEnd(animation: Animator) { |  | ||||||
|                 binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onCreateOptionsMenu(menu: Menu): Boolean { |  | ||||||
|         menuInflater.inflate(R.menu.login_menu, menu) |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { |  | ||||||
|         return when (item.itemId) { |  | ||||||
|             R.id.about -> { |  | ||||||
|                 LibsBuilder() |  | ||||||
|                     .withAboutIconShown(true) |  | ||||||
|                     .withAboutVersionShown(true) |  | ||||||
|                     .start(this) |  | ||||||
|                 true |  | ||||||
|             } |  | ||||||
|             else -> super.onOptionsItemSelected(item) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Bundle |  | ||||||
| import androidx.appcompat.app.AppCompatActivity |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.ActivityMainBinding |  | ||||||
|  |  | ||||||
| class MainActivity : AppCompatActivity() { |  | ||||||
|     private lateinit var binding: ActivityMainBinding |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         binding = ActivityMainBinding.inflate(layoutInflater) |  | ||||||
|         val view = binding.root |  | ||||||
|         setContentView(view) |  | ||||||
|  |  | ||||||
|         val intent = Intent(this, LoginActivity::class.java) |  | ||||||
|  |  | ||||||
|         startActivity(intent) |  | ||||||
|         finish() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,101 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.app.NotificationChannel |  | ||||||
| import android.app.NotificationManager |  | ||||||
| import android.content.Context |  | ||||||
| import android.graphics.drawable.Drawable |  | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Build |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import android.widget.ImageView |  | ||||||
| import androidx.multidex.MultiDexApplication |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth |  | ||||||
| import com.bumptech.glide.Glide |  | ||||||
| import com.bumptech.glide.request.RequestOptions |  | ||||||
| import com.ftinc.scoop.Scoop |  | ||||||
| import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader |  | ||||||
| import com.mikepenz.materialdrawer.util.DrawerImageLoader |  | ||||||
| import java.util.UUID.randomUUID |  | ||||||
|  |  | ||||||
| class MyApp : MultiDexApplication() { |  | ||||||
|     private lateinit var config: Config |  | ||||||
|  |  | ||||||
|     override fun onCreate() { |  | ||||||
|         super.onCreate() |  | ||||||
|         config = Config(baseContext) |  | ||||||
|  |  | ||||||
|         val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|         if (prefs.getString("unique_id", "")!!.isEmpty()) { |  | ||||||
|             val editor = prefs.edit() |  | ||||||
|             editor.putString("unique_id", randomUUID().toString()) |  | ||||||
|             editor.apply() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         initDrawerImageLoader() |  | ||||||
|  |  | ||||||
|         initTheme() |  | ||||||
|  |  | ||||||
|         tryToHandleBug() |  | ||||||
|  |  | ||||||
|         handleNotificationChannels() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun handleNotificationChannels() { |  | ||||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |  | ||||||
|             val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager |  | ||||||
|  |  | ||||||
|             val name = getString(R.string.notification_channel_sync) |  | ||||||
|             val importance = NotificationManager.IMPORTANCE_LOW |  | ||||||
|             val mChannel = NotificationChannel(Config.syncChannelId, name, importance) |  | ||||||
|  |  | ||||||
|             val newItemsChannelname = getString(R.string.new_items_channel_sync) |  | ||||||
|             val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT |  | ||||||
|             val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance) |  | ||||||
|  |  | ||||||
|             notificationManager.createNotificationChannel(mChannel) |  | ||||||
|             notificationManager.createNotificationChannel(newItemsChannelmChannel) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun initDrawerImageLoader() { |  | ||||||
|         DrawerImageLoader.init(object : AbstractDrawerImageLoader() { |  | ||||||
|             override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { |  | ||||||
|                 Glide.with(imageView.context) |  | ||||||
|                     .loadMaybeBasicAuth(config, uri.toString()) |  | ||||||
|                     .apply(RequestOptions.fitCenterTransform().placeholder(placeholder)) |  | ||||||
|                     .into(imageView) |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             override fun cancel(imageView: ImageView) { |  | ||||||
|                 Glide.with(imageView.context).clear(imageView) |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             override fun placeholder(ctx: Context, tag: String?): Drawable { |  | ||||||
|                 return baseContext.resources.getDrawable(R.mipmap.ic_launcher) |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun initTheme() { |  | ||||||
|         Scoop.waffleCone() |  | ||||||
|             .addFlavor(getString(R.string.default_theme), R.style.NoBar, true) |  | ||||||
|             .addFlavor(getString(R.string.default_dark_theme), R.style.NoBarDark, false) |  | ||||||
|             .setSharedPreferences(PreferenceManager.getDefaultSharedPreferences(this)) |  | ||||||
|             .initialize() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun tryToHandleBug() { |  | ||||||
|         val oldHandler = Thread.getDefaultUncaughtExceptionHandler() |  | ||||||
|  |  | ||||||
|         Thread.setDefaultUncaughtExceptionHandler { thread, e -> |  | ||||||
|             if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any { |  | ||||||
|                     it.toString().contains("android.view.ViewDebug") |  | ||||||
|                 }) { |  | ||||||
|                 Unit |  | ||||||
|             } else { |  | ||||||
|                 oldHandler.uncaughtException(thread, e) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,265 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.SharedPreferences |  | ||||||
| import android.graphics.Color |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.KeyEvent |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import androidx.appcompat.app.AppCompatActivity |  | ||||||
| import android.view.Menu |  | ||||||
| import android.view.MenuItem |  | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| import androidx.fragment.app.FragmentActivity |  | ||||||
| import androidx.room.Room |  | ||||||
| import androidx.viewpager2.adapter.FragmentStateAdapter |  | ||||||
| import androidx.viewpager2.widget.ViewPager2 |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.fragments.ArticleFragment |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4 |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.AppColors |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.Toppings |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.SharedItems |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.toggleStar |  | ||||||
| import com.ftinc.scoop.Scoop |  | ||||||
|  |  | ||||||
| class ReaderActivity : AppCompatActivity() { |  | ||||||
|  |  | ||||||
|     private var markOnScroll: Boolean = false |  | ||||||
|     private var currentItem: Int = 0 |  | ||||||
|     private lateinit var userIdentifier: String |  | ||||||
|     private lateinit var appColors: AppColors |  | ||||||
|  |  | ||||||
|     private lateinit var api: SelfossApi |  | ||||||
|  |  | ||||||
|     private lateinit var toolbarMenu: Menu |  | ||||||
|  |  | ||||||
|     private lateinit var db: AppDatabase |  | ||||||
|     private lateinit var prefs: SharedPreferences |  | ||||||
|     private lateinit var binding: ActivityReaderBinding |  | ||||||
|  |  | ||||||
|     private var activeAlignment: Int = 1 |  | ||||||
|     private val JUSTIFY = 1 |  | ||||||
|     private val ALIGN_LEFT = 2 |  | ||||||
|  |  | ||||||
|     private fun showMenuItem(willAddToFavorite: Boolean) { |  | ||||||
|         if (willAddToFavorite) { |  | ||||||
|             toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE) |  | ||||||
|         } else { |  | ||||||
|             toolbarMenu.findItem(R.id.star).icon.setTint(Color.RED) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun canFavorite() { |  | ||||||
|         showMenuItem(true) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun canRemoveFromFavorite() { |  | ||||||
|         showMenuItem(false) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private lateinit var editor: SharedPreferences.Editor |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         appColors = AppColors(this) |  | ||||||
|         binding = ActivityReaderBinding.inflate(layoutInflater) |  | ||||||
|         val view = binding.root |  | ||||||
|  |  | ||||||
|         setContentView(view) |  | ||||||
|  |  | ||||||
|         db = Room.databaseBuilder( |  | ||||||
|             applicationContext, |  | ||||||
|             AppDatabase::class.java, "selfoss-database" |  | ||||||
|         ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() |  | ||||||
|  |  | ||||||
|         val scoop = Scoop.getInstance() |  | ||||||
|         scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar) |  | ||||||
|         scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value) |  | ||||||
|  |  | ||||||
|         setSupportActionBar(binding.toolBar) |  | ||||||
|         supportActionBar?.setDisplayHomeAsUpEnabled(true) |  | ||||||
|         supportActionBar?.setDisplayShowHomeEnabled(true) |  | ||||||
|  |  | ||||||
|         val settings = |  | ||||||
|             getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|  |  | ||||||
|         prefs = PreferenceManager.getDefaultSharedPreferences(this) |  | ||||||
|         editor = prefs.edit() |  | ||||||
|  |  | ||||||
|         userIdentifier = prefs.getString("unique_id", "")!! |  | ||||||
|         markOnScroll = prefs.getBoolean("mark_on_scroll", false) |  | ||||||
|         activeAlignment = prefs.getInt("text_align", JUSTIFY) |  | ||||||
|  |  | ||||||
|         api = SelfossApi( |  | ||||||
|             this, |  | ||||||
|             this@ReaderActivity, |  | ||||||
|             settings.getBoolean("isSelfSignedCert", false), |  | ||||||
|             prefs.getString("api_timeout", "-1")!!.toLong() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         if (allItems.isEmpty()) { |  | ||||||
|             finish() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         currentItem = intent.getIntExtra("currentItem", 0) |  | ||||||
|  |  | ||||||
|         readItem(allItems[currentItem]) |  | ||||||
|  |  | ||||||
|         binding.pager.adapter = ScreenSlidePagerAdapter(this) |  | ||||||
|         binding.pager.setCurrentItem(currentItem, false) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onResume() { |  | ||||||
|         super.onResume() |  | ||||||
|  |  | ||||||
|         binding.indicator.setViewPager(binding.pager) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun readItem(item: Item) { |  | ||||||
|         if (markOnScroll) { |  | ||||||
|                 SharedItems.readItem(applicationContext, api, db, item) |  | ||||||
|             } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onSaveInstanceState(oldInstanceState: Bundle) { |  | ||||||
|         super.onSaveInstanceState(oldInstanceState) |  | ||||||
|         oldInstanceState.clear() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : |  | ||||||
|         FragmentStateAdapter(fa) { |  | ||||||
|  |  | ||||||
|         override fun getItemCount(): Int = allItems.size |  | ||||||
|  |  | ||||||
|         override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position]) |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { |  | ||||||
|         return when (keyCode) { |  | ||||||
|             KeyEvent.KEYCODE_VOLUME_DOWN -> { |  | ||||||
|                 val currentFragment = supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment |  | ||||||
|                 currentFragment.scrollDown() |  | ||||||
|                 true |  | ||||||
|             } |  | ||||||
|             KeyEvent.KEYCODE_VOLUME_UP -> { |  | ||||||
|                 val currentFragment = supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment |  | ||||||
|                 currentFragment.scrollUp() |  | ||||||
|                 true |  | ||||||
|             } |  | ||||||
|             else -> { |  | ||||||
|                 super.onKeyDown(keyCode, event) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun alignmentMenu(showJustify: Boolean) { |  | ||||||
|         toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify |  | ||||||
|         toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onCreateOptionsMenu(menu: Menu): Boolean { |  | ||||||
|         val inflater = menuInflater |  | ||||||
|         inflater.inflate(R.menu.reader_menu, menu) |  | ||||||
|         toolbarMenu = menu |  | ||||||
|  |  | ||||||
|         if (allItems.isNotEmpty() && allItems[currentItem].starred) { |  | ||||||
|             canRemoveFromFavorite() |  | ||||||
|         } else { |  | ||||||
|             canFavorite() |  | ||||||
|         } |  | ||||||
|         if (activeAlignment == JUSTIFY) { |  | ||||||
|             alignmentMenu(false) |  | ||||||
|         } else { |  | ||||||
|             alignmentMenu(true) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         binding.pager.registerOnPageChangeCallback( |  | ||||||
|                 object : ViewPager2.OnPageChangeCallback() { |  | ||||||
|  |  | ||||||
|                     override fun onPageSelected(position: Int) { |  | ||||||
|                         super.onPageSelected(position) |  | ||||||
|  |  | ||||||
|                         if (allItems[position].starred) { |  | ||||||
|                             canRemoveFromFavorite() |  | ||||||
|                         } else { |  | ||||||
|                             canFavorite() |  | ||||||
|                         } |  | ||||||
|                         readItem(allItems[position]) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { |  | ||||||
|         fun afterSave() { |  | ||||||
|             allItems[binding.pager.currentItem] = |  | ||||||
|                     allItems[binding.pager.currentItem].toggleStar() |  | ||||||
|             canRemoveFromFavorite() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         fun afterUnsave() { |  | ||||||
|             allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar() |  | ||||||
|             canFavorite() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         when (item.itemId) { |  | ||||||
|             android.R.id.home -> { |  | ||||||
|                 onBackPressed() |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|             R.id.star -> { |  | ||||||
|                 if (allItems[binding.pager.currentItem].starred) { |  | ||||||
|                     SharedItems.unstarItem( |  | ||||||
|                         this@ReaderActivity, |  | ||||||
|                         api, |  | ||||||
|                         db, |  | ||||||
|                         allItems[binding.pager.currentItem] |  | ||||||
|                     ) |  | ||||||
|                     afterUnsave() |  | ||||||
|                 } else { |  | ||||||
|                     SharedItems.starItem( |  | ||||||
|                         this@ReaderActivity, |  | ||||||
|                         api, |  | ||||||
|                         db, |  | ||||||
|                         allItems[binding.pager.currentItem] |  | ||||||
|                     ) |  | ||||||
|                     afterSave() |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             R.id.align_left -> { |  | ||||||
|                 editor.putInt("text_align", ALIGN_LEFT) |  | ||||||
|                 editor.apply() |  | ||||||
|                 alignmentMenu(true) |  | ||||||
|                 refreshFragment() |  | ||||||
|             } |  | ||||||
|             R.id.align_justify -> { |  | ||||||
|                 editor.putInt("text_align", JUSTIFY) |  | ||||||
|                 editor.apply() |  | ||||||
|                 alignmentMenu(false) |  | ||||||
|                 refreshFragment() |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return super.onOptionsItemSelected(item) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun refreshFragment() { |  | ||||||
|         finish() |  | ||||||
|         overridePendingTransition(0, 0) |  | ||||||
|         startActivity(intent) |  | ||||||
|         overridePendingTransition(0, 0) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         var allItems: ArrayList<Item> = ArrayList() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,109 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.content.res.ColorStateList |  | ||||||
| import android.os.Bundle |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import androidx.appcompat.app.AppCompatActivity |  | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager |  | ||||||
| import android.widget.Toast |  | ||||||
| import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Source |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.ActivitySourcesBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.AppColors |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.Toppings |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible |  | ||||||
| import com.ftinc.scoop.Scoop |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Callback |  | ||||||
| import retrofit2.Response |  | ||||||
|  |  | ||||||
| class SourcesActivity : AppCompatActivity() { |  | ||||||
|  |  | ||||||
|     private lateinit var appColors: AppColors |  | ||||||
|     private lateinit var binding: ActivitySourcesBinding |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         appColors = AppColors(this@SourcesActivity) |  | ||||||
|         binding = ActivitySourcesBinding.inflate(layoutInflater) |  | ||||||
|         val view = binding.root |  | ||||||
|  |  | ||||||
|         val scoop = Scoop.getInstance() |  | ||||||
|         scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar) |  | ||||||
|         scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value) |  | ||||||
|  |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|  |  | ||||||
|         setContentView(view) |  | ||||||
|  |  | ||||||
|         setSupportActionBar(binding.toolbar) |  | ||||||
|         supportActionBar?.setDisplayHomeAsUpEnabled(true) |  | ||||||
|         supportActionBar?.setDisplayShowHomeEnabled(true) |  | ||||||
|  |  | ||||||
|         binding.fab.rippleColor = appColors.colorAccentDark |  | ||||||
|         binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onStop() { |  | ||||||
|         super.onStop() |  | ||||||
|         binding.recyclerView.clearOnScrollListeners() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onResume() { |  | ||||||
|         super.onResume() |  | ||||||
|         val mLayoutManager = LinearLayoutManager(this) |  | ||||||
|  |  | ||||||
|         val settings = |  | ||||||
|             getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|         val prefs = PreferenceManager.getDefaultSharedPreferences(this) |  | ||||||
|  |  | ||||||
|         val api = SelfossApi( |  | ||||||
|             this, |  | ||||||
|             this@SourcesActivity, |  | ||||||
|             settings.getBoolean("isSelfSignedCert", false), |  | ||||||
|             prefs.getString("api_timeout", "-1")!!.toLong() |  | ||||||
|         ) |  | ||||||
|         var items: ArrayList<Source> = ArrayList() |  | ||||||
|  |  | ||||||
|         binding.recyclerView.setHasFixedSize(true) |  | ||||||
|         binding.recyclerView.layoutManager = mLayoutManager |  | ||||||
|  |  | ||||||
|         if (this@SourcesActivity.isNetworkAccessible(binding.recyclerView)) { |  | ||||||
|             api.sources.enqueue(object : Callback<List<Source>> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                     call: Call<List<Source>>, |  | ||||||
|                     response: Response<List<Source>> |  | ||||||
|                 ) { |  | ||||||
|                     if (response.body() != null && response.body()!!.isNotEmpty()) { |  | ||||||
|                         items = response.body() as ArrayList<Source> |  | ||||||
|                     } |  | ||||||
|                     val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api) |  | ||||||
|                     binding.recyclerView.adapter = mAdapter |  | ||||||
|                     mAdapter.notifyDataSetChanged() |  | ||||||
|                     if (items.isEmpty()) { |  | ||||||
|                         Toast.makeText( |  | ||||||
|                             this@SourcesActivity, |  | ||||||
|                             R.string.nothing_here, |  | ||||||
|                             Toast.LENGTH_SHORT |  | ||||||
|                         ).show() |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 override fun onFailure(call: Call<List<Source>>, t: Throwable) { |  | ||||||
|                     Toast.makeText( |  | ||||||
|                         this@SourcesActivity, |  | ||||||
|                         R.string.cant_get_sources, |  | ||||||
|                         Toast.LENGTH_SHORT |  | ||||||
|                     ).show() |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         binding.fab.setOnClickListener { |  | ||||||
|             startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java)) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,153 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.adapters |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import android.content.Context |  | ||||||
| import androidx.recyclerview.widget.RecyclerView |  | ||||||
| import android.view.LayoutInflater |  | ||||||
| import android.view.View |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import android.widget.ImageView.ScaleType |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.CardItemBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.AppColors |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.SharedItems |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.openItemUrl |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.shareLink |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.sourceAndDateText |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.toTextDrawableString |  | ||||||
| import com.amulyakhare.textdrawable.TextDrawable |  | ||||||
| import com.amulyakhare.textdrawable.util.ColorGenerator |  | ||||||
| import com.bumptech.glide.Glide |  | ||||||
|  |  | ||||||
| class ItemCardAdapter( |  | ||||||
|     override val app: Activity, |  | ||||||
|     override var items: ArrayList<Item>, |  | ||||||
|     override val api: SelfossApi, |  | ||||||
|     override val db: AppDatabase, |  | ||||||
|     private val helper: CustomTabActivityHelper, |  | ||||||
|     private val internalBrowser: Boolean, |  | ||||||
|     private val articleViewer: Boolean, |  | ||||||
|     private val fullHeightCards: Boolean, |  | ||||||
|     override val appColors: AppColors, |  | ||||||
|     override val userIdentifier: String, |  | ||||||
|     override val config: Config, |  | ||||||
|     override val updateItems: (ArrayList<Item>) -> Unit |  | ||||||
| ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { |  | ||||||
|     private val c: Context = app.baseContext |  | ||||||
|     private val generator: ColorGenerator = ColorGenerator.MATERIAL |  | ||||||
|     private val imageMaxHeight: Int = |  | ||||||
|         c.resources.getDimension(R.dimen.card_image_max_height).toInt() |  | ||||||
|  |  | ||||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |  | ||||||
|         val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) |  | ||||||
|         return ViewHolder(binding) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onBindViewHolder(holder: ViewHolder, position: Int) { |  | ||||||
|         with(holder) { |  | ||||||
|             val itm = items[position] |  | ||||||
|  |  | ||||||
|             binding.favButton.isSelected = itm.starred |  | ||||||
|             binding.title.text = itm.getTitleDecoded() |  | ||||||
|  |  | ||||||
|             binding.title.setOnTouchListener(LinkOnTouchListener()) |  | ||||||
|  |  | ||||||
|             binding.title.setLinkTextColor(appColors.colorAccent) |  | ||||||
|  |  | ||||||
|             binding.sourceTitleAndDate.text = itm.sourceAndDateText() |  | ||||||
|  |  | ||||||
|             if (!fullHeightCards) { |  | ||||||
|                 binding.itemImage.maxHeight = imageMaxHeight |  | ||||||
|                 binding.itemImage.scaleType = ScaleType.CENTER_CROP |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (itm.getThumbnail(c).isEmpty()) { |  | ||||||
|                 binding.itemImage.visibility = View.GONE |  | ||||||
|                 Glide.with(c).clear(binding.itemImage) |  | ||||||
|                 binding.itemImage.setImageDrawable(null) |  | ||||||
|             } else { |  | ||||||
|                 binding.itemImage.visibility = View.VISIBLE |  | ||||||
|                 c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage) |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (itm.getIcon(c).isEmpty()) { |  | ||||||
|                 val color = generator.getColor(itm.getSourceTitle()) |  | ||||||
|  |  | ||||||
|                 val drawable = |  | ||||||
|                         TextDrawable |  | ||||||
|                                 .builder() |  | ||||||
|                                 .round() |  | ||||||
|                                 .build(itm.getSourceTitle().toTextDrawableString(c), color) |  | ||||||
|                 binding.sourceImage.setImageDrawable(drawable) |  | ||||||
|             } else { |  | ||||||
|                 c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun getItemCount(): Int { |  | ||||||
|         return items.size |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) { |  | ||||||
|         init { |  | ||||||
|             handleClickListeners() |  | ||||||
|             handleCustomTabActions() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         private fun handleClickListeners() { |  | ||||||
|  |  | ||||||
|             binding.favButton.setOnClickListener { |  | ||||||
|                 val item = items[bindingAdapterPosition] |  | ||||||
|                 if (isNetworkAvailable(c)) { |  | ||||||
|                     if (item.starred) { |  | ||||||
|                         SharedItems.unstarItem(c, api, db, item) |  | ||||||
|                         item.starred = false |  | ||||||
|                         binding.favButton.isSelected = false |  | ||||||
|                     } else { |  | ||||||
|                         SharedItems.starItem(c, api, db, item) |  | ||||||
|                         item.starred = true |  | ||||||
|                         binding.favButton.isSelected = true |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|                 binding.shareBtn.setOnClickListener { |  | ||||||
|                     val item = items[bindingAdapterPosition] |  | ||||||
|                     c.shareLink(item.getLinkDecoded(), item.getTitleDecoded()) |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 binding.browserBtn.setOnClickListener { |  | ||||||
|                     c.openInBrowserAsNewTask(items[bindingAdapterPosition]) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|         private fun handleCustomTabActions() { |  | ||||||
|             val customTabsIntent = c.buildCustomTabsIntent() |  | ||||||
|             helper.bindCustomTabsService(app) |  | ||||||
|  |  | ||||||
|             binding.root.setOnClickListener { |  | ||||||
|                 c.openItemUrl( |  | ||||||
|                     items, |  | ||||||
|                     bindingAdapterPosition, |  | ||||||
|                     items[bindingAdapterPosition].getLinkDecoded(), |  | ||||||
|                     customTabsIntent, |  | ||||||
|                     internalBrowser, |  | ||||||
|                     articleViewer, |  | ||||||
|                     app |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,105 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.adapters |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import android.content.Context |  | ||||||
| import androidx.recyclerview.widget.RecyclerView |  | ||||||
| import android.view.LayoutInflater |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.ListItemBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.AppColors |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.openItemUrl |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.sourceAndDateText |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.toTextDrawableString |  | ||||||
| import com.amulyakhare.textdrawable.TextDrawable |  | ||||||
| import com.amulyakhare.textdrawable.util.ColorGenerator |  | ||||||
| import kotlin.collections.ArrayList |  | ||||||
|  |  | ||||||
| class ItemListAdapter( |  | ||||||
|     override val app: Activity, |  | ||||||
|     override var items: ArrayList<Item>, |  | ||||||
|     override val api: SelfossApi, |  | ||||||
|     override val db: AppDatabase, |  | ||||||
|     private val helper: CustomTabActivityHelper, |  | ||||||
|     private val internalBrowser: Boolean, |  | ||||||
|     private val articleViewer: Boolean, |  | ||||||
|     override val userIdentifier: String, |  | ||||||
|     override val appColors: AppColors, |  | ||||||
|     override val config: Config, |  | ||||||
|     override val updateItems: (ArrayList<Item>) -> Unit |  | ||||||
| ) : ItemsAdapter<ItemListAdapter.ViewHolder>() { |  | ||||||
|     private val generator: ColorGenerator = ColorGenerator.MATERIAL |  | ||||||
|     private val c: Context = app.baseContext |  | ||||||
|  |  | ||||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |  | ||||||
|         val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) |  | ||||||
|         return ViewHolder(binding) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onBindViewHolder(holder: ViewHolder, position: Int) { |  | ||||||
|         with(holder) { |  | ||||||
|             val itm = items[position] |  | ||||||
|  |  | ||||||
|             binding.title.text = itm.getTitleDecoded() |  | ||||||
|  |  | ||||||
|             binding.title.setOnTouchListener(LinkOnTouchListener()) |  | ||||||
|  |  | ||||||
|             binding.title.setLinkTextColor(appColors.colorAccent) |  | ||||||
|  |  | ||||||
|             binding.sourceTitleAndDate.text = itm.sourceAndDateText() |  | ||||||
|  |  | ||||||
|             if (itm.getThumbnail(c).isEmpty()) { |  | ||||||
|  |  | ||||||
|                 if (itm.getIcon(c).isEmpty()) { |  | ||||||
|                     val color = generator.getColor(itm.getSourceTitle()) |  | ||||||
|  |  | ||||||
|                     val drawable = |  | ||||||
|                             TextDrawable |  | ||||||
|                                     .builder() |  | ||||||
|                                     .round() |  | ||||||
|                                     .build(itm.getSourceTitle().toTextDrawableString(c), color) |  | ||||||
|  |  | ||||||
|                     binding.itemImage.setImageDrawable(drawable) |  | ||||||
|                 } else { |  | ||||||
|                     c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage) |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun getItemCount(): Int = items.size |  | ||||||
|  |  | ||||||
|     inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) { |  | ||||||
|  |  | ||||||
|         init { |  | ||||||
|             handleCustomTabActions() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         private fun handleCustomTabActions() { |  | ||||||
|             val customTabsIntent = c.buildCustomTabsIntent() |  | ||||||
|             helper.bindCustomTabsService(app) |  | ||||||
|  |  | ||||||
|             binding.root.setOnClickListener { |  | ||||||
|                 c.openItemUrl( |  | ||||||
|                     items, |  | ||||||
|                     bindingAdapterPosition, |  | ||||||
|                     items[bindingAdapterPosition].getLinkDecoded(), |  | ||||||
|                     customTabsIntent, |  | ||||||
|                     internalBrowser, |  | ||||||
|                     articleViewer, |  | ||||||
|                     app |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,119 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.adapters |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import android.graphics.Color |  | ||||||
| import android.widget.TextView |  | ||||||
| import androidx.recyclerview.widget.RecyclerView |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.AppColors |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.SharedItems |  | ||||||
| import com.google.android.material.snackbar.Snackbar |  | ||||||
|  |  | ||||||
| abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() { |  | ||||||
|     abstract var items: ArrayList<Item> |  | ||||||
|     abstract val api: SelfossApi |  | ||||||
|     abstract val db: AppDatabase |  | ||||||
|     abstract val userIdentifier: String |  | ||||||
|     abstract val app: Activity |  | ||||||
|     abstract val appColors: AppColors |  | ||||||
|     abstract val config: Config |  | ||||||
|     abstract val updateItems: (ArrayList<Item>) -> Unit |  | ||||||
|  |  | ||||||
|     fun updateAllItems() { |  | ||||||
|         items = SharedItems.focusedItems |  | ||||||
|         notifyDataSetChanged() |  | ||||||
|         updateItems(items) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun unmarkSnackbar(i: Item, position: Int) { |  | ||||||
|         val s = Snackbar |  | ||||||
|             .make( |  | ||||||
|                 app.findViewById(R.id.coordLayout), |  | ||||||
|                 R.string.marked_as_read, |  | ||||||
|                 Snackbar.LENGTH_LONG |  | ||||||
|             ) |  | ||||||
|             .setAction(R.string.undo_string) { |  | ||||||
|                 SharedItems.unreadItem(app, api, db, i) |  | ||||||
|                 if (SharedItems.displayedItems == "unread") { |  | ||||||
|                     addItemAtIndex(i, position) |  | ||||||
|                 } else { |  | ||||||
|                     notifyItemChanged(position) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|         val view = s.view |  | ||||||
|         val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text) |  | ||||||
|         tv.setTextColor(Color.WHITE) |  | ||||||
|         s.show() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun markSnackbar(position: Int) { |  | ||||||
|         val s = Snackbar |  | ||||||
|             .make( |  | ||||||
|                 app.findViewById(R.id.coordLayout), |  | ||||||
|                 R.string.marked_as_unread, |  | ||||||
|                 Snackbar.LENGTH_LONG |  | ||||||
|             ) |  | ||||||
|             .setAction(R.string.undo_string) { |  | ||||||
|                 SharedItems.readItem(app, api, db, items[position]) |  | ||||||
|                 items = SharedItems.focusedItems |  | ||||||
|                 if (SharedItems.displayedItems == "unread") { |  | ||||||
|                     notifyItemRemoved(position) |  | ||||||
|                     updateItems(items) |  | ||||||
|                 } else { |  | ||||||
|                     notifyItemChanged(position) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|         val view = s.view |  | ||||||
|         val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text) |  | ||||||
|         tv.setTextColor(Color.WHITE) |  | ||||||
|         s.show() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun handleItemAtIndex(position: Int) { |  | ||||||
|         if (SharedItems.unreadItemStatusAtIndex(position)) { |  | ||||||
|             readItemAtIndex(position) |  | ||||||
|         } else { |  | ||||||
|             unreadItemAtIndex(position) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun readItemAtIndex(position: Int) { |  | ||||||
|         val i = items[position] |  | ||||||
|         SharedItems.readItem(app, api, db, i) |  | ||||||
|         if (SharedItems.displayedItems == "unread") { |  | ||||||
|             items.remove(i) |  | ||||||
|             notifyItemRemoved(position) |  | ||||||
|             updateItems(items) |  | ||||||
|         } else { |  | ||||||
|             notifyItemChanged(position) |  | ||||||
|         } |  | ||||||
|         unmarkSnackbar(i, position) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun unreadItemAtIndex(position: Int) { |  | ||||||
|         SharedItems.unreadItem(app, api, db, items[position]) |  | ||||||
|         notifyItemChanged(position) |  | ||||||
|         markSnackbar(position) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun addItemAtIndex(item: Item, position: Int) { |  | ||||||
|         items.add(position, item) |  | ||||||
|         notifyItemInserted(position) |  | ||||||
|         updateItems(items) |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun addItemsAtEnd(newItems: List<Item>) { |  | ||||||
|         val oldSize = items.size |  | ||||||
|         items.addAll(newItems) |  | ||||||
|         notifyItemRangeInserted(oldSize, newItems.size) |  | ||||||
|         updateItems(items) |  | ||||||
|  |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,106 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.adapters |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import android.content.Context |  | ||||||
| import androidx.constraintlayout.widget.ConstraintLayout |  | ||||||
| import androidx.recyclerview.widget.RecyclerView |  | ||||||
| import android.view.LayoutInflater |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import android.widget.Button |  | ||||||
| import android.widget.Toast |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Source |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.SourceListItemBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.toTextDrawableString |  | ||||||
| import com.amulyakhare.textdrawable.TextDrawable |  | ||||||
| import com.amulyakhare.textdrawable.util.ColorGenerator |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Callback |  | ||||||
| import retrofit2.Response |  | ||||||
|  |  | ||||||
| class SourcesListAdapter( |  | ||||||
|     private val app: Activity, |  | ||||||
|     private val items: ArrayList<Source>, |  | ||||||
|     private val api: SelfossApi |  | ||||||
| ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() { |  | ||||||
|     private val c: Context = app.baseContext |  | ||||||
|     private val generator: ColorGenerator = ColorGenerator.MATERIAL |  | ||||||
|     private lateinit var config: Config |  | ||||||
|     private lateinit var binding: SourceListItemBinding |  | ||||||
|  |  | ||||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |  | ||||||
|         binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) |  | ||||||
|         return ViewHolder(binding.root) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onBindViewHolder(holder: ViewHolder, position: Int) { |  | ||||||
|         val itm = items[position] |  | ||||||
|         config = Config(c) |  | ||||||
|  |  | ||||||
|         if (itm.getIcon(c).isEmpty()) { |  | ||||||
|             val color = generator.getColor(itm.getTitleDecoded()) |  | ||||||
|  |  | ||||||
|             val drawable = |  | ||||||
|                 TextDrawable |  | ||||||
|                     .builder() |  | ||||||
|                     .round() |  | ||||||
|                     .build(itm.getTitleDecoded().toTextDrawableString(c), color) |  | ||||||
|             binding.itemImage.setImageDrawable(drawable) |  | ||||||
|         } else { |  | ||||||
|             c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         binding.sourceTitle.text = itm.getTitleDecoded() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun getItemCount(): Int = items.size |  | ||||||
|  |  | ||||||
|     inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) { |  | ||||||
|  |  | ||||||
|         init { |  | ||||||
|             handleClickListeners() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         private fun handleClickListeners() { |  | ||||||
|  |  | ||||||
|             val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) |  | ||||||
|  |  | ||||||
|             deleteBtn.setOnClickListener { |  | ||||||
|                 if (c.isNetworkAccessible(null)) { |  | ||||||
|                     val (id) = items[adapterPosition] |  | ||||||
|                     api.deleteSource(id).enqueue(object : Callback<SuccessResponse> { |  | ||||||
|                         override fun onResponse( |  | ||||||
|                             call: Call<SuccessResponse>, |  | ||||||
|                             response: Response<SuccessResponse> |  | ||||||
|                         ) { |  | ||||||
|                             if (response.body() != null && response.body()!!.isSuccess) { |  | ||||||
|                                 items.removeAt(adapterPosition) |  | ||||||
|                                 notifyItemRemoved(adapterPosition) |  | ||||||
|                                 notifyItemRangeChanged(adapterPosition, itemCount) |  | ||||||
|                             } else { |  | ||||||
|                                 Toast.makeText( |  | ||||||
|                                     app, |  | ||||||
|                                     R.string.can_delete_source, |  | ||||||
|                                     Toast.LENGTH_SHORT |  | ||||||
|                                 ).show() |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { |  | ||||||
|                             Toast.makeText( |  | ||||||
|                                 app, |  | ||||||
|                                 R.string.can_delete_source, |  | ||||||
|                                 Toast.LENGTH_SHORT |  | ||||||
|                             ).show() |  | ||||||
|                         } |  | ||||||
|                     }) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,35 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.mercury |  | ||||||
|  |  | ||||||
| import com.google.gson.GsonBuilder |  | ||||||
| import okhttp3.OkHttpClient |  | ||||||
| import okhttp3.logging.HttpLoggingInterceptor |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Retrofit |  | ||||||
| import retrofit2.converter.gson.GsonConverterFactory |  | ||||||
|  |  | ||||||
| class MercuryApi() { |  | ||||||
|     private val service: MercuryService |  | ||||||
|  |  | ||||||
|     init { |  | ||||||
|  |  | ||||||
|         val interceptor = HttpLoggingInterceptor() |  | ||||||
|         interceptor.level = HttpLoggingInterceptor.Level.NONE |  | ||||||
|         val client = OkHttpClient.Builder().addInterceptor(interceptor).build() |  | ||||||
|  |  | ||||||
|         val gson = GsonBuilder() |  | ||||||
|             .setLenient() |  | ||||||
|             .create() |  | ||||||
|         val retrofit = |  | ||||||
|             Retrofit |  | ||||||
|                 .Builder() |  | ||||||
|                 .baseUrl("https://www.amine-bou.fr") |  | ||||||
|                 .client(client) |  | ||||||
|                 .addConverterFactory(GsonConverterFactory.create(gson)) |  | ||||||
|                 .build() |  | ||||||
|         service = retrofit.create(MercuryService::class.java) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun parseUrl(url: String): Call<ParsedContent> { |  | ||||||
|         return service.parseUrl(url) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,59 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.mercury |  | ||||||
|  |  | ||||||
| import android.os.Parcel |  | ||||||
| import android.os.Parcelable |  | ||||||
| import com.google.gson.annotations.SerializedName |  | ||||||
|  |  | ||||||
| class ParsedContent( |  | ||||||
|     @SerializedName("title") val title: String, |  | ||||||
|     @SerializedName("content") val content: String?, |  | ||||||
|     @SerializedName("date_published") val date_published: String, |  | ||||||
|     @SerializedName("lead_image_url") val lead_image_url: String?, |  | ||||||
|     @SerializedName("dek") val dek: String, |  | ||||||
|     @SerializedName("url") val url: String, |  | ||||||
|     @SerializedName("domain") val domain: String, |  | ||||||
|     @SerializedName("excerpt") val excerpt: String, |  | ||||||
|     @SerializedName("total_pages") val total_pages: Int, |  | ||||||
|     @SerializedName("rendered_pages") val rendered_pages: Int, |  | ||||||
|     @SerializedName("next_page_url") val next_page_url: String |  | ||||||
| ) : Parcelable { |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         @JvmField |  | ||||||
|         val CREATOR: Parcelable.Creator<ParsedContent> = |  | ||||||
|             object : Parcelable.Creator<ParsedContent> { |  | ||||||
|                 override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source) |  | ||||||
|                 override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size) |  | ||||||
|             } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     constructor(source: Parcel) : this( |  | ||||||
|         title = source.readString().orEmpty(), |  | ||||||
|         content = source.readString(), |  | ||||||
|         date_published = source.readString().orEmpty(), |  | ||||||
|         lead_image_url = source.readString(), |  | ||||||
|         dek = source.readString().orEmpty(), |  | ||||||
|         url = source.readString().orEmpty(), |  | ||||||
|         domain = source.readString().orEmpty(), |  | ||||||
|         excerpt = source.readString().orEmpty(), |  | ||||||
|         total_pages = source.readInt(), |  | ||||||
|         rendered_pages = source.readInt(), |  | ||||||
|         next_page_url = source.readString().orEmpty() |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     override fun describeContents() = 0 |  | ||||||
|  |  | ||||||
|     override fun writeToParcel(dest: Parcel, flags: Int) { |  | ||||||
|         dest.writeString(title) |  | ||||||
|         dest.writeString(content) |  | ||||||
|         dest.writeString(date_published) |  | ||||||
|         dest.writeString(lead_image_url) |  | ||||||
|         dest.writeString(dek) |  | ||||||
|         dest.writeString(url) |  | ||||||
|         dest.writeString(domain) |  | ||||||
|         dest.writeString(excerpt) |  | ||||||
|         dest.writeInt(total_pages) |  | ||||||
|         dest.writeInt(rendered_pages) |  | ||||||
|         dest.writeString(next_page_url) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.mercury |  | ||||||
|  |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.http.GET |  | ||||||
| import retrofit2.http.Header |  | ||||||
| import retrofit2.http.Query |  | ||||||
|  |  | ||||||
| interface MercuryService { |  | ||||||
|     @GET("parser.php") |  | ||||||
|     fun parseUrl(@Query("link") link: String): Call<ParsedContent> |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.selfoss |  | ||||||
|  |  | ||||||
| import com.google.gson.JsonDeserializationContext |  | ||||||
| import com.google.gson.JsonDeserializer |  | ||||||
| import com.google.gson.JsonElement |  | ||||||
| import com.google.gson.JsonParseException |  | ||||||
| import java.lang.reflect.Type |  | ||||||
|  |  | ||||||
| internal class BooleanTypeAdapter : JsonDeserializer<Boolean> { |  | ||||||
|  |  | ||||||
|     @Throws(JsonParseException::class) |  | ||||||
|     override fun deserialize( |  | ||||||
|         json: JsonElement, |  | ||||||
|         typeOfT: Type, |  | ||||||
|         context: JsonDeserializationContext |  | ||||||
|     ): Boolean? = |  | ||||||
|         try { |  | ||||||
|             json.asInt == 1 |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             json.asBoolean |  | ||||||
|         } |  | ||||||
| } |  | ||||||
| @@ -1,245 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.selfoss |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import android.content.Context |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.SharedItems |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient |  | ||||||
| import com.burgstaller.okhttp.AuthenticationCacheInterceptor |  | ||||||
| import com.burgstaller.okhttp.CachingAuthenticatorDecorator |  | ||||||
| import com.burgstaller.okhttp.DispatchingAuthenticator |  | ||||||
| import com.burgstaller.okhttp.basic.BasicAuthenticator |  | ||||||
| import com.burgstaller.okhttp.digest.CachingAuthenticator |  | ||||||
| import com.burgstaller.okhttp.digest.Credentials |  | ||||||
| import com.burgstaller.okhttp.digest.DigestAuthenticator |  | ||||||
| import com.google.gson.GsonBuilder |  | ||||||
| import okhttp3.* |  | ||||||
| import okhttp3.MediaType.Companion.toMediaTypeOrNull |  | ||||||
| import okhttp3.ResponseBody.Companion.toResponseBody |  | ||||||
| import okhttp3.logging.HttpLoggingInterceptor |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Retrofit |  | ||||||
| import retrofit2.converter.gson.GsonConverterFactory |  | ||||||
| import java.net.SocketTimeoutException |  | ||||||
| import java.util.concurrent.ConcurrentHashMap |  | ||||||
| import java.util.concurrent.TimeUnit |  | ||||||
|  |  | ||||||
| class SelfossApi( |  | ||||||
|     c: Context, |  | ||||||
|     callingActivity: Activity?, |  | ||||||
|     isWithSelfSignedCert: Boolean, |  | ||||||
|     timeout: Long |  | ||||||
| ) { |  | ||||||
|  |  | ||||||
|     private lateinit var service: SelfossService |  | ||||||
|     private val config: Config = Config(c) |  | ||||||
|     private val userName: String |  | ||||||
|     private val password: String |  | ||||||
|  |  | ||||||
|     fun OkHttpClient.Builder.maybeWithSelfSigned(isWithSelfSignedCert: Boolean): OkHttpClient.Builder = |  | ||||||
|         if (isWithSelfSignedCert) { |  | ||||||
|             getUnsafeHttpClient() |  | ||||||
|         } else { |  | ||||||
|             this |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     fun OkHttpClient.Builder.maybeWithSettingsTimeout(timeout: Long): OkHttpClient.Builder = |  | ||||||
|         if (timeout != -1L) { |  | ||||||
|             this.readTimeout(timeout, TimeUnit.SECONDS) |  | ||||||
|                 .connectTimeout(timeout, TimeUnit.SECONDS) |  | ||||||
|         } else { |  | ||||||
|             this |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     fun Credentials.createAuthenticator(): DispatchingAuthenticator = |  | ||||||
|         DispatchingAuthenticator.Builder() |  | ||||||
|             .with("digest", DigestAuthenticator(this)) |  | ||||||
|             .with("basic", BasicAuthenticator(this)) |  | ||||||
|             .build() |  | ||||||
|  |  | ||||||
|     fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean, timeout: Long): OkHttpClient.Builder { |  | ||||||
|         val authCache = ConcurrentHashMap<String, CachingAuthenticator>() |  | ||||||
|         return OkHttpClient |  | ||||||
|             .Builder() |  | ||||||
|             .maybeWithSettingsTimeout(timeout) |  | ||||||
|             .maybeWithSelfSigned(isWithSelfSignedCert) |  | ||||||
|             .authenticator(CachingAuthenticatorDecorator(this, authCache)) |  | ||||||
|             .addInterceptor(AuthenticationCacheInterceptor(authCache)) |  | ||||||
|             .addInterceptor(object: Interceptor { |  | ||||||
|                 override fun intercept(chain: Interceptor.Chain): Response { |  | ||||||
|                     val request: Request = chain.request() |  | ||||||
|                     val response: Response = chain.proceed(request) |  | ||||||
|  |  | ||||||
|                     if (response.code == 408) { |  | ||||||
|                         return response |  | ||||||
|                     } |  | ||||||
|                     return response |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     init { |  | ||||||
|         userName = config.userLogin |  | ||||||
|         password = config.userPassword |  | ||||||
|  |  | ||||||
|         val authenticator = |  | ||||||
|             Credentials( |  | ||||||
|                 config.httpUserLogin, |  | ||||||
|                 config.httpUserPassword |  | ||||||
|             ).createAuthenticator() |  | ||||||
|  |  | ||||||
|         val gson = |  | ||||||
|             GsonBuilder() |  | ||||||
|                 .registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter()) |  | ||||||
|                 .registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter()) |  | ||||||
|                 .setLenient() |  | ||||||
|                 .create() |  | ||||||
|  |  | ||||||
|         val logging = HttpLoggingInterceptor() |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         logging.level = HttpLoggingInterceptor.Level.NONE |  | ||||||
|         val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout) |  | ||||||
|  |  | ||||||
|         val timeoutCode = 504 |  | ||||||
|         httpClient |  | ||||||
|                 .addInterceptor { chain -> |  | ||||||
|                     val res = chain.proceed(chain.request()) |  | ||||||
|                     if (res.code == timeoutCode) { |  | ||||||
|                         throw SocketTimeoutException("timeout") |  | ||||||
|                     } |  | ||||||
|                     res |  | ||||||
|                 } |  | ||||||
|                 .addInterceptor(logging) |  | ||||||
|                 .addInterceptor { chain -> |  | ||||||
|                     val request = chain.request() |  | ||||||
|                     try { |  | ||||||
|                         chain.proceed(request) |  | ||||||
|                     } catch (e: SocketTimeoutException) { |  | ||||||
|                         Response.Builder() |  | ||||||
|                                 .code(timeoutCode) |  | ||||||
|                                 .protocol(Protocol.HTTP_2) |  | ||||||
|                                 .body("".toResponseBody("text/plain".toMediaTypeOrNull())) |  | ||||||
|                                 .message("") |  | ||||||
|                                 .request(request) |  | ||||||
|                                 .build() |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             val retrofit = |  | ||||||
|                 Retrofit |  | ||||||
|                     .Builder() |  | ||||||
|                     .baseUrl(config.baseUrl) |  | ||||||
|                     .client(httpClient.build()) |  | ||||||
|                     .addConverterFactory(GsonConverterFactory.create(gson)) |  | ||||||
|                     .build() |  | ||||||
|             service = retrofit.create(SelfossService::class.java) |  | ||||||
|         } catch (e: IllegalArgumentException) { |  | ||||||
|             if (callingActivity != null) { |  | ||||||
|                 Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun login(): Call<SuccessResponse> = |  | ||||||
|         service.loginToSelfoss(config.userLogin, config.userPassword) |  | ||||||
|  |  | ||||||
|     suspend fun readItems( |  | ||||||
|         itemsNumber: Int, |  | ||||||
|         offset: Int |  | ||||||
|     ): retrofit2.Response<List<Item>> = |  | ||||||
|         getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) |  | ||||||
|  |  | ||||||
|     suspend fun newItems( |  | ||||||
|         itemsNumber: Int, |  | ||||||
|         offset: Int |  | ||||||
|     ): retrofit2.Response<List<Item>> = |  | ||||||
|         getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) |  | ||||||
|  |  | ||||||
|     suspend fun starredItems( |  | ||||||
|         itemsNumber: Int, |  | ||||||
|         offset: Int |  | ||||||
|     ): retrofit2.Response<List<Item>> = |  | ||||||
|         getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) |  | ||||||
|  |  | ||||||
|     fun allItems(): Call<List<Item>> = |  | ||||||
|         service.allItems(userName, password) |  | ||||||
|  |  | ||||||
|     suspend fun allNewItems(): retrofit2.Response<List<Item>> = |  | ||||||
|             getItems("unread", null, null, null, 200, 0) |  | ||||||
|  |  | ||||||
|     suspend fun allReadItems(): retrofit2.Response<List<Item>> = |  | ||||||
|             getItems("read", null, null, null, 200, 0) |  | ||||||
|  |  | ||||||
|     suspend fun allStarredItems(): retrofit2.Response<List<Item>> = |  | ||||||
|         getItems("read", null, null, null, 200, 0) |  | ||||||
|  |  | ||||||
|     private suspend fun getItems( |  | ||||||
|         type: String, |  | ||||||
|         tag: String?, |  | ||||||
|         sourceId: Long?, |  | ||||||
|         search: String?, |  | ||||||
|         items: Int, |  | ||||||
|         offset: Int |  | ||||||
|     ): retrofit2.Response<List<Item>> = |  | ||||||
|         service.getItems(type, tag, sourceId, search, null, userName, password, items, offset) |  | ||||||
|  |  | ||||||
|     suspend fun updateItems( |  | ||||||
|         updatedSince: String |  | ||||||
|     ): retrofit2.Response<List<Item>> = |  | ||||||
|         service.getItems("read", null, null, null, updatedSince, userName, password, 200, 0) |  | ||||||
|  |  | ||||||
|     fun markItem(itemId: String): Call<SuccessResponse> = |  | ||||||
|         service.markAsRead(itemId, userName, password) |  | ||||||
|  |  | ||||||
|     fun unmarkItem(itemId: String): Call<SuccessResponse> = |  | ||||||
|         service.unmarkAsRead(itemId, userName, password) |  | ||||||
|  |  | ||||||
|     suspend fun readAll(ids: List<String>): SuccessResponse = |  | ||||||
|         service.markAllAsRead(ids, userName, password) |  | ||||||
|  |  | ||||||
|     fun starrItem(itemId: String): Call<SuccessResponse> = |  | ||||||
|         service.starr(itemId, userName, password) |  | ||||||
|  |  | ||||||
|     fun unstarrItem(itemId: String): Call<SuccessResponse> = |  | ||||||
|         service.unstarr(itemId, userName, password) |  | ||||||
|  |  | ||||||
|     suspend fun stats(): retrofit2.Response<Stats> = service.stats(userName, password) |  | ||||||
|  |  | ||||||
|     val tags: Call<List<Tag>> |  | ||||||
|         get() = service.tags(userName, password) |  | ||||||
|  |  | ||||||
|     fun update(): Call<String> = |  | ||||||
|         service.update(userName, password) |  | ||||||
|  |  | ||||||
|     val apiVersion: Call<ApiVersion> |  | ||||||
|         get() = service.version() |  | ||||||
|  |  | ||||||
|     val sources: Call<List<Source>> |  | ||||||
|         get() = service.sources(userName, password) |  | ||||||
|  |  | ||||||
|     fun deleteSource(id: String): Call<SuccessResponse> = |  | ||||||
|         service.deleteSource(id, userName, password) |  | ||||||
|  |  | ||||||
|     fun spouts(): Call<Map<String, Spout>> = |  | ||||||
|         service.spouts(userName, password) |  | ||||||
|  |  | ||||||
|     fun createSource( |  | ||||||
|         title: String, |  | ||||||
|         url: String, |  | ||||||
|         spout: String, |  | ||||||
|         tags: String, |  | ||||||
|         filter: String |  | ||||||
|     ): Call<SuccessResponse> = |  | ||||||
|         service.createSource(title, url, spout, tags, filter, userName, password) |  | ||||||
|  |  | ||||||
|     fun createSourceApi2( |  | ||||||
|         title: String, |  | ||||||
|         url: String, |  | ||||||
|         spout: String, |  | ||||||
|         tags: List<String>, |  | ||||||
|         filter: String |  | ||||||
|     ): Call<SuccessResponse> = |  | ||||||
|         service.createSourceApi2(title, url, spout, tags, filter, userName, password) |  | ||||||
| } |  | ||||||
| @@ -1,134 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.selfoss |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.SharedItems |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable |  | ||||||
| import kotlinx.coroutines.* |  | ||||||
| import retrofit2.Response |  | ||||||
|  |  | ||||||
| suspend fun getAndStoreAllItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) { |  | ||||||
|     if (isNetworkAvailable(context)) { |  | ||||||
|         launch { |  | ||||||
|             try { |  | ||||||
|                 enqueueArticles(api.allNewItems(), db, true) |  | ||||||
|             } catch (e: Throwable) {} |  | ||||||
|         } |  | ||||||
|         launch { |  | ||||||
|             try { |  | ||||||
|                 enqueueArticles(api.allReadItems(), db, false) |  | ||||||
|             } catch (e: Throwable) {} |  | ||||||
|         } |  | ||||||
|         launch { |  | ||||||
|             try { |  | ||||||
|                 enqueueArticles(api.allStarredItems(), db, false) |  | ||||||
|             } catch (e: Throwable) {} |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         launch { SharedItems.updateDatabase(db) } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| suspend fun updateItems(context: Context, api: SelfossApi, db: AppDatabase) = coroutineScope { |  | ||||||
|     if (isNetworkAvailable(context)) { |  | ||||||
|         launch { |  | ||||||
|             try { |  | ||||||
|                 enqueueArticles(api.updateItems(SharedItems.items[0].datetime), db, true) |  | ||||||
|             } catch (e: Throwable) {} |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| suspend fun refreshFocusedItems(context: Context, api: SelfossApi, db: AppDatabase, itemsNumber: Int) = withContext(Dispatchers.IO) { |  | ||||||
|     if (isNetworkAvailable(context)) { |  | ||||||
|         val response = when (SharedItems.displayedItems) { |  | ||||||
|             "read" -> api.readItems(itemsNumber, 0) |  | ||||||
|             "unread" -> api.newItems(itemsNumber, 0) |  | ||||||
|             "starred" -> api.starredItems(itemsNumber, 0) |  | ||||||
|             else -> api.readItems(itemsNumber, 0) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (response.isSuccessful) { |  | ||||||
|             SharedItems.refreshFocusedItems(response.body() as ArrayList<Item>) |  | ||||||
|             SharedItems.updateDatabase(db) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| suspend fun getReadItems(context: Context, api: SelfossApi, db: AppDatabase, itemsNumber: Int, offset: Int) = withContext(Dispatchers.IO) { |  | ||||||
|     if (isNetworkAvailable(context)) { |  | ||||||
|             try { |  | ||||||
|                 enqueueArticles(api.readItems( itemsNumber, offset), db, false) |  | ||||||
|                 SharedItems.fetchedAll = true |  | ||||||
|                 SharedItems.updateDatabase(db) |  | ||||||
|             } catch (e: Throwable) {} |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| suspend fun getUnreadItems(context: Context, api: SelfossApi, db: AppDatabase, itemsNumber: Int, offset: Int) = withContext(Dispatchers.IO) { |  | ||||||
|     if (isNetworkAvailable(context)) { |  | ||||||
|         try { |  | ||||||
|             if (!SharedItems.fetchedUnread) { |  | ||||||
|                 SharedItems.clearDBItems(db) |  | ||||||
|             } |  | ||||||
|             enqueueArticles(api.newItems(itemsNumber, offset), db, false) |  | ||||||
|             SharedItems.fetchedUnread = true |  | ||||||
|         } catch (e: Throwable) {} |  | ||||||
|     } |  | ||||||
|     SharedItems.updateDatabase(db) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| suspend fun getStarredItems(context: Context, api: SelfossApi, db: AppDatabase, itemsNumber: Int, offset: Int) = withContext(Dispatchers.IO) { |  | ||||||
|     if (isNetworkAvailable(context)) { |  | ||||||
|         try { |  | ||||||
|             enqueueArticles(api.starredItems(itemsNumber, offset), db, false) |  | ||||||
|             SharedItems.fetchedStarred = true |  | ||||||
|             SharedItems.updateDatabase(db) |  | ||||||
|         } catch (e: Throwable) { |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| suspend fun readAll(context: Context, api: SelfossApi, db: AppDatabase): Boolean { |  | ||||||
|     var success = false |  | ||||||
|     if (isNetworkAvailable(context)) { |  | ||||||
|         try { |  | ||||||
|             val ids = SharedItems.focusedItems.map { it.id } |  | ||||||
|             if (ids.isNotEmpty()) { |  | ||||||
|                 val result = api.readAll(ids) |  | ||||||
|                 SharedItems.readItems(db, ids) |  | ||||||
|                 success = result.isSuccess |  | ||||||
|             } |  | ||||||
|         } catch (e: Throwable) {} |  | ||||||
|     } |  | ||||||
|     return success |  | ||||||
| } |  | ||||||
|  |  | ||||||
| suspend fun reloadBadges(context: Context, api: SelfossApi) = withContext(Dispatchers.IO) { |  | ||||||
|     if (isNetworkAvailable(context)) { |  | ||||||
|         try { |  | ||||||
|             val response = api.stats() |  | ||||||
|  |  | ||||||
|             if (response.isSuccessful) { |  | ||||||
|                 val badges = response.body() |  | ||||||
|                 SharedItems.badgeUnread = badges!!.unread |  | ||||||
|                 SharedItems.badgeAll = badges.total |  | ||||||
|                 SharedItems.badgeStarred = badges.starred |  | ||||||
|             } |  | ||||||
|         } catch (e: Throwable) {} |  | ||||||
|     } else { |  | ||||||
|         SharedItems.computeBadges() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| private fun enqueueArticles(response: Response<List<Item>>, db: AppDatabase, clearDatabase: Boolean) { |  | ||||||
|         if (response.isSuccessful) { |  | ||||||
|             if (clearDatabase) { |  | ||||||
|                 CoroutineScope(Dispatchers.IO).launch { |  | ||||||
|                     SharedItems.clearDBItems(db) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             val allItems = response.body() as ArrayList<Item> |  | ||||||
|             SharedItems.appendNewItems(allItems) |  | ||||||
|         } |  | ||||||
| } |  | ||||||
| @@ -1,253 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.selfoss |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Parcel |  | ||||||
| import android.os.Parcelable |  | ||||||
| import android.text.Html |  | ||||||
| import android.webkit.URLUtil |  | ||||||
| import org.jsoup.Jsoup |  | ||||||
|  |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString |  | ||||||
| import com.bumptech.glide.Glide |  | ||||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy |  | ||||||
| import com.bumptech.glide.request.RequestOptions |  | ||||||
| import com.google.gson.annotations.SerializedName |  | ||||||
| import java.util.* |  | ||||||
| import kotlin.collections.ArrayList |  | ||||||
|  |  | ||||||
| private fun constructUrl(config: Config?, path: String, file: String?): String { |  | ||||||
|     return if (file.isEmptyOrNullOrNullString()) { |  | ||||||
|         "" |  | ||||||
|     } else { |  | ||||||
|         val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon() |  | ||||||
|         baseUriBuilder.appendPath(path).appendPath(file) |  | ||||||
|  |  | ||||||
|         baseUriBuilder.toString() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| data class Tag( |  | ||||||
|     @SerializedName("tag") val tag: String, |  | ||||||
|     @SerializedName("color") val color: String, |  | ||||||
|     @SerializedName("unread") val unread: Int |  | ||||||
| ) { |  | ||||||
|     fun getTitleDecoded(): String { |  | ||||||
|         return Html.fromHtml(tag).toString() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class SuccessResponse(@SerializedName("success") val success: Boolean) { |  | ||||||
|     val isSuccess: Boolean |  | ||||||
|         get() = success |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class Stats( |  | ||||||
|     @SerializedName("total") val total: Int, |  | ||||||
|     @SerializedName("unread") val unread: Int, |  | ||||||
|     @SerializedName("starred") val starred: Int |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| data class Spout( |  | ||||||
|     @SerializedName("name") val name: String, |  | ||||||
|     @SerializedName("description") val description: String |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| data class ApiVersion( |  | ||||||
|         @SerializedName("version") val version: String?, |  | ||||||
|         @SerializedName("apiversion") val apiversion: String? |  | ||||||
| ) { |  | ||||||
|     fun getApiMajorVersion() : Int { |  | ||||||
|         var versionNumber = 0 |  | ||||||
|         if (apiversion != null) { |  | ||||||
|             versionNumber = apiversion.substringBefore(".").toInt() |  | ||||||
|         } |  | ||||||
|         return versionNumber |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| data class Source( |  | ||||||
|     @SerializedName("id") val id: String, |  | ||||||
|     @SerializedName("title") val title: String, |  | ||||||
|     @SerializedName("tags") val tags: SelfossTagType, |  | ||||||
|     @SerializedName("spout") val spout: String, |  | ||||||
|     @SerializedName("error") val error: String, |  | ||||||
|     @SerializedName("icon") val icon: String |  | ||||||
| ) { |  | ||||||
|     var config: Config? = null |  | ||||||
|  |  | ||||||
|     fun getIcon(app: Context): String { |  | ||||||
|         if (config == null) { |  | ||||||
|             config = Config(app) |  | ||||||
|         } |  | ||||||
|         return constructUrl(config, "favicons", icon) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getTitleDecoded(): String { |  | ||||||
|         return Html.fromHtml(title).toString() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| data class Item( |  | ||||||
|     @SerializedName("id") val id: String, |  | ||||||
|     @SerializedName("datetime") val datetime: String, |  | ||||||
|     @SerializedName("title") val title: String, |  | ||||||
|     @SerializedName("content") val content: String, |  | ||||||
|     @SerializedName("unread") var unread: Boolean, |  | ||||||
|     @SerializedName("starred") var starred: Boolean, |  | ||||||
|     @SerializedName("thumbnail") val thumbnail: String?, |  | ||||||
|     @SerializedName("icon") val icon: String?, |  | ||||||
|     @SerializedName("link") val link: String, |  | ||||||
|     @SerializedName("sourcetitle") val sourcetitle: String, |  | ||||||
|     @SerializedName("tags") val tags: SelfossTagType |  | ||||||
| ) : Parcelable { |  | ||||||
|  |  | ||||||
|     var config: Config? = null |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         @JvmField val CREATOR: Parcelable.Creator<Item> = object : Parcelable.Creator<Item> { |  | ||||||
|             override fun createFromParcel(source: Parcel): Item = Item(source) |  | ||||||
|             override fun newArray(size: Int): Array<Item?> = arrayOfNulls(size) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     constructor(source: Parcel) : this( |  | ||||||
|         id = source.readString().orEmpty(), |  | ||||||
|         datetime = source.readString().orEmpty(), |  | ||||||
|         title = source.readString().orEmpty(), |  | ||||||
|         content = source.readString().orEmpty(), |  | ||||||
|         unread = 0.toByte() != source.readByte(), |  | ||||||
|         starred = 0.toByte() != source.readByte(), |  | ||||||
|         thumbnail = source.readString(), |  | ||||||
|         icon = source.readString(), |  | ||||||
|         link = source.readString().orEmpty(), |  | ||||||
|         sourcetitle = source.readString().orEmpty(), |  | ||||||
|         tags = if (source.readParcelable<SelfossTagType>(ClassLoader.getSystemClassLoader()) != null) source.readParcelable(ClassLoader.getSystemClassLoader())!! else SelfossTagType("") |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     override fun describeContents() = 0 |  | ||||||
|  |  | ||||||
|     override fun writeToParcel(dest: Parcel, flags: Int) { |  | ||||||
|         dest.writeString(id) |  | ||||||
|         dest.writeString(datetime) |  | ||||||
|         dest.writeString(title) |  | ||||||
|         dest.writeString(content) |  | ||||||
|         dest.writeByte((if (unread) 1 else 0)) |  | ||||||
|         dest.writeByte((if (starred) 1 else 0)) |  | ||||||
|         dest.writeString(thumbnail) |  | ||||||
|         dest.writeString(icon) |  | ||||||
|         dest.writeString(link) |  | ||||||
|         dest.writeString(sourcetitle) |  | ||||||
|         dest.writeParcelable(tags, flags) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getIcon(app: Context): String { |  | ||||||
|         if (config == null) { |  | ||||||
|             config = Config(app) |  | ||||||
|         } |  | ||||||
|         return constructUrl(config, "favicons", icon) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getThumbnail(app: Context): String { |  | ||||||
|         if (config == null) { |  | ||||||
|             config = Config(app) |  | ||||||
|         } |  | ||||||
|         return constructUrl(config, "thumbnails", thumbnail) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getImages() : ArrayList<String> { |  | ||||||
|         val allImages = ArrayList<String>() |  | ||||||
|  |  | ||||||
|         for ( image in Jsoup.parse(content).getElementsByTag("img")) { |  | ||||||
|             val url = image.attr("src") |  | ||||||
|             if (url.lowercase(Locale.US).contains(".jpg") || |  | ||||||
|                     url.lowercase(Locale.US).contains(".jpeg") || |  | ||||||
|                     url.lowercase(Locale.US).contains(".png") || |  | ||||||
|                     url.lowercase(Locale.US).contains(".webp")) |  | ||||||
|             { |  | ||||||
|                 allImages.add(url) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return allImages |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun preloadImages(context: Context) : Boolean { |  | ||||||
|         val imageUrls = this.getImages() |  | ||||||
|  |  | ||||||
|         val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             for (url in imageUrls) { |  | ||||||
|                 if ( URLUtil.isValidUrl(url)) { |  | ||||||
|                     val image = Glide.with(context).asBitmap() |  | ||||||
|                             .apply(glideOptions) |  | ||||||
|                             .load(url).submit() |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } catch (e : Error) { |  | ||||||
|             return false |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getTitleDecoded(): String { |  | ||||||
|         return Html.fromHtml(title).toString() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getSourceTitle(): String { |  | ||||||
|         return Html.fromHtml(sourcetitle).toString() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // TODO: maybe find a better way to handle these kind of urls |  | ||||||
|     fun getLinkDecoded(): String { |  | ||||||
|         var stringUrl: String |  | ||||||
|         stringUrl = |  | ||||||
|                 if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) { |  | ||||||
|                     if (link.contains("&url=")) { |  | ||||||
|                         link.substringAfter("&url=") |  | ||||||
|                     } else { |  | ||||||
|                         this.link.replace("&", "&") |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     this.link.replace("&", "&") |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|         // handle :443 => https |  | ||||||
|         if (stringUrl.contains(":443")) { |  | ||||||
|             stringUrl = stringUrl.replace(":443", "").replace("http://", "https://") |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // handle url not starting with http |  | ||||||
|         if (stringUrl.startsWith("//")) { |  | ||||||
|             stringUrl = "http:$stringUrl" |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return stringUrl |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| data class SelfossTagType(val tags: String) : Parcelable { |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         @JvmField val CREATOR: Parcelable.Creator<SelfossTagType> = |  | ||||||
|             object : Parcelable.Creator<SelfossTagType> { |  | ||||||
|                 override fun createFromParcel(source: Parcel): SelfossTagType = |  | ||||||
|                     SelfossTagType(source) |  | ||||||
|  |  | ||||||
|                 override fun newArray(size: Int): Array<SelfossTagType?> = arrayOfNulls(size) |  | ||||||
|             } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     constructor(source: Parcel) : this( |  | ||||||
|         tags = source.readString().orEmpty() |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     override fun describeContents() = 0 |  | ||||||
|  |  | ||||||
|     override fun writeToParcel(dest: Parcel, flags: Int) { |  | ||||||
|         dest.writeString(tags) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,141 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.selfoss |  | ||||||
|  |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Response |  | ||||||
| import retrofit2.http.DELETE |  | ||||||
| import retrofit2.http.Field |  | ||||||
| import retrofit2.http.FormUrlEncoded |  | ||||||
| import retrofit2.http.GET |  | ||||||
| import retrofit2.http.Headers |  | ||||||
| import retrofit2.http.POST |  | ||||||
| import retrofit2.http.Path |  | ||||||
| import retrofit2.http.Query |  | ||||||
|  |  | ||||||
| internal interface SelfossService { |  | ||||||
|  |  | ||||||
|     @GET("login") |  | ||||||
|     fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse> |  | ||||||
|  |  | ||||||
|     @GET("items") |  | ||||||
|     suspend fun getItems( |  | ||||||
|         @Query("type") type: String, |  | ||||||
|         @Query("tag") tag: String?, |  | ||||||
|         @Query("source") source: Long?, |  | ||||||
|         @Query("search") search: String?, |  | ||||||
|         @Query("updatedsince") updatedSince: String?, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String, |  | ||||||
|         @Query("items") items: Int, |  | ||||||
|         @Query("offset") offset: Int |  | ||||||
|     ): Response<List<Item>> |  | ||||||
|  |  | ||||||
|     @GET("items") |  | ||||||
|     fun allItems( |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<List<Item>> |  | ||||||
|  |  | ||||||
|     @Headers("Content-Type: application/x-www-form-urlencoded") |  | ||||||
|     @POST("mark/{id}") |  | ||||||
|     fun markAsRead( |  | ||||||
|         @Path("id") id: String, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<SuccessResponse> |  | ||||||
|  |  | ||||||
|     @Headers("Content-Type: application/x-www-form-urlencoded") |  | ||||||
|     @POST("unmark/{id}") |  | ||||||
|     fun unmarkAsRead( |  | ||||||
|         @Path("id") id: String, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<SuccessResponse> |  | ||||||
|  |  | ||||||
|     @FormUrlEncoded |  | ||||||
|     @POST("mark") |  | ||||||
|     suspend fun markAllAsRead( |  | ||||||
|         @Field("ids[]") ids: List<String>, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): SuccessResponse |  | ||||||
|  |  | ||||||
|     @Headers("Content-Type: application/x-www-form-urlencoded") |  | ||||||
|     @POST("starr/{id}") |  | ||||||
|     fun starr( |  | ||||||
|         @Path("id") id: String, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<SuccessResponse> |  | ||||||
|  |  | ||||||
|     @Headers("Content-Type: application/x-www-form-urlencoded") |  | ||||||
|     @POST("unstarr/{id}") |  | ||||||
|     fun unstarr( |  | ||||||
|         @Path("id") id: String, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<SuccessResponse> |  | ||||||
|  |  | ||||||
|     @GET("stats") |  | ||||||
|     suspend fun stats( |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Response<Stats> |  | ||||||
|  |  | ||||||
|     @GET("tags") |  | ||||||
|     fun tags( |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<List<Tag>> |  | ||||||
|  |  | ||||||
|     @GET("update") |  | ||||||
|     fun update( |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<String> |  | ||||||
|  |  | ||||||
|     @GET("sources/spouts") |  | ||||||
|     fun spouts( |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<Map<String, Spout>> |  | ||||||
|  |  | ||||||
|     @GET("sources/list") |  | ||||||
|     fun sources( |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<List<Source>> |  | ||||||
|  |  | ||||||
|     @GET("api/about") |  | ||||||
|     fun version(): Call<ApiVersion> |  | ||||||
|  |  | ||||||
|     @DELETE("source/{id}") |  | ||||||
|     fun deleteSource( |  | ||||||
|         @Path("id") id: String, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<SuccessResponse> |  | ||||||
|  |  | ||||||
|     @FormUrlEncoded |  | ||||||
|     @POST("source") |  | ||||||
|     fun createSource( |  | ||||||
|         @Field("title") title: String, |  | ||||||
|         @Field("url") url: String, |  | ||||||
|         @Field("spout") spout: String, |  | ||||||
|         @Field("tags") tags: String, |  | ||||||
|         @Field("filter") filter: String, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<SuccessResponse> |  | ||||||
|  |  | ||||||
|     @FormUrlEncoded |  | ||||||
|     @POST("source") |  | ||||||
|     fun createSourceApi2( |  | ||||||
|         @Field("title") title: String, |  | ||||||
|         @Field("url") url: String, |  | ||||||
|         @Field("spout") spout: String, |  | ||||||
|         @Field("tags[]") tags: List<String>, |  | ||||||
|         @Field("filter") filter: String, |  | ||||||
|         @Query("username") username: String, |  | ||||||
|         @Query("password") password: String |  | ||||||
|     ): Call<SuccessResponse> |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.api.selfoss |  | ||||||
|  |  | ||||||
| import com.google.gson.JsonDeserializationContext |  | ||||||
| import com.google.gson.JsonDeserializer |  | ||||||
| import com.google.gson.JsonElement |  | ||||||
| import com.google.gson.JsonParseException |  | ||||||
| import java.lang.reflect.Type |  | ||||||
|  |  | ||||||
| internal class SelfossTagTypeTypeAdapter : JsonDeserializer<SelfossTagType> { |  | ||||||
|  |  | ||||||
|     @Throws(JsonParseException::class) |  | ||||||
|     override fun deserialize( |  | ||||||
|         json: JsonElement, |  | ||||||
|         typeOfT: Type, |  | ||||||
|         context: JsonDeserializationContext |  | ||||||
|     ): SelfossTagType? = |  | ||||||
|         if (json.isJsonArray) { |  | ||||||
|             SelfossTagType(json.asJsonArray.joinToString(",") { it.toString() }) |  | ||||||
|         } else { |  | ||||||
|             SelfossTagType(json.toString()) |  | ||||||
|         } |  | ||||||
| } |  | ||||||
| @@ -1,169 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.background |  | ||||||
|  |  | ||||||
| import android.app.NotificationManager |  | ||||||
| import android.app.PendingIntent |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Build |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import androidx.core.app.NotificationCompat |  | ||||||
| import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT |  | ||||||
| import androidx.core.app.NotificationCompat.PRIORITY_LOW |  | ||||||
| import androidx.room.Room |  | ||||||
| import androidx.work.Worker |  | ||||||
| import androidx.work.WorkerParameters |  | ||||||
| import apps.amine.bou.readerforselfoss.MainActivity |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.getAndStoreAllItems |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4 |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.SharedItems |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
| import kotlinx.coroutines.launch |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Callback |  | ||||||
| import retrofit2.Response |  | ||||||
| import java.util.* |  | ||||||
| import kotlin.concurrent.schedule |  | ||||||
| import kotlin.concurrent.thread |  | ||||||
|  |  | ||||||
| class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { |  | ||||||
|     lateinit var db: AppDatabase |  | ||||||
|  |  | ||||||
| override fun doWork(): Result { |  | ||||||
|     val settings = |  | ||||||
|         this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|     val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context) |  | ||||||
|     val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false) |  | ||||||
|     if (periodicRefresh) { |  | ||||||
|         val api = SelfossApi( |  | ||||||
|             this.context, |  | ||||||
|             null, |  | ||||||
|             settings.getBoolean("isSelfSignedCert", false), |  | ||||||
|             sharedPref.getString("api_timeout", "-1")!!.toLong() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         if (isNetworkAvailable(context)) { |  | ||||||
|  |  | ||||||
|             CoroutineScope(Dispatchers.IO).launch { |  | ||||||
|                 val notificationManager = |  | ||||||
|                     applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager |  | ||||||
|  |  | ||||||
|                 val notification = |  | ||||||
|                     NotificationCompat.Builder(applicationContext, Config.syncChannelId) |  | ||||||
|                         .setContentTitle(context.getString(R.string.loading_notification_title)) |  | ||||||
|                         .setContentText(context.getString(R.string.loading_notification_text)) |  | ||||||
|                         .setOngoing(true) |  | ||||||
|                         .setPriority(PRIORITY_LOW) |  | ||||||
|                         .setChannelId(Config.syncChannelId) |  | ||||||
|                         .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp) |  | ||||||
|  |  | ||||||
|                 notificationManager.notify(1, notification.build()) |  | ||||||
|  |  | ||||||
|                 val notifyNewItems = sharedPref.getBoolean("notify_new_items", false) |  | ||||||
|  |  | ||||||
|                 db = Room.databaseBuilder( |  | ||||||
|                     applicationContext, |  | ||||||
|                     AppDatabase::class.java, "selfoss-database" |  | ||||||
|                 ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3) |  | ||||||
|                     .addMigrations(MIGRATION_3_4).build() |  | ||||||
|  |  | ||||||
|                 val actions = db.actionsDao().actions() |  | ||||||
|  |  | ||||||
|                 actions.forEach { action -> |  | ||||||
|                     when { |  | ||||||
|                         action.read -> doAndReportOnFail( |  | ||||||
|                             api.markItem(action.articleId), |  | ||||||
|                             action |  | ||||||
|                         ) |  | ||||||
|                         action.unread -> doAndReportOnFail( |  | ||||||
|                             api.unmarkItem(action.articleId), |  | ||||||
|                             action |  | ||||||
|                         ) |  | ||||||
|                         action.starred -> doAndReportOnFail( |  | ||||||
|                             api.starrItem(action.articleId), |  | ||||||
|                             action |  | ||||||
|                         ) |  | ||||||
|                         action.unstarred -> doAndReportOnFail( |  | ||||||
|                             api.unstarrItem(action.articleId), |  | ||||||
|                             action |  | ||||||
|                         ) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 getAndStoreAllItems(context, api, db) |  | ||||||
|                 SharedItems.updateDatabase(db) |  | ||||||
|                 storeItems(notifyNewItems, notificationManager) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     return Result.success() |  | ||||||
| } |  | ||||||
|  |  | ||||||
|     private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) { |  | ||||||
|         CoroutineScope(Dispatchers.IO).launch { |  | ||||||
|                 val apiItems = SharedItems.items |  | ||||||
|  |  | ||||||
|  |  | ||||||
|                 val newSize = apiItems.filter { it.unread }.size |  | ||||||
|                 if (notifyNewItems && newSize > 0) { |  | ||||||
|  |  | ||||||
|                     val intent = Intent(context, MainActivity::class.java).apply { |  | ||||||
|                         flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK |  | ||||||
|                     } |  | ||||||
|                     val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |  | ||||||
|                         PendingIntent.FLAG_IMMUTABLE |  | ||||||
|                     } else { |  | ||||||
|                         0 |  | ||||||
|                     } |  | ||||||
|                     val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags) |  | ||||||
|  |  | ||||||
|                     val newItemsNotification = |  | ||||||
|                         NotificationCompat.Builder(applicationContext, Config.newItemsChannelId) |  | ||||||
|                             .setContentTitle(context.getString(R.string.new_items_notification_title)) |  | ||||||
|                             .setContentText( |  | ||||||
|                                 context.getString( |  | ||||||
|                                     R.string.new_items_notification_text, |  | ||||||
|                                     newSize |  | ||||||
|                                 ) |  | ||||||
|                             ) |  | ||||||
|                             .setPriority(PRIORITY_DEFAULT) |  | ||||||
|                             .setChannelId(Config.newItemsChannelId) |  | ||||||
|                             .setContentIntent(pendingIntent) |  | ||||||
|                             .setAutoCancel(true) |  | ||||||
|                             .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp) |  | ||||||
|  |  | ||||||
|                     Timer("", false).schedule(4000) { |  | ||||||
|                         notificationManager.notify(2, newItemsNotification.build()) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 apiItems.map { it.preloadImages(context) } |  | ||||||
|             Timer("", false).schedule(4000) { |  | ||||||
|                 notificationManager.cancel(1) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) { |  | ||||||
|         call.enqueue(object : Callback<T> { |  | ||||||
|             override fun onResponse( |  | ||||||
|                 call: Call<T>, |  | ||||||
|                 response: Response<T> |  | ||||||
|             ) { |  | ||||||
|                 thread { |  | ||||||
|                     db.actionsDao().delete(action) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             override fun onFailure(call: Call<T>, t: Throwable) { |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,563 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.fragments |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.content.SharedPreferences |  | ||||||
| import android.content.res.ColorStateList |  | ||||||
| import android.content.res.TypedArray |  | ||||||
| import android.graphics.Bitmap |  | ||||||
| import android.graphics.Typeface |  | ||||||
| import android.graphics.drawable.ColorDrawable |  | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Bundle |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import android.view.* |  | ||||||
| import android.webkit.* |  | ||||||
| import android.widget.Toast |  | ||||||
| import com.google.android.material.floatingactionbutton.FloatingActionButton |  | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| import androidx.core.widget.NestedScrollView |  | ||||||
| import androidx.appcompat.app.AlertDialog |  | ||||||
| import androidx.browser.customtabs.CustomTabsIntent |  | ||||||
| import androidx.core.content.res.ResourcesCompat |  | ||||||
| import androidx.room.Room |  | ||||||
| import apps.amine.bou.readerforselfoss.ImageActivity |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi |  | ||||||
| import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4 |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.AppColors |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.* |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible |  | ||||||
| import com.bumptech.glide.Glide |  | ||||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy |  | ||||||
| import com.bumptech.glide.request.RequestOptions |  | ||||||
| import com.github.rubensousa.floatingtoolbar.FloatingToolbar |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Callback |  | ||||||
| import retrofit2.Response |  | ||||||
| import java.net.MalformedURLException |  | ||||||
| import java.net.URL |  | ||||||
| import java.util.* |  | ||||||
| import java.util.concurrent.ExecutionException |  | ||||||
| import kotlin.collections.ArrayList |  | ||||||
|  |  | ||||||
| class ArticleFragment : Fragment() { |  | ||||||
|     private var fontSize: Int = 16 |  | ||||||
|     private lateinit var item: Item |  | ||||||
|     private var mCustomTabActivityHelper: CustomTabActivityHelper? = null |  | ||||||
|     private lateinit var url: String |  | ||||||
|     private lateinit var contentText: String |  | ||||||
|     private lateinit var contentSource: String |  | ||||||
|     private lateinit var contentImage: String |  | ||||||
|     private lateinit var contentTitle: String |  | ||||||
|     private lateinit var allImages : ArrayList<String> |  | ||||||
|     private lateinit var editor: SharedPreferences.Editor |  | ||||||
|     private lateinit var fab: FloatingActionButton |  | ||||||
|     private lateinit var appColors: AppColors |  | ||||||
|     private lateinit var db: AppDatabase |  | ||||||
|     private lateinit var textAlignment: String |  | ||||||
|     private lateinit var config: Config |  | ||||||
|     private var _binding: FragmentArticleBinding? = null |  | ||||||
|     private val binding get() = _binding!! |  | ||||||
|  |  | ||||||
|     private lateinit var prefs: SharedPreferences |  | ||||||
|  |  | ||||||
|     private var typeface: Typeface? = null |  | ||||||
|     private var resId: Int = 0 |  | ||||||
|     private var font = "" |  | ||||||
|     private var staticBar = false |  | ||||||
|  |  | ||||||
|     override fun onStop() { |  | ||||||
|         super.onStop() |  | ||||||
|         if (mCustomTabActivityHelper != null) { |  | ||||||
|             mCustomTabActivityHelper!!.unbindCustomTabsService(activity) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         appColors = AppColors(requireActivity()) |  | ||||||
|         config = Config(requireActivity()) |  | ||||||
|  |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|  |  | ||||||
|         item = requireArguments().getParcelable(ARG_ITEMS)!! |  | ||||||
|  |  | ||||||
|         db = Room.databaseBuilder( |  | ||||||
|             requireContext(), |  | ||||||
|             AppDatabase::class.java, "selfoss-database" |  | ||||||
|         ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onCreateView( |  | ||||||
|         inflater: LayoutInflater, |  | ||||||
|         container: ViewGroup?, |  | ||||||
|         savedInstanceState: Bundle? |  | ||||||
|     ): View { |  | ||||||
|         try { |  | ||||||
|             _binding = FragmentArticleBinding.inflate(inflater, container, false) |  | ||||||
|  |  | ||||||
|             url = item.getLinkDecoded() |  | ||||||
|             contentText = item.content |  | ||||||
|             contentTitle = item.getTitleDecoded() |  | ||||||
|             contentImage = item.getThumbnail(requireActivity()) |  | ||||||
|             contentSource = item.sourceAndDateText() |  | ||||||
|             allImages = item.getImages() |  | ||||||
|  |  | ||||||
|             prefs = PreferenceManager.getDefaultSharedPreferences(activity) |  | ||||||
|             editor = prefs.edit() |  | ||||||
|             fontSize = prefs.getString("reader_font_size", "16")!!.toInt() |  | ||||||
|             staticBar = prefs.getBoolean("reader_static_bar", false) |  | ||||||
|  |  | ||||||
|             font = prefs.getString("reader_font", "")!! |  | ||||||
|             if (font.isNotEmpty()) { |  | ||||||
|                 resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName) |  | ||||||
|                 typeface = try { |  | ||||||
|                     ResourcesCompat.getFont(requireContext(), resId)!! |  | ||||||
|                 } catch (e: java.lang.Exception) { |  | ||||||
|                     // ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext()) |  | ||||||
|                     // Just to be sure |  | ||||||
|                     null |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             refreshAlignment() |  | ||||||
|  |  | ||||||
|             val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|  |  | ||||||
|             val api = SelfossApi( |  | ||||||
|                 requireContext(), |  | ||||||
|                 requireActivity(), |  | ||||||
|                 settings.getBoolean("isSelfSignedCert", false), |  | ||||||
|                 prefs.getString("api_timeout", "-1")!!.toLong() |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             fab = binding.fab |  | ||||||
|  |  | ||||||
|             fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent) |  | ||||||
|  |  | ||||||
|             fab.rippleColor = appColors.colorAccentDark |  | ||||||
|  |  | ||||||
|             val floatingToolbar: FloatingToolbar = binding.floatingToolbar |  | ||||||
|             floatingToolbar.attachFab(fab) |  | ||||||
|  |  | ||||||
|             floatingToolbar.background = ColorDrawable(appColors.colorAccent) |  | ||||||
|  |  | ||||||
|             val customTabsIntent = requireActivity().buildCustomTabsIntent() |  | ||||||
|             mCustomTabActivityHelper = CustomTabActivityHelper() |  | ||||||
|             mCustomTabActivityHelper!!.bindCustomTabsService(activity) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|             floatingToolbar.setClickListener( |  | ||||||
|                 object : FloatingToolbar.ItemClickListener { |  | ||||||
|                     override fun onItemClick(item: MenuItem) { |  | ||||||
|                         when (item.itemId) { |  | ||||||
|                             R.id.more_action -> getContentFromMercury(customTabsIntent) |  | ||||||
|                             R.id.share_action -> requireActivity().shareLink(url, contentTitle) |  | ||||||
|                             R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) |  | ||||||
|                             R.id.unread_action -> if (context != null) { |  | ||||||
|                                 if (this@ArticleFragment.item.unread) { |  | ||||||
|                                     SharedItems.readItem( |  | ||||||
|                                         context!!, |  | ||||||
|                                         api, |  | ||||||
|                                         db, |  | ||||||
|                                             this@ArticleFragment.item |  | ||||||
|                                     ) |  | ||||||
|                                     this@ArticleFragment.item.unread = false |  | ||||||
|                                     Toast.makeText( |  | ||||||
|                                         context, |  | ||||||
|                                         R.string.marked_as_read, |  | ||||||
|                                         Toast.LENGTH_LONG |  | ||||||
|                                     ).show() |  | ||||||
|                                 } else { |  | ||||||
|                                     SharedItems.unreadItem( |  | ||||||
|                                         context!!, |  | ||||||
|                                         api, |  | ||||||
|                                         db, |  | ||||||
|                                             this@ArticleFragment.item |  | ||||||
|                                     ) |  | ||||||
|                                     this@ArticleFragment.item.unread = true |  | ||||||
|                                     Toast.makeText( |  | ||||||
|                                         context, |  | ||||||
|                                         R.string.marked_as_unread, |  | ||||||
|                                         Toast.LENGTH_LONG |  | ||||||
|                                     ).show() |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                             else -> Unit |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     override fun onItemLongClick(item: MenuItem?) { |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             if (staticBar) { |  | ||||||
|                 fab.hide() |  | ||||||
|                 floatingToolbar.show() |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             binding.source.text = contentSource |  | ||||||
|             if (typeface != null) { |  | ||||||
|                 binding.source.typeface = typeface |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (contentText.isEmptyOrNullOrNullString()) { |  | ||||||
|                 getContentFromMercury(customTabsIntent) |  | ||||||
|             } else { |  | ||||||
|                 binding.titleView.text = contentTitle |  | ||||||
|                 if (typeface != null) { |  | ||||||
|                     binding.titleView.typeface = typeface |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 htmlToWebview() |  | ||||||
|  |  | ||||||
|                 if (!contentImage.isEmptyOrNullOrNullString() && context != null) { |  | ||||||
|                     binding.imageView.visibility = View.VISIBLE |  | ||||||
|                     Glide |  | ||||||
|                         .with(requireContext()) |  | ||||||
|                         .asBitmap() |  | ||||||
|                         .loadMaybeBasicAuth(config, contentImage) |  | ||||||
|                         .apply(RequestOptions.fitCenterTransform()) |  | ||||||
|                         .into(binding.imageView) |  | ||||||
|                 } else { |  | ||||||
|                     binding.imageView.visibility = View.GONE |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             binding.nestedScrollView.setOnScrollChangeListener( |  | ||||||
|                 NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> |  | ||||||
|                     if (scrollY > oldScrollY) { |  | ||||||
|                         floatingToolbar.hide() |  | ||||||
|                         fab.hide() |  | ||||||
|                     } else { |  | ||||||
|                         if (staticBar) { |  | ||||||
|                             floatingToolbar.show() |  | ||||||
|                         } else { |  | ||||||
|                             if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show() |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         } catch (e: InflateException) { |  | ||||||
|             AlertDialog.Builder(requireContext()) |  | ||||||
|                 .setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) |  | ||||||
|                 .setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) |  | ||||||
|                 .setPositiveButton(android.R.string.ok |  | ||||||
|                 ) { _, _ -> |  | ||||||
|                     val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext()) |  | ||||||
|                     val editor = sharedPref.edit() |  | ||||||
|                     editor.putBoolean("prefer_article_viewer", false) |  | ||||||
|                     editor.apply() |  | ||||||
|                     requireActivity().finish() |  | ||||||
|                 } |  | ||||||
|                 .create() |  | ||||||
|                 .show() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return binding.root |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onDestroyView() { |  | ||||||
|         super.onDestroyView() |  | ||||||
|         _binding = null |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun refreshAlignment() { |  | ||||||
|         textAlignment = when (prefs.getInt("text_align", 1)) { |  | ||||||
|             1 -> "justify" |  | ||||||
|             2 -> "left" |  | ||||||
|             else -> "justify" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) { |  | ||||||
|         if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) { |  | ||||||
|             binding.progressBar.visibility = View.VISIBLE |  | ||||||
|             val parser = MercuryApi() |  | ||||||
|  |  | ||||||
|             parser.parseUrl(url).enqueue( |  | ||||||
|                 object : Callback<ParsedContent> { |  | ||||||
|                     override fun onResponse( |  | ||||||
|                         call: Call<ParsedContent>, |  | ||||||
|                         response: Response<ParsedContent> |  | ||||||
|                     ) { |  | ||||||
|                         // TODO: clean all the following after finding the mercury content issue |  | ||||||
|                         try { |  | ||||||
|                             if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) { |  | ||||||
|                                 try { |  | ||||||
|                                     binding.titleView.text = response.body()!!.title |  | ||||||
|                                     if (typeface != null) { |  | ||||||
|                                         binding.titleView.typeface = typeface |  | ||||||
|                                     } |  | ||||||
|                                     try { |  | ||||||
|                                         // Note: Mercury may return relative urls... If it does the url val will not be changed. |  | ||||||
|                                         URL(response.body()!!.url) |  | ||||||
|                                         url = response.body()!!.url |  | ||||||
|                                     } catch (e: MalformedURLException) { |  | ||||||
|                                         // Mercury returned a relative url. We do nothing. |  | ||||||
|                                     } |  | ||||||
|                                 } catch (e: Exception) { |  | ||||||
|                                 } |  | ||||||
|  |  | ||||||
|                                 try { |  | ||||||
|                                     contentText = response.body()!!.content.orEmpty() |  | ||||||
|                                     htmlToWebview() |  | ||||||
|                                 } catch (e: Exception) { |  | ||||||
|                                 } |  | ||||||
|  |  | ||||||
|                                 try { |  | ||||||
|                                     if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) { |  | ||||||
|                                         binding.imageView.visibility = View.VISIBLE |  | ||||||
|                                         try { |  | ||||||
|                                             Glide |  | ||||||
|                                                 .with(requireContext()) |  | ||||||
|                                                 .asBitmap() |  | ||||||
|                                                 .loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty()) |  | ||||||
|                                                 .apply(RequestOptions.fitCenterTransform()) |  | ||||||
|                                                 .into(binding.imageView) |  | ||||||
|                                         } catch (e: Exception) { |  | ||||||
|                                         } |  | ||||||
|                                     } else { |  | ||||||
|                                         binding.imageView.visibility = View.GONE |  | ||||||
|                                     } |  | ||||||
|                                 } catch (e: Exception) { |  | ||||||
|                                     if (context != null) { |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|  |  | ||||||
|                                 try { |  | ||||||
|                                     binding.nestedScrollView.scrollTo(0, 0) |  | ||||||
|  |  | ||||||
|                                     binding.progressBar.visibility = View.GONE |  | ||||||
|                                 } catch (e: Exception) { |  | ||||||
|                                     if (context != null) { |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } else { |  | ||||||
|                                 try { |  | ||||||
|                                     openInBrowserAfterFailing(customTabsIntent) |  | ||||||
|                                 } catch (e: Exception) { |  | ||||||
|                                     if (context != null) { |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } catch (e: Exception) { |  | ||||||
|                             if (context != null) { |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     override fun onFailure( |  | ||||||
|                         call: Call<ParsedContent>, |  | ||||||
|                         t: Throwable |  | ||||||
|                     ) = openInBrowserAfterFailing(customTabsIntent) |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun htmlToWebview() { |  | ||||||
|         val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent) |  | ||||||
|  |  | ||||||
|         val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) |  | ||||||
|         val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         binding.webcontent.settings.standardFontFamily = a.getString(0) |  | ||||||
|         binding.webcontent.visibility = View.VISIBLE |  | ||||||
|  |  | ||||||
|         // TODO: Set the color strings programmatically |  | ||||||
|         val (stringTextColor, stringBackgroundColor) = if (appColors.isDarkTheme) { |  | ||||||
|             Pair("#FFFFFF", "#303030") |  | ||||||
|         } else { |  | ||||||
|             Pair("#212121", "#FAFAFA") |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         binding.webcontent.settings.useWideViewPort = true |  | ||||||
|         binding.webcontent.settings.loadWithOverviewMode = true |  | ||||||
|         binding.webcontent.settings.javaScriptEnabled = false |  | ||||||
|  |  | ||||||
|         binding.webcontent.webViewClient = object : WebViewClient() { |  | ||||||
|             override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean { |  | ||||||
|                 if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { |  | ||||||
|                     requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) |  | ||||||
|                 } |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? { |  | ||||||
|                 val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) |  | ||||||
|                 if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) { |  | ||||||
|                     try { |  | ||||||
|                         val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() |  | ||||||
|                         return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)) |  | ||||||
|                     }catch ( e : ExecutionException) {} |  | ||||||
|                 } |  | ||||||
|                 else if (url.lowercase(Locale.US).contains(".png")) { |  | ||||||
|                     try { |  | ||||||
|                         val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() |  | ||||||
|                         return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG)) |  | ||||||
|                     }catch ( e : ExecutionException) {} |  | ||||||
|                 } |  | ||||||
|                 else if (url.lowercase(Locale.US).contains(".webp")) { |  | ||||||
|                     try { |  | ||||||
|                         val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() |  | ||||||
|                         return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)) |  | ||||||
|                     }catch ( e : ExecutionException) {} |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return super.shouldInterceptRequest(view, url) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { |  | ||||||
|             override fun onSingleTapUp(e: MotionEvent?): Boolean { |  | ||||||
|                 return performClick() |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
|         binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)} |  | ||||||
|  |  | ||||||
|         binding.webcontent.settings.layoutAlgorithm = |  | ||||||
|                 WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING |  | ||||||
|  |  | ||||||
|         var baseUrl: String? = null |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             val itemUrl = URL(url) |  | ||||||
|             baseUrl = itemUrl.protocol + "://" + itemUrl.host |  | ||||||
|         } catch (e: MalformedURLException) { |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         val fontName =  when (font) { |  | ||||||
|             getString(R.string.open_sans_font_id) -> "Open Sans" |  | ||||||
|             getString(R.string.roboto_font_id) -> "Roboto" |  | ||||||
|             else -> "" |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         val fontLinkAndStyle = if (font.isNotEmpty()) { |  | ||||||
|             """<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet"> |  | ||||||
|                 |<style> |  | ||||||
|                 |   * { |  | ||||||
|                 |       font-family: '$fontName'; |  | ||||||
|                 |   } |  | ||||||
|                 |</style> |  | ||||||
|             """.trimMargin() |  | ||||||
|         } else { |  | ||||||
|             "" |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         binding.webcontent.loadDataWithBaseURL( |  | ||||||
|             baseUrl, |  | ||||||
|             """<html> |  | ||||||
|                 |<head> |  | ||||||
|                 |   <meta name="viewport" content="width=device-width, initial-scale=1"> |  | ||||||
|                 |   <style> |  | ||||||
|                 |      img { |  | ||||||
|                 |        display: inline-block; |  | ||||||
|                 |        height: auto; |  | ||||||
|                 |        width: 100%; |  | ||||||
|                 |        max-width: 100%; |  | ||||||
|                 |      } |  | ||||||
|                 |      a { |  | ||||||
|                 |        color: $stringColor !important; |  | ||||||
|                 |      } |  | ||||||
|                 |      *:not(a) { |  | ||||||
|                 |        color: $stringTextColor; |  | ||||||
|                 |      } |  | ||||||
|                 |      * { |  | ||||||
|                 |        font-size: ${fontSize}px; |  | ||||||
|                 |        text-align: $textAlignment; |  | ||||||
|                 |        word-break: break-word; |  | ||||||
|                 |        overflow:hidden; |  | ||||||
|                 |        line-height: 1.5em; |  | ||||||
|                 |        background-color: $stringBackgroundColor; |  | ||||||
|                 |      } |  | ||||||
|                 |      body, html { |  | ||||||
|                 |        background-color: $stringBackgroundColor !important; |  | ||||||
|                 |        border-color: $stringBackgroundColor  !important; |  | ||||||
|                 |        padding: 0 !important; |  | ||||||
|                 |        margin: 0 !important; |  | ||||||
|                 |      } |  | ||||||
|                 |      a, pre, code { |  | ||||||
|                 |        text-align: $textAlignment; |  | ||||||
|                 |      } |  | ||||||
|                 |      pre, code { |  | ||||||
|                 |        white-space: pre-wrap; |  | ||||||
|                 |        width:100%; |  | ||||||
|                 |        background-color: $stringBackgroundColor; |  | ||||||
|                 |      } |  | ||||||
|                 |   </style> |  | ||||||
|                 |   $fontLinkAndStyle |  | ||||||
|                 |</head> |  | ||||||
|                 |<body> |  | ||||||
|                 |   $contentText |  | ||||||
|                 |</body>""".trimMargin(), |  | ||||||
|             "text/html", |  | ||||||
|             "utf-8", |  | ||||||
|             null |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun scrollDown() { |  | ||||||
|         val height = binding.nestedScrollView.measuredHeight |  | ||||||
|         binding.nestedScrollView.smoothScrollBy(0, height/2) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun scrollUp() { |  | ||||||
|         val height = binding.nestedScrollView.measuredHeight |  | ||||||
|         binding.nestedScrollView.smoothScrollBy(0, -height/2) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) { |  | ||||||
|         binding.progressBar.visibility = View.GONE |  | ||||||
|         requireActivity().openItemUrlInternalBrowser( |  | ||||||
|                 url, |  | ||||||
|                 customTabsIntent, |  | ||||||
|                 requireActivity() |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         private const val ARG_ITEMS = "items" |  | ||||||
|  |  | ||||||
|         fun newInstance( |  | ||||||
|                 item: Item |  | ||||||
|         ): ArticleFragment { |  | ||||||
|             val fragment = ArticleFragment() |  | ||||||
|             val args = Bundle() |  | ||||||
|             args.putParcelable(ARG_ITEMS, item) |  | ||||||
|             fragment.arguments = args |  | ||||||
|             return fragment |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun performClick(): Boolean { |  | ||||||
|         if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || |  | ||||||
|                 binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { |  | ||||||
|  |  | ||||||
|             val position : Int = allImages.indexOf(binding.webcontent.hitTestResult.extra) |  | ||||||
|  |  | ||||||
|             val intent = Intent(activity, ImageActivity::class.java) |  | ||||||
|             intent.putExtra("allImages", allImages) |  | ||||||
|             intent.putExtra("position", position) |  | ||||||
|             startActivity(intent) |  | ||||||
|             return false |  | ||||||
|         } |  | ||||||
|         return false |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -1,57 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.fragments |  | ||||||
|  |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.* |  | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.FragmentImageBinding |  | ||||||
| import com.bumptech.glide.Glide |  | ||||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy |  | ||||||
| import com.bumptech.glide.request.RequestOptions |  | ||||||
|  |  | ||||||
| class ImageFragment : Fragment() { |  | ||||||
|  |  | ||||||
|     private lateinit var imageUrl : String |  | ||||||
|     private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) |  | ||||||
|     private var _binding: FragmentImageBinding? = null |  | ||||||
|     private val binding get() = _binding |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|  |  | ||||||
|         imageUrl = requireArguments().getString("imageUrl")!! |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { |  | ||||||
|         _binding = FragmentImageBinding.inflate(inflater, container, false) |  | ||||||
|         val view = binding?.root |  | ||||||
|  |  | ||||||
|         binding!!.photoView.visibility = View.VISIBLE |  | ||||||
|         Glide.with(activity) |  | ||||||
|                 .asBitmap() |  | ||||||
|                 .apply(glideOptions) |  | ||||||
|                 .load(imageUrl) |  | ||||||
|                 .into(binding!!.photoView) |  | ||||||
|  |  | ||||||
|         return view |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onDestroyView() { |  | ||||||
|         super.onDestroyView() |  | ||||||
|         _binding = null |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         private const val ARG_IMAGE = "imageUrl" |  | ||||||
|  |  | ||||||
|         fun newInstance( |  | ||||||
|                 imageUrl : String |  | ||||||
|         ): ImageFragment { |  | ||||||
|             val fragment = ImageFragment() |  | ||||||
|             val args = Bundle() |  | ||||||
|             args.putString(ARG_IMAGE, imageUrl) |  | ||||||
|             fragment.arguments = args |  | ||||||
|             return fragment |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.persistence.dao |  | ||||||
|  |  | ||||||
| import androidx.room.Dao |  | ||||||
| import androidx.room.Delete |  | ||||||
| import androidx.room.Insert |  | ||||||
| import androidx.room.OnConflictStrategy |  | ||||||
| import androidx.room.Query |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity |  | ||||||
|  |  | ||||||
| @Dao |  | ||||||
| interface ActionsDao { |  | ||||||
|     @Query("SELECT * FROM actions order by id asc") |  | ||||||
|     suspend fun actions(): List<ActionEntity> |  | ||||||
|  |  | ||||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) |  | ||||||
|     fun insertAllActions(vararg actions: ActionEntity) |  | ||||||
|  |  | ||||||
|     @Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1") |  | ||||||
|     fun deleteReadActionForArticle(article_id: String) |  | ||||||
|  |  | ||||||
|     @Delete |  | ||||||
|     fun delete(action: ActionEntity) |  | ||||||
| } |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.persistence.dao |  | ||||||
|  |  | ||||||
| import androidx.room.Delete |  | ||||||
| import androidx.room.Dao |  | ||||||
| import androidx.room.Insert |  | ||||||
| import androidx.room.OnConflictStrategy |  | ||||||
| import androidx.room.Query |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity |  | ||||||
|  |  | ||||||
| @Dao |  | ||||||
| interface DrawerDataDao { |  | ||||||
|     @Query("SELECT * FROM tags") |  | ||||||
|     fun tags(): List<TagEntity> |  | ||||||
|  |  | ||||||
|     @Query("SELECT * FROM sources") |  | ||||||
|     fun sources(): List<SourceEntity> |  | ||||||
|  |  | ||||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) |  | ||||||
|     fun insertAllTags(vararg tags: TagEntity) |  | ||||||
|  |  | ||||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) |  | ||||||
|     fun insertAllSources(vararg sources: SourceEntity) |  | ||||||
|  |  | ||||||
|     @Query("DELETE FROM tags") |  | ||||||
|     fun deleteAllTags() |  | ||||||
|  |  | ||||||
|     @Query("DELETE FROM sources") |  | ||||||
|     fun deleteAllSources() |  | ||||||
|  |  | ||||||
|     @Delete |  | ||||||
|     fun deleteTag(tag: TagEntity) |  | ||||||
|  |  | ||||||
|     @Delete |  | ||||||
|     fun deleteSource(source: SourceEntity) |  | ||||||
| } |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.persistence.dao |  | ||||||
|  |  | ||||||
| import androidx.room.Dao |  | ||||||
| import androidx.room.Delete |  | ||||||
| import androidx.room.Insert |  | ||||||
| import androidx.room.OnConflictStrategy |  | ||||||
| import androidx.room.Query |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity |  | ||||||
| import androidx.room.Update |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @Dao |  | ||||||
| interface ItemsDao { |  | ||||||
|     @Query("SELECT * FROM items order by id desc") |  | ||||||
|     suspend fun items(): List<ItemEntity> |  | ||||||
|  |  | ||||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) |  | ||||||
|     suspend fun insertAllItems(vararg items: ItemEntity) |  | ||||||
|  |  | ||||||
|     @Query("DELETE FROM items") |  | ||||||
|     suspend fun deleteAllItems() |  | ||||||
|  |  | ||||||
|     @Delete |  | ||||||
|     suspend fun delete(item: ItemEntity) |  | ||||||
|  |  | ||||||
|     @Update |  | ||||||
|     suspend fun updateItem(item: ItemEntity) |  | ||||||
| } |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.persistence.database |  | ||||||
|  |  | ||||||
| import androidx.room.RoomDatabase |  | ||||||
| import androidx.room.Database |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.dao.ActionsDao |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.dao.DrawerDataDao |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.dao.ItemsDao |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity |  | ||||||
|  |  | ||||||
| @Database(entities = [TagEntity::class, SourceEntity::class, ItemEntity::class, ActionEntity::class], version = 4) |  | ||||||
| abstract class AppDatabase : RoomDatabase() { |  | ||||||
|     abstract fun drawerDataDao(): DrawerDataDao |  | ||||||
|  |  | ||||||
|     abstract fun itemsDao(): ItemsDao |  | ||||||
|  |  | ||||||
|     abstract fun actionsDao(): ActionsDao |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.persistence.entities |  | ||||||
|  |  | ||||||
| import androidx.room.ColumnInfo |  | ||||||
| import androidx.room.Entity |  | ||||||
| import androidx.room.PrimaryKey |  | ||||||
|  |  | ||||||
| @Entity(tableName = "actions") |  | ||||||
| data class ActionEntity( |  | ||||||
|     @ColumnInfo(name = "articleid") |  | ||||||
|     val articleId: String, |  | ||||||
|     @ColumnInfo(name = "read") |  | ||||||
|     val read: Boolean, |  | ||||||
|     @ColumnInfo(name = "unread") |  | ||||||
|     val unread: Boolean, |  | ||||||
|     @ColumnInfo(name = "starred") |  | ||||||
|     var starred: Boolean, |  | ||||||
|     @ColumnInfo(name = "unstarred") |  | ||||||
|     var unstarred: Boolean |  | ||||||
| ) { |  | ||||||
|     @PrimaryKey(autoGenerate = true) |  | ||||||
|     var id: Int = 0 |  | ||||||
| } |  | ||||||
| @@ -1,33 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.persistence.entities |  | ||||||
|  |  | ||||||
| import androidx.room.ColumnInfo |  | ||||||
| import androidx.room.Entity |  | ||||||
| import androidx.room.PrimaryKey |  | ||||||
|  |  | ||||||
| @Entity(tableName = "tags") |  | ||||||
| data class TagEntity( |  | ||||||
|     @PrimaryKey |  | ||||||
|     @ColumnInfo(name = "tag") |  | ||||||
|     val tag: String, |  | ||||||
|     @ColumnInfo(name = "color") |  | ||||||
|     val color: String, |  | ||||||
|     @ColumnInfo(name = "unread") |  | ||||||
|     val unread: Int |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| @Entity(tableName = "sources") |  | ||||||
| data class SourceEntity( |  | ||||||
|     @PrimaryKey |  | ||||||
|     @ColumnInfo(name = "id") |  | ||||||
|     val id: String, |  | ||||||
|     @ColumnInfo(name = "title") |  | ||||||
|     val title: String, |  | ||||||
|     @ColumnInfo(name = "tags") |  | ||||||
|     val tags: String, |  | ||||||
|     @ColumnInfo(name = "spout") |  | ||||||
|     val spout: String, |  | ||||||
|     @ColumnInfo(name = "error") |  | ||||||
|     val error: String, |  | ||||||
|     @ColumnInfo(name = "icon") |  | ||||||
|     val icon: String |  | ||||||
| ) |  | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.persistence.entities |  | ||||||
|  |  | ||||||
| import androidx.room.ColumnInfo |  | ||||||
| import androidx.room.Entity |  | ||||||
| import androidx.room.PrimaryKey |  | ||||||
|  |  | ||||||
| @Entity(tableName = "items") |  | ||||||
| data class ItemEntity( |  | ||||||
|     @PrimaryKey |  | ||||||
|     @ColumnInfo(name = "id") |  | ||||||
|     val id: String, |  | ||||||
|     @ColumnInfo(name = "datetime") |  | ||||||
|     val datetime: String, |  | ||||||
|     @ColumnInfo(name = "title") |  | ||||||
|     val title: String, |  | ||||||
|     @ColumnInfo(name = "content") |  | ||||||
|     val content: String, |  | ||||||
|     @ColumnInfo(name = "unread") |  | ||||||
|     val unread: Boolean, |  | ||||||
|     @ColumnInfo(name = "starred") |  | ||||||
|     var starred: Boolean, |  | ||||||
|     @ColumnInfo(name = "thumbnail") |  | ||||||
|     val thumbnail: String?, |  | ||||||
|     @ColumnInfo(name = "icon") |  | ||||||
|     val icon: String?, |  | ||||||
|     @ColumnInfo(name = "link") |  | ||||||
|     val link: String, |  | ||||||
|     @ColumnInfo(name = "sourcetitle") |  | ||||||
|     val sourcetitle: String, |  | ||||||
|     @ColumnInfo(name = "tags") |  | ||||||
|     val tags: String |  | ||||||
| ) |  | ||||||
| @@ -1,34 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.persistence.migrations |  | ||||||
|  |  | ||||||
| import androidx.sqlite.db.SupportSQLiteDatabase |  | ||||||
| import androidx.room.migration.Migration |  | ||||||
|  |  | ||||||
| val MIGRATION_1_2: Migration = object : Migration(1, 2) { |  | ||||||
|     override fun migrate(database: SupportSQLiteDatabase) { |  | ||||||
|         database.execSQL("CREATE TABLE IF NOT EXISTS `items` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))") |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| val MIGRATION_2_3: Migration = object : Migration(2, 3) { |  | ||||||
|     override fun migrate(database: SupportSQLiteDatabase) { |  | ||||||
|         database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))") |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| val MIGRATION_3_4: Migration = object : Migration(3, 4) { |  | ||||||
|     override fun migrate(database: SupportSQLiteDatabase) { |  | ||||||
|         // @see https://stackoverflow.com/questions/57392015/how-to-migrate-not-null-table-column-into-null-in-android-room-database |  | ||||||
|         // Create the new table |  | ||||||
|         database.execSQL("CREATE TABLE IF NOT EXISTS `itemstmp` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))") |  | ||||||
|  |  | ||||||
|         // Copy the data |  | ||||||
|         database.execSQL( |  | ||||||
|                 "INSERT INTO itemstmp (`id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags`) SELECT `id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags` FROM items") |  | ||||||
|  |  | ||||||
|         // Remove the old table |  | ||||||
|         database.execSQL("DROP TABLE items") |  | ||||||
|  |  | ||||||
|         // Change the table name to the correct one |  | ||||||
|         database.execSQL("ALTER TABLE itemstmp RENAME TO items") |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,221 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.settings |  | ||||||
|  |  | ||||||
| import android.content.Intent |  | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.text.* |  | ||||||
| import androidx.preference.EditTextPreference |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import android.view.* |  | ||||||
| import android.widget.Toast |  | ||||||
| import androidx.appcompat.app.AppCompatActivity |  | ||||||
| import androidx.core.widget.addTextChangedListener |  | ||||||
| import androidx.preference.Preference |  | ||||||
| import androidx.preference.PreferenceFragmentCompat |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.databinding.ActivitySettingsBinding |  | ||||||
| import apps.amine.bou.readerforselfoss.themes.Toppings |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import com.ftinc.scoop.Scoop |  | ||||||
| import java.lang.NumberFormatException |  | ||||||
|  |  | ||||||
| private const val TITLE_TAG = "settingsActivityTitle" |  | ||||||
|  |  | ||||||
| class SettingsActivity : AppCompatActivity(), |  | ||||||
|         PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { |  | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("dark_theme", false)) { |  | ||||||
|             setTheme(R.style.NoBarDark) |  | ||||||
|         } |  | ||||||
|         val binding = ActivitySettingsBinding.inflate(layoutInflater) |  | ||||||
|  |  | ||||||
|         val scoop = Scoop.getInstance() |  | ||||||
|         scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar) |  | ||||||
|         scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value) |  | ||||||
|  |  | ||||||
|         setContentView(binding.root) |  | ||||||
|         if (savedInstanceState == null) { |  | ||||||
|             supportFragmentManager |  | ||||||
|                     .beginTransaction() |  | ||||||
|                     .replace(R.id.settings, MainPreferenceFragment()) |  | ||||||
|                     .commit() |  | ||||||
|         } else { |  | ||||||
|             title = savedInstanceState.getCharSequence(TITLE_TAG) |  | ||||||
|         } |  | ||||||
|         supportFragmentManager.addOnBackStackChangedListener { |  | ||||||
|             if (supportFragmentManager.backStackEntryCount == 0) { |  | ||||||
|                 setTitle(R.string.title_activity_settings) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         setSupportActionBar(binding.toolbar) |  | ||||||
|  |  | ||||||
|         supportActionBar?.setDisplayHomeAsUpEnabled(true) |  | ||||||
|         supportActionBar?.setDisplayShowHomeEnabled(true) |  | ||||||
|         supportActionBar?.title = title |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onSaveInstanceState(outState: Bundle) { |  | ||||||
|         super.onSaveInstanceState(outState) |  | ||||||
|         // Save current activity title so we can set it again after a configuration change |  | ||||||
|         outState.putCharSequence(TITLE_TAG, title) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onSupportNavigateUp(): Boolean { |  | ||||||
|         if (supportFragmentManager.popBackStackImmediate()) { |  | ||||||
|             supportActionBar?.title = getText(R.string.title_activity_settings) |  | ||||||
|             return true |  | ||||||
|         } |  | ||||||
|         return super.onSupportNavigateUp() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onPreferenceStartFragment( |  | ||||||
|             caller: PreferenceFragmentCompat, |  | ||||||
|             pref: Preference |  | ||||||
|     ): Boolean { |  | ||||||
|         // Instantiate the new Fragment |  | ||||||
|         val args = pref.extras |  | ||||||
|         val fragment = supportFragmentManager.fragmentFactory.instantiate( |  | ||||||
|                 classLoader, |  | ||||||
|                 pref.fragment |  | ||||||
|         ).apply { |  | ||||||
|             arguments = args |  | ||||||
|             setTargetFragment(caller, 0) |  | ||||||
|         } |  | ||||||
|         // Replace the existing Fragment with the new Fragment |  | ||||||
|         supportFragmentManager.beginTransaction() |  | ||||||
|                 .replace(R.id.settings, fragment) |  | ||||||
|                 .addToBackStack(null) |  | ||||||
|                 .commit() |  | ||||||
|         title = pref.title |  | ||||||
|         supportActionBar?.title = title |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     class MainPreferenceFragment : PreferenceFragmentCompat() { |  | ||||||
|         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { |  | ||||||
|             setPreferencesFromResource(R.xml.pref_main, rootKey) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     class GeneralPreferenceFragment : PreferenceFragmentCompat() { |  | ||||||
|         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { |  | ||||||
|             setPreferencesFromResource(R.xml.pref_general, rootKey) |  | ||||||
|  |  | ||||||
|             val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number") |  | ||||||
|             editTextPreference?.setOnBindEditTextListener { editText -> |  | ||||||
|                 editText.inputType = InputType.TYPE_CLASS_NUMBER |  | ||||||
|                 editText.filters = arrayOf( |  | ||||||
|                         InputFilter { source, _, _, dest, _, _ -> |  | ||||||
|                             try { |  | ||||||
|                                 val input: Int = (dest.toString() + source.toString()).toInt() |  | ||||||
|                                 if (input in 1..200) return@InputFilter null |  | ||||||
|                             } catch (nfe: NumberFormatException) { |  | ||||||
|                                 Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show() |  | ||||||
|                             } |  | ||||||
|                             "" |  | ||||||
|                         } |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() { |  | ||||||
|         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { |  | ||||||
|             setPreferencesFromResource(R.xml.pref_viewer, rootKey) |  | ||||||
|  |  | ||||||
|             val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size") |  | ||||||
|             fontSize?.setOnBindEditTextListener { editText -> |  | ||||||
|                 editText.inputType = InputType.TYPE_CLASS_NUMBER |  | ||||||
|                 editText.addTextChangedListener { object : TextWatcher { |  | ||||||
|                     override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} |  | ||||||
|                     override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} |  | ||||||
|                     override fun afterTextChanged(editable: Editable) { |  | ||||||
|                         try { |  | ||||||
|                             editText.textSize = editable.toString().toInt().toFloat() |  | ||||||
|                         } catch (e: NumberFormatException) { |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } } |  | ||||||
|                 editText.filters = arrayOf( |  | ||||||
|                         InputFilter { source, _, _, dest, _, _ -> |  | ||||||
|                             try { |  | ||||||
|                                 val input = (dest.toString() + source.toString()).toInt() |  | ||||||
|                                 if (input > 0) return@InputFilter null |  | ||||||
|                             } catch (nfe: NumberFormatException) { |  | ||||||
|                             } |  | ||||||
|                             "" |  | ||||||
|                         } |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     class OfflinePreferenceFragment : PreferenceFragmentCompat() { |  | ||||||
|         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { |  | ||||||
|             setPreferencesFromResource(R.xml.pref_offline, rootKey) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     class ThemePreferenceFragment : PreferenceFragmentCompat() { |  | ||||||
|         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { |  | ||||||
|             setPreferencesFromResource(R.xml.pref_theme, rootKey) |  | ||||||
|             setHasOptionsMenu(true) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { |  | ||||||
|             super.onCreateOptionsMenu(menu, inflater) |  | ||||||
|             inflater.inflate(R.menu.settings_theme, menu) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         override fun onOptionsItemSelected(item: MenuItem): Boolean { |  | ||||||
|             val id = item.itemId |  | ||||||
|             if (id == R.id.clear) { |  | ||||||
|                 val pref = PreferenceManager.getDefaultSharedPreferences(activity) |  | ||||||
|                 val editor = pref.edit() |  | ||||||
|                 editor.remove("color_primary") |  | ||||||
|                 editor.remove("color_primary_dark") |  | ||||||
|                 editor.remove("color_accent") |  | ||||||
|                 editor.remove("color_accent_dark") |  | ||||||
|                 editor.remove("dark_theme") |  | ||||||
|                 editor.apply() |  | ||||||
|                 requireActivity().recreate() |  | ||||||
|             } |  | ||||||
|             return super.onOptionsItemSelected(item) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     class LinksPreferenceFragment : PreferenceFragmentCompat() { |  | ||||||
|         private fun openUrl(uri: Uri?) { |  | ||||||
|             val browserIntent = Intent(Intent.ACTION_VIEW, uri) |  | ||||||
|             startActivity(browserIntent) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { |  | ||||||
|             setPreferencesFromResource(R.xml.pref_links, rootKey) |  | ||||||
|  |  | ||||||
|             preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { |  | ||||||
|                 openUrl(Uri.parse(Config.trackerUrl)) |  | ||||||
|                 true |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { |  | ||||||
|                 openUrl(Uri.parse(Config.sourceUrl)) |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { |  | ||||||
|                 openUrl(Uri.parse(Config.translationUrl)) |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     class ExperimentalPreferenceFragment : PreferenceFragmentCompat() { |  | ||||||
|         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { |  | ||||||
|             setPreferencesFromResource(R.xml.pref_experimental, rootKey) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,61 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.themes |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import androidx.annotation.ColorInt |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
|  |  | ||||||
| class AppColors(a: Activity) { |  | ||||||
|  |  | ||||||
|     @ColorInt val colorPrimary: Int |  | ||||||
|     @ColorInt val colorPrimaryDark: Int |  | ||||||
|     @ColorInt val colorAccent: Int |  | ||||||
|     @ColorInt val colorAccentDark: Int |  | ||||||
|     @ColorInt val colorBackground: Int |  | ||||||
|     @ColorInt val textColor: Int |  | ||||||
|     val isDarkTheme: Boolean |  | ||||||
|  |  | ||||||
|     init { |  | ||||||
|         val sharedPref = PreferenceManager.getDefaultSharedPreferences(a) |  | ||||||
|  |  | ||||||
|         colorPrimary = |  | ||||||
|                 sharedPref.getInt( |  | ||||||
|                     "color_primary", |  | ||||||
|                     a.resources.getColor(R.color.colorPrimary) |  | ||||||
|                 ) |  | ||||||
|         colorPrimaryDark = |  | ||||||
|                 sharedPref.getInt( |  | ||||||
|                     "color_primary_dark", |  | ||||||
|                     a.resources.getColor(R.color.colorPrimaryDark) |  | ||||||
|                 ) |  | ||||||
|         colorAccent = |  | ||||||
|                 sharedPref.getInt( |  | ||||||
|                     "color_accent", |  | ||||||
|                     a.resources.getColor(R.color.colorAccent) |  | ||||||
|                 ) |  | ||||||
|         colorAccentDark = |  | ||||||
|                 sharedPref.getInt( |  | ||||||
|                     "color_accent_dark", |  | ||||||
|                     a.resources.getColor(R.color.colorAccentDark) |  | ||||||
|                 ) |  | ||||||
|         isDarkTheme = |  | ||||||
|                 sharedPref.getBoolean( |  | ||||||
|                     "dark_theme", |  | ||||||
|                     false |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|         colorBackground = if (isDarkTheme) { |  | ||||||
|             a.setTheme(R.style.NoBarDark) |  | ||||||
|             R.color.darkBackground |  | ||||||
|         } else { |  | ||||||
|             a.setTheme(R.style.NoBar) |  | ||||||
|             R.color.grey_50 |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         textColor = if (isDarkTheme) { |  | ||||||
|             R.color.white |  | ||||||
|         } else { |  | ||||||
|             R.color.grey_900 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.themes |  | ||||||
|  |  | ||||||
| enum class Toppings(val value: Int) { |  | ||||||
|     PRIMARY(1), |  | ||||||
|     PRIMARY_DARK(2), |  | ||||||
|     ACCENT(3), |  | ||||||
|     ACCENT_DARK(4) |  | ||||||
| } |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse |  | ||||||
| import retrofit2.Response |  | ||||||
|  |  | ||||||
| fun Response<SuccessResponse>.succeeded(): Boolean = |  | ||||||
|     this.code() === 200 && this.body() != null && this.body()!!.isSuccess |  | ||||||
| @@ -1,41 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
|  |  | ||||||
| fun String?.isEmptyOrNullOrNullString(): Boolean = |  | ||||||
|     this == null || this == "null" || this.isEmpty() |  | ||||||
|  |  | ||||||
| fun String.longHash(): Long { |  | ||||||
|     var h = 98764321261L |  | ||||||
|     val l = this.length |  | ||||||
|     val chars = this.toCharArray() |  | ||||||
|  |  | ||||||
|     for (i in 0 until l) { |  | ||||||
|         h = 31 * h + chars[i].code.toLong() |  | ||||||
|     } |  | ||||||
|     return h |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun String.toStringUriWithHttp(): String = |  | ||||||
|     if (!this.startsWith("https://") && !this.startsWith("http://")) { |  | ||||||
|         "http://" + this |  | ||||||
|     } else { |  | ||||||
|         this |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| fun Context.shareLink(itemUrl: String, itemTitle: String) { |  | ||||||
|     val sendIntent = Intent() |  | ||||||
|     sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK |  | ||||||
|     sendIntent.action = Intent.ACTION_SEND |  | ||||||
|     sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp()) |  | ||||||
|     sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle) |  | ||||||
|     sendIntent.type = "text/plain" |  | ||||||
|     startActivity( |  | ||||||
|         Intent.createChooser( |  | ||||||
|             sendIntent, |  | ||||||
|             getString(R.string.share) |  | ||||||
|         ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |  | ||||||
|     ) |  | ||||||
| } |  | ||||||
| @@ -1,64 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.content.SharedPreferences |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import apps.amine.bou.readerforselfoss.LoginActivity |  | ||||||
|  |  | ||||||
| class Config(c: Context) { |  | ||||||
|  |  | ||||||
|     val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(c) |  | ||||||
|  |  | ||||||
|     val baseUrl: String |  | ||||||
|         get() = settings.getString("url", "")!! |  | ||||||
|  |  | ||||||
|     val userLogin: String |  | ||||||
|         get() = settings.getString("login", "")!! |  | ||||||
|  |  | ||||||
|     val userPassword: String |  | ||||||
|         get() = settings.getString("password", "")!! |  | ||||||
|  |  | ||||||
|     val httpUserLogin: String |  | ||||||
|         get() = settings.getString("httpUserName", "")!! |  | ||||||
|  |  | ||||||
|     val httpUserPassword: String |  | ||||||
|         get() = settings.getString("httpPassword", "")!! |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         const val settingsName = "paramsselfoss" |  | ||||||
|  |  | ||||||
|         const val feedbackEmail = "aminecmi@gmail.com" |  | ||||||
|  |  | ||||||
|         const val translationUrl = "https://crwd.in/readerforselfoss" |  | ||||||
|  |  | ||||||
|         const val sourceUrl = "https://github.com/aminecmi/ReaderforSelfoss" |  | ||||||
|  |  | ||||||
|         const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues" |  | ||||||
|  |  | ||||||
|         const val syncChannelId = "sync-channel-id" |  | ||||||
|  |  | ||||||
|         const val newItemsChannelId = "new-items-channel-id" |  | ||||||
|  |  | ||||||
|         var apiVersion = 0 |  | ||||||
|  |  | ||||||
|         /* Execute logout and clear all settings to default */ |  | ||||||
|         fun logoutAndRedirect( |  | ||||||
|             c: Context, |  | ||||||
|             callingActivity: Activity, |  | ||||||
|             editor: SharedPreferences.Editor, |  | ||||||
|             baseUrlFail: Boolean = false |  | ||||||
|         ): Boolean { |  | ||||||
|             val settings = PreferenceManager.getDefaultSharedPreferences(c) |  | ||||||
|             settings.edit().clear().commit() |  | ||||||
|             val intent = Intent(c, LoginActivity::class.java) |  | ||||||
|             if (baseUrlFail) { |  | ||||||
|                 intent.putExtra("baseUrlFail", baseUrlFail) |  | ||||||
|             } |  | ||||||
|             c.startActivity(intent) |  | ||||||
|             callingActivity.finish() |  | ||||||
|             return true |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,31 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import android.text.format.DateUtils |  | ||||||
| import java.time.Instant |  | ||||||
| import java.time.LocalDateTime |  | ||||||
| import java.time.OffsetDateTime |  | ||||||
| import java.time.ZoneOffset |  | ||||||
| import java.time.format.DateTimeFormatter |  | ||||||
|  |  | ||||||
| fun parseDate(dateString: String): Instant { |  | ||||||
|  |  | ||||||
|     val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss" |  | ||||||
|  |  | ||||||
|     return if (Config.apiVersion >= 4) { |  | ||||||
|         OffsetDateTime.parse(dateString).toInstant() |  | ||||||
|     } else { |  | ||||||
|         LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun parseRelativeDate(dateString: String): String { |  | ||||||
|  |  | ||||||
|     val date = parseDate(dateString) |  | ||||||
|  |  | ||||||
|     return " " + DateUtils.getRelativeTimeSpanString( |  | ||||||
|             date.toEpochMilli(), |  | ||||||
|             Instant.now().toEpochMilli(), |  | ||||||
|             DateUtils.MINUTE_IN_MILLIS, |  | ||||||
|             DateUtils.FORMAT_ABBREV_RELATIVE |  | ||||||
|     ) |  | ||||||
| } |  | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import okhttp3.OkHttpClient |  | ||||||
| import java.security.cert.CertificateException |  | ||||||
| import java.security.cert.X509Certificate |  | ||||||
| import javax.net.ssl.SSLContext |  | ||||||
| import javax.net.ssl.TrustManager |  | ||||||
| import javax.net.ssl.X509TrustManager |  | ||||||
|  |  | ||||||
| fun getUnsafeHttpClient(): OkHttpClient.Builder = |  | ||||||
|     try { |  | ||||||
|         // Create a trust manager that does not validate certificate chains |  | ||||||
|         val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager { |  | ||||||
|             override fun getAcceptedIssuers(): Array<X509Certificate> = |  | ||||||
|                 arrayOf() |  | ||||||
|  |  | ||||||
|             @Throws(CertificateException::class) |  | ||||||
|             override fun checkClientTrusted( |  | ||||||
|                 chain: Array<java.security.cert.X509Certificate>, |  | ||||||
|                 authType: String |  | ||||||
|             ) { |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Throws(CertificateException::class) |  | ||||||
|             override fun checkServerTrusted( |  | ||||||
|                 chain: Array<java.security.cert.X509Certificate>, |  | ||||||
|                 authType: String |  | ||||||
|             ) { |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
|         // Install the all-trusting trust manager |  | ||||||
|         val sslContext = SSLContext.getInstance("SSL") |  | ||||||
|         sslContext.init(null, trustAllCerts, java.security.SecureRandom()) |  | ||||||
|  |  | ||||||
|         val sslSocketFactory = sslContext.socketFactory |  | ||||||
|  |  | ||||||
|         OkHttpClient.Builder() |  | ||||||
|             .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) |  | ||||||
|             .hostnameVerifier { _, _ -> true } |  | ||||||
|     } catch (e: Exception) { |  | ||||||
|         throw RuntimeException(e) |  | ||||||
|     } |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType |  | ||||||
|  |  | ||||||
| fun String.toTextDrawableString(c: Context): String { |  | ||||||
|     val textDrawable = StringBuilder() |  | ||||||
|     for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) { |  | ||||||
|         try { |  | ||||||
|             textDrawable.append(s[0]) |  | ||||||
|         } catch (e: StringIndexOutOfBoundsException) { |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     return textDrawable.toString() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun Item.sourceAndDateText(): String { |  | ||||||
|     val formattedDate = parseRelativeDate(this.datetime) |  | ||||||
|  |  | ||||||
|     return this.getSourceTitle() + formattedDate |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun Item.toggleStar(): Item { |  | ||||||
|     this.starred = !this.starred |  | ||||||
|     return this |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun List<Item>.flattenTags(): List<Item> = |  | ||||||
|     this.flatMap { |  | ||||||
|         val item = it |  | ||||||
|         val tags: List<String> = it.tags.tags.split(",") |  | ||||||
|         tags.map { t -> |  | ||||||
|             item.copy(tags = SelfossTagType(t.trim())) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| @@ -1,220 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import android.app.PendingIntent |  | ||||||
| import android.content.ActivityNotFoundException |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.graphics.BitmapFactory |  | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Build |  | ||||||
| import android.text.Spannable |  | ||||||
| import android.text.style.ClickableSpan |  | ||||||
| import androidx.browser.customtabs.CustomTabsIntent |  | ||||||
| import android.util.Patterns |  | ||||||
| import android.view.MotionEvent |  | ||||||
| import android.view.View |  | ||||||
| import android.widget.TextView |  | ||||||
| import android.widget.Toast |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.ReaderActivity |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper |  | ||||||
| import okhttp3.HttpUrl |  | ||||||
| import okhttp3.HttpUrl.Companion.toHttpUrlOrNull |  | ||||||
|  |  | ||||||
| fun Context.buildCustomTabsIntent(): CustomTabsIntent { |  | ||||||
|  |  | ||||||
|     val actionIntent = Intent(Intent.ACTION_SEND) |  | ||||||
|     actionIntent.type = "text/plain" |  | ||||||
|     val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |  | ||||||
|         PendingIntent.FLAG_IMMUTABLE |  | ||||||
|     } else { |  | ||||||
|         0 |  | ||||||
|     } |  | ||||||
|     val createPendingShareIntent: PendingIntent = PendingIntent.getActivity( |  | ||||||
|             this, |  | ||||||
|             0, |  | ||||||
|             actionIntent, |  | ||||||
|             pflags |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     val intentBuilder = CustomTabsIntent.Builder() |  | ||||||
|  |  | ||||||
|     // TODO: change to primary when it's possible to customize custom tabs title color |  | ||||||
|     //intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary)); |  | ||||||
|     intentBuilder.setToolbarColor(resources.getColor(R.color.colorAccentDark)) |  | ||||||
|     intentBuilder.setShowTitle(true) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     intentBuilder.setStartAnimations( |  | ||||||
|         this, |  | ||||||
|         R.anim.slide_in_right, |  | ||||||
|         R.anim.slide_out_left |  | ||||||
|     ) |  | ||||||
|     intentBuilder.setExitAnimations( |  | ||||||
|         this, |  | ||||||
|         android.R.anim.slide_in_left, |  | ||||||
|         android.R.anim.slide_out_right |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     val closeicon = BitmapFactory.decodeResource(resources, R.drawable.ic_close_white_24dp) |  | ||||||
|     intentBuilder.setCloseButtonIcon(closeicon) |  | ||||||
|  |  | ||||||
|     val shareLabel = this.getString(R.string.label_share) |  | ||||||
|     val icon = BitmapFactory.decodeResource( |  | ||||||
|         resources, |  | ||||||
|         R.drawable.ic_share_white_24dp |  | ||||||
|     ) |  | ||||||
|     intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent) |  | ||||||
|  |  | ||||||
|     return intentBuilder.build() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun Context.openItemUrlInternally( |  | ||||||
|     allItems: ArrayList<Item>, |  | ||||||
|     currentItem: Int, |  | ||||||
|     linkDecoded: String, |  | ||||||
|     customTabsIntent: CustomTabsIntent, |  | ||||||
|     articleViewer: Boolean, |  | ||||||
|     app: Activity |  | ||||||
| ) { |  | ||||||
|     if (articleViewer) { |  | ||||||
|         ReaderActivity.allItems = allItems |  | ||||||
|         SharedItems.position = currentItem |  | ||||||
|         val intent = Intent(this, ReaderActivity::class.java) |  | ||||||
|         intent.putExtra("currentItem", currentItem) |  | ||||||
|         app.startActivity(intent) |  | ||||||
|     } else { |  | ||||||
|         this.openItemUrlInternalBrowser( |  | ||||||
|                 linkDecoded, |  | ||||||
|                 customTabsIntent, |  | ||||||
|                 app) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun Context.openItemUrlInternalBrowser( |  | ||||||
|         linkDecoded: String, |  | ||||||
|         customTabsIntent: CustomTabsIntent, |  | ||||||
|         app: Activity |  | ||||||
| ) { |  | ||||||
|     try { |  | ||||||
|         CustomTabActivityHelper.openCustomTab( |  | ||||||
|                 app, |  | ||||||
|                 customTabsIntent, |  | ||||||
|                 Uri.parse(linkDecoded) |  | ||||||
|         ) { _, uri -> |  | ||||||
|             val intent = Intent(Intent.ACTION_VIEW, uri) |  | ||||||
|             intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK |  | ||||||
|             startActivity(intent) |  | ||||||
|         } |  | ||||||
|     } catch (e: Exception) { |  | ||||||
|         openInBrowser(linkDecoded, app) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun Context.openItemUrl( |  | ||||||
|     allItems: ArrayList<Item>, |  | ||||||
|     currentItem: Int, |  | ||||||
|     linkDecoded: String, |  | ||||||
|     customTabsIntent: CustomTabsIntent, |  | ||||||
|     internalBrowser: Boolean, |  | ||||||
|     articleViewer: Boolean, |  | ||||||
|     app: Activity |  | ||||||
| ) { |  | ||||||
|  |  | ||||||
|     if (!linkDecoded.isUrlValid()) { |  | ||||||
|         Toast.makeText( |  | ||||||
|             this, |  | ||||||
|             this.getString(R.string.cant_open_invalid_url), |  | ||||||
|             Toast.LENGTH_LONG |  | ||||||
|         ).show() |  | ||||||
|     } else { |  | ||||||
|         if (!internalBrowser) { |  | ||||||
|             openInBrowser(linkDecoded, app) |  | ||||||
|         } else if (articleViewer) { |  | ||||||
|             this.openItemUrlInternally( |  | ||||||
|                 allItems, |  | ||||||
|                 currentItem, |  | ||||||
|                 linkDecoded, |  | ||||||
|                 customTabsIntent, |  | ||||||
|                 articleViewer, |  | ||||||
|                 app |  | ||||||
|             ) |  | ||||||
|         } else { |  | ||||||
|             this.openItemUrlInternalBrowser( |  | ||||||
|                     linkDecoded, |  | ||||||
|                     customTabsIntent, |  | ||||||
|                     app |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| private fun openInBrowser(linkDecoded: String, app: Activity) { |  | ||||||
|     val intent = Intent(Intent.ACTION_VIEW) |  | ||||||
|     intent.data = Uri.parse(linkDecoded) |  | ||||||
|     try { |  | ||||||
|         app.startActivity(intent) |  | ||||||
|     } catch (e: ActivityNotFoundException) { |  | ||||||
|         Toast.makeText(app.baseContext, e.message, Toast.LENGTH_LONG).show() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun String.isUrlValid(): Boolean = |  | ||||||
|     this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches() |  | ||||||
|  |  | ||||||
| fun String.isBaseUrlValid(ctx: Context): Boolean { |  | ||||||
|     val baseUrl = this.toHttpUrlOrNull() |  | ||||||
|     var existsAndEndsWithSlash = false |  | ||||||
|     if (baseUrl != null) { |  | ||||||
|         val pathSegments = baseUrl.pathSegments |  | ||||||
|         existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1] |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun Context.openInBrowserAsNewTask(i: Item) { |  | ||||||
|     val intent = Intent(Intent.ACTION_VIEW) |  | ||||||
|     intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK |  | ||||||
|     intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp()) |  | ||||||
|     startActivity(intent) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class LinkOnTouchListener: View.OnTouchListener { |  | ||||||
|     override fun onTouch(v: View?, event: MotionEvent?): Boolean { |  | ||||||
|         var ret = false |  | ||||||
|         val widget: TextView = v as TextView |  | ||||||
|         val text: CharSequence = widget.text |  | ||||||
|         val stext = Spannable.Factory.getInstance().newSpannable(text) |  | ||||||
|  |  | ||||||
|         val action = event!!.action |  | ||||||
|  |  | ||||||
|         if (action == MotionEvent.ACTION_UP || |  | ||||||
|             action == MotionEvent.ACTION_DOWN) { |  | ||||||
|             var x: Float = event.x |  | ||||||
|             var y: Float = event.y |  | ||||||
|  |  | ||||||
|             x -= widget.totalPaddingLeft |  | ||||||
|             y -= widget.totalPaddingTop |  | ||||||
|  |  | ||||||
|             x += widget.scrollX |  | ||||||
|             y += widget.scrollY |  | ||||||
|  |  | ||||||
|             val layout = widget.layout |  | ||||||
|             val line = layout.getLineForVertical(y.toInt()) |  | ||||||
|             val off = layout.getOffsetForHorizontal(line, x) |  | ||||||
|  |  | ||||||
|             val link = stext.getSpans(off, off, ClickableSpan::class.java) |  | ||||||
|  |  | ||||||
|             if (link.isNotEmpty()) { |  | ||||||
|                 if (action == MotionEvent.ACTION_UP) { |  | ||||||
|                     link[0].onClick(widget) |  | ||||||
|                 } |  | ||||||
|                 ret = true |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return ret |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,404 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.widget.Toast |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.persistence.toEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.persistence.toView |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
| import kotlinx.coroutines.launch |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Callback |  | ||||||
| import retrofit2.Response |  | ||||||
| import java.text.SimpleDateFormat |  | ||||||
| import kotlin.concurrent.thread |  | ||||||
|  |  | ||||||
| /* |  | ||||||
| * Singleton class that contains the articles fetched from Selfoss, it allows sharing the items list |  | ||||||
| * between Activities and Fragments |  | ||||||
| */ |  | ||||||
| object SharedItems { |  | ||||||
|     var items: ArrayList<Item> = arrayListOf<Item>() |  | ||||||
|         get() { |  | ||||||
|             return ArrayList(field) |  | ||||||
|         } |  | ||||||
|         set(value) { |  | ||||||
|             field = ArrayList(value) |  | ||||||
|         } |  | ||||||
|     var focusedItems: ArrayList<Item> = arrayListOf<Item>() |  | ||||||
|         get() { |  | ||||||
|             return ArrayList(field) |  | ||||||
|         } |  | ||||||
|         set(value) { |  | ||||||
|             field = ArrayList(value) |  | ||||||
|         } |  | ||||||
|     var position = 0 |  | ||||||
|         set(value) { |  | ||||||
|             field = when { |  | ||||||
|                 value < 0 -> 0 |  | ||||||
|                 value > items.size -> items.size |  | ||||||
|                 else -> value |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     var displayedItems: String = "unread" |  | ||||||
|         set(value) { |  | ||||||
|             field = when (value) { |  | ||||||
|                 "all" -> "all" |  | ||||||
|                 "unread" -> "unread" |  | ||||||
|                 "read" -> "read" |  | ||||||
|                 "starred" -> "starred" |  | ||||||
|                 else -> "all" |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     var searchFilter: String? = null |  | ||||||
|     var sourceIDFilter: Long? = null |  | ||||||
|     var sourceFilter: String? = null |  | ||||||
|     var tagFilter: String? = null |  | ||||||
|     var itemsCaching = false |  | ||||||
|  |  | ||||||
|     var fetchedUnread = false |  | ||||||
|     var fetchedAll = false |  | ||||||
|     var fetchedStarred = false |  | ||||||
|  |  | ||||||
|     var badgeUnread = -1 |  | ||||||
|     var badgeAll = -1 |  | ||||||
|     var badgeStarred = -1 |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Add new items to the SharedItems list |  | ||||||
|      * |  | ||||||
|      * The new items are considered more updated than the ones already in the list. |  | ||||||
|      * The old items present in the new list are discarded and replaced by the new ones. |  | ||||||
|      * Items are compared according to the selfoss id, which should always be unique. |  | ||||||
|      */ |  | ||||||
|     fun appendNewItems(newItems: ArrayList<Item>) { |  | ||||||
|         var tmpItems = items |  | ||||||
|         if (tmpItems != newItems) { |  | ||||||
|             tmpItems = tmpItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList<Item> |  | ||||||
|             tmpItems.addAll(newItems) |  | ||||||
|             items = tmpItems |  | ||||||
|  |  | ||||||
|             sortItems() |  | ||||||
|             getFocusedItems() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun refreshFocusedItems(newItems: ArrayList<Item>) { |  | ||||||
|         val tmpItems = items |  | ||||||
|         tmpItems.removeAll(focusedItems) |  | ||||||
|  |  | ||||||
|         appendNewItems(newItems) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     suspend fun clearDBItems(db: AppDatabase) { |  | ||||||
|         db.itemsDao().deleteAllItems() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     suspend fun updateDatabase(db: AppDatabase) { |  | ||||||
|         if (itemsCaching) { |  | ||||||
|             if (items.isEmpty()) { |  | ||||||
|                 getFromDB(db) |  | ||||||
|             } |  | ||||||
|             db.itemsDao().deleteAllItems() |  | ||||||
|             db.itemsDao().insertAllItems(*(items.map { it.toEntity() }).toTypedArray()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun filter() { |  | ||||||
|         fun filterSearch(item: Item): Boolean { |  | ||||||
|             return if (!searchFilter.isEmptyOrNullOrNullString()) { |  | ||||||
|                 var matched = item.title.contains(searchFilter.toString(), true) |  | ||||||
|                 matched = matched || item.content.contains(searchFilter.toString(), true) |  | ||||||
|                 matched = matched || item.sourcetitle.contains(searchFilter.toString(), true) |  | ||||||
|                 matched |  | ||||||
|             } else { |  | ||||||
|                 true |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var tmpItems = focusedItems |  | ||||||
|         if (tagFilter != null) { |  | ||||||
|             tmpItems = tmpItems.filter { it.tags.tags.contains(tagFilter.toString()) } as ArrayList<Item> |  | ||||||
|         } |  | ||||||
|         if (searchFilter != null) { |  | ||||||
|             tmpItems = tmpItems.filter { filterSearch(it) } as ArrayList<Item> |  | ||||||
|         } |  | ||||||
|         if (sourceFilter != null) { |  | ||||||
|             tmpItems = tmpItems.filter { it.sourcetitle == sourceFilter } as ArrayList<Item> |  | ||||||
|         } |  | ||||||
|         focusedItems = tmpItems |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun getFocusedItems() { |  | ||||||
|         when (displayedItems) { |  | ||||||
|             "all" -> getAll() |  | ||||||
|             "unread" -> getUnRead() |  | ||||||
|             "read" -> getRead() |  | ||||||
|             "starred" -> getStarred() |  | ||||||
|             else -> getUnRead() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getUnRead() { |  | ||||||
|         displayedItems = "unread" |  | ||||||
|         focusedItems = items.filter { item -> item.unread } as ArrayList<Item> |  | ||||||
|         filter() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getRead() { |  | ||||||
|         displayedItems = "read" |  | ||||||
|         focusedItems = items.filter { item -> !item.unread } as ArrayList<Item> |  | ||||||
|         filter() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getStarred() { |  | ||||||
|         displayedItems = "starred" |  | ||||||
|         focusedItems = items.filter { item -> item.starred } as ArrayList<Item> |  | ||||||
|         filter() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun getAll() { |  | ||||||
|         displayedItems = "all" |  | ||||||
|         focusedItems = items |  | ||||||
|         filter() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     suspend fun getFromDB(db: AppDatabase) { |  | ||||||
|         if (itemsCaching) { |  | ||||||
|                     val dbItems = db.itemsDao().items().map { it.toView() } as ArrayList<Item> |  | ||||||
|                     appendNewItems(dbItems) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun removeItemAtIndex(index: Int) { |  | ||||||
|         val i = focusedItems[index] |  | ||||||
|         val tmpItems = focusedItems |  | ||||||
|         tmpItems.remove(i) |  | ||||||
|         focusedItems = tmpItems |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun addItemAtIndex(newItem: Item, index: Int) { |  | ||||||
|         val tmpItems = focusedItems |  | ||||||
|         tmpItems.add(index, newItem) |  | ||||||
|         focusedItems = tmpItems |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun readItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) { |  | ||||||
|         if (items.contains(item)) { |  | ||||||
|             position = items.indexOf(item) |  | ||||||
|             readItemAtPosition(app, api, db) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun readItems(db: AppDatabase, ids: List<String>) { |  | ||||||
|         for (id in ids) { |  | ||||||
|             val match = items.filter { it -> it.id == id } |  | ||||||
|             if (match.isNotEmpty() && match.size == 1) { |  | ||||||
|                 position = items.indexOf(match[0]) |  | ||||||
|                 val tmpItems = items |  | ||||||
|                 tmpItems[position].unread = false |  | ||||||
|                 items = tmpItems |  | ||||||
|                 resetDBItem(db) |  | ||||||
|                 badgeUnread-- |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun readItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) { |  | ||||||
|         val i = items[position] |  | ||||||
|  |  | ||||||
|         if (app.isNetworkAccessible(null)) { |  | ||||||
|             api.markItem(i.id).enqueue(object : Callback<SuccessResponse> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                         call: Call<SuccessResponse>, |  | ||||||
|                         response: Response<SuccessResponse> |  | ||||||
|                 ) { |  | ||||||
|  |  | ||||||
|                     val tmpItems = items |  | ||||||
|                     tmpItems[position].unread = false |  | ||||||
|                     items = tmpItems |  | ||||||
|  |  | ||||||
|                     resetDBItem(db) |  | ||||||
|                     getFocusedItems() |  | ||||||
|                     badgeUnread-- |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { |  | ||||||
|                     Toast.makeText( |  | ||||||
|                             app, |  | ||||||
|                             app.getString(R.string.cant_mark_read), |  | ||||||
|                             Toast.LENGTH_SHORT |  | ||||||
|                     ).show() |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         } else if (itemsCaching) { |  | ||||||
|             thread { |  | ||||||
|                 db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false)) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (position > items.size) { |  | ||||||
|             position -= 1 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun unreadItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) { |  | ||||||
|         if (items.contains(item) && !item.unread) { |  | ||||||
|             position = items.indexOf(item) |  | ||||||
|             unreadItemAtPosition(app, api, db) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun unreadItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) { |  | ||||||
|         val i = items[position] |  | ||||||
|  |  | ||||||
|         if (app.isNetworkAccessible(null)) { |  | ||||||
|             api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                     call: Call<SuccessResponse>, |  | ||||||
|                     response: Response<SuccessResponse> |  | ||||||
|                 ) { |  | ||||||
|  |  | ||||||
|                     val tmpItems = items |  | ||||||
|                     tmpItems[position].unread = true |  | ||||||
|                     items = tmpItems |  | ||||||
|  |  | ||||||
|                     resetDBItem(db) |  | ||||||
|                     getFocusedItems() |  | ||||||
|                     badgeUnread++ |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { |  | ||||||
|                     Toast.makeText( |  | ||||||
|                         app, |  | ||||||
|                         app.getString(R.string.cant_mark_unread), |  | ||||||
|                         Toast.LENGTH_SHORT |  | ||||||
|                     ).show() |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         } else if (itemsCaching) { |  | ||||||
|             thread { |  | ||||||
|                 db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false)) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun starItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) { |  | ||||||
|         if (items.contains(item) && !item.starred) { |  | ||||||
|             position = items.indexOf(item) |  | ||||||
|             starItemAtPosition(app, api, db) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun starItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) { |  | ||||||
|         val i = items[position] |  | ||||||
|  |  | ||||||
|         if (app.isNetworkAccessible(null)) { |  | ||||||
|             api.starrItem(i.id).enqueue(object : Callback<SuccessResponse> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                     call: Call<SuccessResponse>, |  | ||||||
|                     response: Response<SuccessResponse> |  | ||||||
|                 ) { |  | ||||||
|                     val tmpItems = items |  | ||||||
|                     tmpItems[position].starred = true |  | ||||||
|                     items = tmpItems |  | ||||||
|  |  | ||||||
|                     resetDBItem(db) |  | ||||||
|                     getFocusedItems() |  | ||||||
|                     badgeStarred++ |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 override fun onFailure( |  | ||||||
|                     call: Call<SuccessResponse>, |  | ||||||
|                     t: Throwable |  | ||||||
|                 ) { |  | ||||||
|                     Toast.makeText( |  | ||||||
|                         app, |  | ||||||
|                         app.getString(R.string.cant_mark_favortie), |  | ||||||
|                         Toast.LENGTH_SHORT |  | ||||||
|                     ).show() |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         } else { |  | ||||||
|             thread { |  | ||||||
|                 db.actionsDao().insertAllActions(ActionEntity(i.id, false, false, true, false)) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun unstarItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) { |  | ||||||
|         if (items.contains(item) && item.starred) { |  | ||||||
|             position = items.indexOf(item) |  | ||||||
|             unstarItemAtPosition(app, api, db) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun unstarItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) { |  | ||||||
|         val i = items[position] |  | ||||||
|  |  | ||||||
|         if (app.isNetworkAccessible(null)) { |  | ||||||
|             api.unstarrItem(i.id).enqueue(object : Callback<SuccessResponse> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                     call: Call<SuccessResponse>, |  | ||||||
|                     response: Response<SuccessResponse> |  | ||||||
|                 ) { |  | ||||||
|                     val tmpItems = items |  | ||||||
|                     tmpItems[position].starred = false |  | ||||||
|                     items = tmpItems |  | ||||||
|  |  | ||||||
|                     resetDBItem(db) |  | ||||||
|                     getFocusedItems() |  | ||||||
|                     badgeStarred-- |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 override fun onFailure( |  | ||||||
|                     call: Call<SuccessResponse>, |  | ||||||
|                     t: Throwable |  | ||||||
|                 ) { |  | ||||||
|                     Toast.makeText( |  | ||||||
|                         app, |  | ||||||
|                         app.getString(R.string.cant_unmark_favortie), |  | ||||||
|                         Toast.LENGTH_SHORT |  | ||||||
|                     ).show() |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         } else { |  | ||||||
|             thread { |  | ||||||
|                 db.actionsDao().insertAllActions(ActionEntity(i.id, false, false, false, true)) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun resetDBItem(db: AppDatabase) { |  | ||||||
|         if (itemsCaching) { |  | ||||||
|             val i = items[position] |  | ||||||
|             CoroutineScope(Dispatchers.IO).launch { |  | ||||||
|                 db.itemsDao().delete(i.toEntity()) |  | ||||||
|                 db.itemsDao().insertAllItems(i.toEntity()) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun unreadItemStatusAtIndex(position: Int): Boolean { |  | ||||||
|         return focusedItems[position].unread |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun computeBadges() { |  | ||||||
|         badgeUnread = items.filter { item -> item.unread }.size |  | ||||||
|         badgeStarred = items.filter { item -> item.starred }.size |  | ||||||
|         badgeAll = items.size |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun sortItems() { |  | ||||||
|         val tmpItems = ArrayList(items.sortedByDescending { parseDate(it.datetime) }) |  | ||||||
|         items = tmpItems |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils |  | ||||||
|  |  | ||||||
| import android.content.res.Resources |  | ||||||
|  |  | ||||||
| val Int.toPx: Int |  | ||||||
|     get() = (this * Resources.getSystem().displayMetrics.density).toInt() |  | ||||||
|  |  | ||||||
| val Int.toDp: Int |  | ||||||
|     get() = (this / Resources.getSystem().displayMetrics.density).toInt() |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.bottombar |  | ||||||
|  |  | ||||||
| import com.ashokvarma.bottomnavigation.TextBadgeItem |  | ||||||
|  |  | ||||||
| fun TextBadgeItem.removeBadge(): TextBadgeItem { |  | ||||||
|     this.setText("") |  | ||||||
|     this.hide() |  | ||||||
|     return this |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun TextBadgeItem.maybeShow(): TextBadgeItem = |  | ||||||
|     if (this.isHidden) this.show() else this |  | ||||||
| @@ -1,153 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.customtabs; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import androidx.browser.customtabs.CustomTabsClient; |  | ||||||
| import androidx.browser.customtabs.CustomTabsIntent; |  | ||||||
| import androidx.browser.customtabs.CustomTabsServiceConnection; |  | ||||||
| import androidx.browser.customtabs.CustomTabsSession; |  | ||||||
|  |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * This is a helper class to manage the connection to the Custom Tabs Service. |  | ||||||
|  */ |  | ||||||
| public class CustomTabActivityHelper implements ServiceConnectionCallback { |  | ||||||
|     private CustomTabsSession mCustomTabsSession; |  | ||||||
|     private CustomTabsClient mClient; |  | ||||||
|     private CustomTabsServiceConnection mConnection; |  | ||||||
|     private ConnectionCallback mConnectionCallback; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView. |  | ||||||
|      * |  | ||||||
|      * @param activity         The host activity. |  | ||||||
|      * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available. |  | ||||||
|      * @param uri              the Uri to be opened. |  | ||||||
|      * @param fallback         a CustomTabFallback to be used if Custom Tabs is not available. |  | ||||||
|      */ |  | ||||||
|     public static void openCustomTab(Activity activity, |  | ||||||
|                                      CustomTabsIntent customTabsIntent, |  | ||||||
|                                      Uri uri, |  | ||||||
|                                      CustomTabFallback fallback) { |  | ||||||
|         String packageName = CustomTabsHelper.getPackageNameToUse(activity); |  | ||||||
|  |  | ||||||
|         //If we cant find a package name, it means theres no browser that supports |  | ||||||
|         //Chrome Custom Tabs installed. So, we fallback to the webview |  | ||||||
|         if (packageName == null) { |  | ||||||
|             if (fallback != null) { |  | ||||||
|                 fallback.openUri(activity, uri); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             customTabsIntent.intent.setPackage(packageName); |  | ||||||
|             customTabsIntent.launchUrl(activity, uri); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Unbinds the Activity from the Custom Tabs Service. |  | ||||||
|      * |  | ||||||
|      * @param activity the activity that is connected to the service. |  | ||||||
|      */ |  | ||||||
|     public void unbindCustomTabsService(Activity activity) { |  | ||||||
|         if (mConnection == null) return; |  | ||||||
|         activity.unbindService(mConnection); |  | ||||||
|         mClient = null; |  | ||||||
|         mCustomTabsSession = null; |  | ||||||
|         mConnection = null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Creates or retrieves an exiting CustomTabsSession. |  | ||||||
|      * |  | ||||||
|      * @return a CustomTabsSession. |  | ||||||
|      */ |  | ||||||
|     public CustomTabsSession getSession() { |  | ||||||
|         if (mClient == null) { |  | ||||||
|             mCustomTabsSession = null; |  | ||||||
|         } else if (mCustomTabsSession == null) { |  | ||||||
|             mCustomTabsSession = mClient.newSession(null); |  | ||||||
|         } |  | ||||||
|         return mCustomTabsSession; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Register a Callback to be called when connected or disconnected from the Custom Tabs Service. |  | ||||||
|      * |  | ||||||
|      * @param connectionCallback |  | ||||||
|      */ |  | ||||||
|     public void setConnectionCallback(ConnectionCallback connectionCallback) { |  | ||||||
|         this.mConnectionCallback = connectionCallback; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Binds the Activity to the Custom Tabs Service. |  | ||||||
|      * |  | ||||||
|      * @param activity the activity to be binded to the service. |  | ||||||
|      */ |  | ||||||
|     public void bindCustomTabsService(Activity activity) { |  | ||||||
|         if (mClient != null) return; |  | ||||||
|  |  | ||||||
|         String packageName = CustomTabsHelper.getPackageNameToUse(activity); |  | ||||||
|         if (packageName == null) return; |  | ||||||
|  |  | ||||||
|         mConnection = new ServiceConnection(this); |  | ||||||
|         CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @return true if call to mayLaunchUrl was accepted. |  | ||||||
|      * @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}. |  | ||||||
|      */ |  | ||||||
|     public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) { |  | ||||||
|         if (mClient == null) return false; |  | ||||||
|  |  | ||||||
|         CustomTabsSession session = getSession(); |  | ||||||
|         return session != null && session.mayLaunchUrl(uri, extras, otherLikelyBundles); |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onServiceConnected(CustomTabsClient client) { |  | ||||||
|         mClient = client; |  | ||||||
|         mClient.warmup(0L); |  | ||||||
|         if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onServiceDisconnected() { |  | ||||||
|         mClient = null; |  | ||||||
|         mCustomTabsSession = null; |  | ||||||
|         if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * A Callback for when the service is connected or disconnected. Use those callbacks to |  | ||||||
|      * handle UI changes when the service is connected or disconnected. |  | ||||||
|      */ |  | ||||||
|     public interface ConnectionCallback { |  | ||||||
|         /** |  | ||||||
|          * Called when the service is connected. |  | ||||||
|          */ |  | ||||||
|         void onCustomTabsConnected(); |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Called when the service is disconnected. |  | ||||||
|          */ |  | ||||||
|         void onCustomTabsDisconnected(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * To be used as a fallback to open the Uri when Custom Tabs is not available. |  | ||||||
|      */ |  | ||||||
|     public interface CustomTabFallback { |  | ||||||
|         /** |  | ||||||
|          * @param activity The Activity that wants to open the Uri. |  | ||||||
|          * @param uri      The uri to be opened by the fallback. |  | ||||||
|          */ |  | ||||||
|         void openUri(Activity activity, Uri uri); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -1,130 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.customtabs; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.content.IntentFilter; |  | ||||||
| import android.content.pm.PackageManager; |  | ||||||
| import android.content.pm.ResolveInfo; |  | ||||||
| import android.net.Uri; |  | ||||||
| import androidx.browser.customtabs.CustomTabsService; |  | ||||||
| import android.text.TextUtils; |  | ||||||
| import android.util.Log; |  | ||||||
|  |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService; |  | ||||||
|  |  | ||||||
| @SuppressWarnings("ALL") |  | ||||||
| class CustomTabsHelper { |  | ||||||
|     private static final String TAG = "CustomTabsHelper"; |  | ||||||
|     private static final String STABLE_PACKAGE = "com.android.chrome"; |  | ||||||
|     private static final String BETA_PACKAGE = "com.chrome.beta"; |  | ||||||
|     private static final String DEV_PACKAGE = "com.chrome.dev"; |  | ||||||
|     private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; |  | ||||||
|     private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = |  | ||||||
|             "android.support.customtabs.extra.KEEP_ALIVE"; |  | ||||||
|  |  | ||||||
|     private static String sPackageNameToUse; |  | ||||||
|  |  | ||||||
|     private CustomTabsHelper() { |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static void addKeepAliveExtra(Context context, Intent intent) { |  | ||||||
|         Intent keepAliveIntent = new Intent().setClassName( |  | ||||||
|                 context.getPackageName(), KeepAliveService.class.getCanonicalName()); |  | ||||||
|         intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Goes through all apps that handle VIEW intents and have a warmup service. Picks |  | ||||||
|      * the one chosen by the user if there is one, otherwise makes a best effort to return a |  | ||||||
|      * valid package name. |  | ||||||
|      * <p> |  | ||||||
|      * This is <strong>not</strong> threadsafe. |  | ||||||
|      * |  | ||||||
|      * @param context {@link Context} to use for accessing {@link PackageManager}. |  | ||||||
|      * @return The package name recommended to use for connecting to custom tabs related components. |  | ||||||
|      */ |  | ||||||
|     public static String getPackageNameToUse(Context context) { |  | ||||||
|         if (sPackageNameToUse != null) return sPackageNameToUse; |  | ||||||
|  |  | ||||||
|         PackageManager pm = context.getPackageManager(); |  | ||||||
|         // Get default VIEW intent handler. |  | ||||||
|         Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); |  | ||||||
|         ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); |  | ||||||
|         String defaultViewHandlerPackageName = null; |  | ||||||
|         if (defaultViewHandlerInfo != null) { |  | ||||||
|             defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Get all apps that can handle VIEW intents. |  | ||||||
|         List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); |  | ||||||
|         List<String> packagesSupportingCustomTabs = new ArrayList<>(); |  | ||||||
|         for (ResolveInfo info : resolvedActivityList) { |  | ||||||
|             Intent serviceIntent = new Intent(); |  | ||||||
|             serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); |  | ||||||
|             serviceIntent.setPackage(info.activityInfo.packageName); |  | ||||||
|             if (pm.resolveService(serviceIntent, 0) != null) { |  | ||||||
|                 packagesSupportingCustomTabs.add(info.activityInfo.packageName); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents |  | ||||||
|         // and service calls. |  | ||||||
|         if (packagesSupportingCustomTabs.isEmpty()) { |  | ||||||
|             sPackageNameToUse = null; |  | ||||||
|         } else if (packagesSupportingCustomTabs.size() == 1) { |  | ||||||
|             sPackageNameToUse = packagesSupportingCustomTabs.get(0); |  | ||||||
|         } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) |  | ||||||
|                 && !hasSpecializedHandlerIntents(context, activityIntent) |  | ||||||
|                 && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { |  | ||||||
|             sPackageNameToUse = defaultViewHandlerPackageName; |  | ||||||
|         } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { |  | ||||||
|             sPackageNameToUse = STABLE_PACKAGE; |  | ||||||
|         } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { |  | ||||||
|             sPackageNameToUse = BETA_PACKAGE; |  | ||||||
|         } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { |  | ||||||
|             sPackageNameToUse = DEV_PACKAGE; |  | ||||||
|         } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { |  | ||||||
|             sPackageNameToUse = LOCAL_PACKAGE; |  | ||||||
|         } |  | ||||||
|         return sPackageNameToUse; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Used to check whether there is a specialized handler for a given intent. |  | ||||||
|      * |  | ||||||
|      * @param intent The intent to check with. |  | ||||||
|      * @return Whether there is a specialized handler for the given intent. |  | ||||||
|      */ |  | ||||||
|     private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) { |  | ||||||
|         try { |  | ||||||
|             PackageManager pm = context.getPackageManager(); |  | ||||||
|             List<ResolveInfo> handlers = pm.queryIntentActivities( |  | ||||||
|                     intent, |  | ||||||
|                     PackageManager.GET_RESOLVED_FILTER); |  | ||||||
|             if (handlers == null || handlers.isEmpty()) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             for (ResolveInfo resolveInfo : handlers) { |  | ||||||
|                 IntentFilter filter = resolveInfo.filter; |  | ||||||
|                 if (filter == null) continue; |  | ||||||
|                 if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue; |  | ||||||
|                 if (resolveInfo.activityInfo == null) continue; |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } catch (RuntimeException e) { |  | ||||||
|             Log.e(TAG, "Runtime exception while getting specialized handlers"); |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @return All possible chrome package names that provide custom tabs feature. |  | ||||||
|      */ |  | ||||||
|     public static String[] getPackages() { |  | ||||||
|         return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,33 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.customtabs; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| import android.content.ComponentName; |  | ||||||
| import androidx.browser.customtabs.CustomTabsClient; |  | ||||||
| import androidx.browser.customtabs.CustomTabsServiceConnection; |  | ||||||
|  |  | ||||||
| import java.lang.ref.WeakReference; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Implementation for the CustomTabsServiceConnection that avoids leaking the |  | ||||||
|  * ServiceConnectionCallback |  | ||||||
|  */ |  | ||||||
| public class ServiceConnection extends CustomTabsServiceConnection { |  | ||||||
|     // A weak reference to the ServiceConnectionCallback to avoid leaking it. |  | ||||||
|     private WeakReference<ServiceConnectionCallback> mConnectionCallback; |  | ||||||
|  |  | ||||||
|     public ServiceConnection(ServiceConnectionCallback connectionCallback) { |  | ||||||
|         mConnectionCallback = new WeakReference<>(connectionCallback); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { |  | ||||||
|         ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); |  | ||||||
|         if (connectionCallback != null) connectionCallback.onServiceConnected(client); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onServiceDisconnected(ComponentName name) { |  | ||||||
|         ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); |  | ||||||
|         if (connectionCallback != null) connectionCallback.onServiceDisconnected(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.customtabs; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| import androidx.browser.customtabs.CustomTabsClient; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| public interface ServiceConnectionCallback { |  | ||||||
|     /** |  | ||||||
|      * Called when the service is connected. |  | ||||||
|      * |  | ||||||
|      * @param client a CustomTabsClient |  | ||||||
|      */ |  | ||||||
|     void onServiceConnected(CustomTabsClient client); |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Called when the service is disconnected. |  | ||||||
|      */ |  | ||||||
|     void onServiceDisconnected(); |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.customtabs.helpers; |  | ||||||
|  |  | ||||||
| import android.app.Service; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Binder; |  | ||||||
| import android.os.IBinder; |  | ||||||
|  |  | ||||||
| public class KeepAliveService extends Service { |  | ||||||
|     private static final Binder sBinder = new Binder(); |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public IBinder onBind(Intent intent) { |  | ||||||
|         return sBinder; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| /* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */ |  | ||||||
| package apps.amine.bou.readerforselfoss.utils.drawer |  | ||||||
|  |  | ||||||
| import androidx.recyclerview.widget.RecyclerView |  | ||||||
| import android.view.View |  | ||||||
| import android.widget.ImageView |  | ||||||
| import android.widget.TextView |  | ||||||
|  |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
|  |  | ||||||
| open class CustomBaseViewHolder(var view: View) : RecyclerView.ViewHolder(view) { |  | ||||||
|     var icon: ImageView = view.findViewById(R.id.material_drawer_icon) |  | ||||||
|     var name: TextView = view.findViewById(R.id.material_drawer_name) |  | ||||||
|     var description: TextView = view.findViewById(R.id.material_drawer_description) |  | ||||||
| } |  | ||||||
| @@ -1,69 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.glide |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.graphics.Bitmap |  | ||||||
| import android.graphics.drawable.Drawable |  | ||||||
| import android.util.Base64 |  | ||||||
| import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory |  | ||||||
| import android.widget.ImageView |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import com.bumptech.glide.Glide |  | ||||||
| import com.bumptech.glide.RequestBuilder |  | ||||||
| import com.bumptech.glide.RequestManager |  | ||||||
| import com.bumptech.glide.load.model.GlideUrl |  | ||||||
| import com.bumptech.glide.load.model.LazyHeaders |  | ||||||
| import com.bumptech.glide.request.RequestOptions |  | ||||||
| import com.bumptech.glide.request.target.BitmapImageViewTarget |  | ||||||
| import java.io.ByteArrayInputStream |  | ||||||
| import java.io.ByteArrayOutputStream |  | ||||||
| import java.io.InputStream |  | ||||||
|  |  | ||||||
| fun Context.bitmapCenterCrop(config: Config, url: String, iv: ImageView) = |  | ||||||
|     Glide.with(this) |  | ||||||
|         .asBitmap() |  | ||||||
|         .loadMaybeBasicAuth(config, url) |  | ||||||
|         .apply(RequestOptions.centerCropTransform()) |  | ||||||
|         .into(iv) |  | ||||||
|  |  | ||||||
| fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) = |  | ||||||
|     Glide.with(this) |  | ||||||
|         .asBitmap() |  | ||||||
|         .loadMaybeBasicAuth(config, url) |  | ||||||
|         .apply(RequestOptions.centerCropTransform()) |  | ||||||
|         .into(object : BitmapImageViewTarget(iv) { |  | ||||||
|             override fun setResource(resource: Bitmap?) { |  | ||||||
|                 val circularBitmapDrawable = RoundedBitmapDrawableFactory.create( |  | ||||||
|                     resources, |  | ||||||
|                     resource |  | ||||||
|                 ) |  | ||||||
|                 circularBitmapDrawable.isCircular = true |  | ||||||
|                 iv.setImageDrawable(circularBitmapDrawable) |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
| fun RequestBuilder<Bitmap>.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Bitmap> { |  | ||||||
|     val builder: LazyHeaders.Builder = LazyHeaders.Builder() |  | ||||||
|     if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) { |  | ||||||
|         val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP) |  | ||||||
|         builder.addHeader("Authorization", basicAuth) |  | ||||||
|     } |  | ||||||
|     val glideUrl = GlideUrl(url, builder.build()) |  | ||||||
|     return this.load(glideUrl) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun RequestManager.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Drawable> { |  | ||||||
|     val builder: LazyHeaders.Builder = LazyHeaders.Builder() |  | ||||||
|     if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) { |  | ||||||
|         val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP) |  | ||||||
|         builder.addHeader("Authorization", basicAuth) |  | ||||||
|     } |  | ||||||
|     val glideUrl = GlideUrl(url, builder.build()) |  | ||||||
|     return this.load(glideUrl) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream { |  | ||||||
|     val byteArrayOutputStream = ByteArrayOutputStream() |  | ||||||
|     bitmap.compress(compressFormat, 80, byteArrayOutputStream) |  | ||||||
|     val bitmapData: ByteArray = byteArrayOutputStream.toByteArray() |  | ||||||
|     return ByteArrayInputStream(bitmapData) |  | ||||||
| } |  | ||||||
| @@ -1,33 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.glide |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.Config |  | ||||||
| import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient |  | ||||||
| import com.bumptech.glide.Glide |  | ||||||
| import com.bumptech.glide.GlideBuilder |  | ||||||
| import com.bumptech.glide.Registry |  | ||||||
| import com.bumptech.glide.load.model.GlideUrl |  | ||||||
| import com.bumptech.glide.module.GlideModule |  | ||||||
| import java.io.InputStream |  | ||||||
|  |  | ||||||
| class SelfSignedGlideModule : GlideModule { |  | ||||||
|  |  | ||||||
|     override fun applyOptions(context: Context?, builder: GlideBuilder?) { |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) { |  | ||||||
|  |  | ||||||
|         if (context != null) { |  | ||||||
|             val pref = context?.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) |  | ||||||
|             if (pref.getBoolean("isSelfSignedCert", false)) { |  | ||||||
|                 val client = getUnsafeHttpClient().build() |  | ||||||
|  |  | ||||||
|                 registry?.append( |  | ||||||
|                     GlideUrl::class.java, |  | ||||||
|                     InputStream::class.java, |  | ||||||
|                     com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory(client) |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,64 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.network |  | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.graphics.Color |  | ||||||
| import android.net.ConnectivityManager |  | ||||||
| import android.net.NetworkCapabilities |  | ||||||
| import android.os.Build |  | ||||||
| import android.view.View |  | ||||||
| import android.widget.TextView |  | ||||||
| import apps.amine.bou.readerforselfoss.R |  | ||||||
| import com.google.android.material.snackbar.Snackbar |  | ||||||
|  |  | ||||||
| var snackBarShown = false |  | ||||||
| var view: View? = null |  | ||||||
| lateinit var s: Snackbar |  | ||||||
|  |  | ||||||
| fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean { |  | ||||||
|     val networkIsAccessible = isNetworkAvailable(this) |  | ||||||
|  |  | ||||||
|     if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) { |  | ||||||
|         view = v |  | ||||||
|         s = Snackbar |  | ||||||
|             .make( |  | ||||||
|                 v, |  | ||||||
|                 R.string.no_network_connectivity, |  | ||||||
|                 Snackbar.LENGTH_INDEFINITE |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         s.setAction(android.R.string.ok) { |  | ||||||
|             snackBarShown = false |  | ||||||
|             s.dismiss() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         val view = s.view |  | ||||||
|         val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text) |  | ||||||
|         tv.setTextColor(Color.WHITE) |  | ||||||
|         s.show() |  | ||||||
|         snackBarShown = true |  | ||||||
|     } |  | ||||||
|     if (snackBarShown && networkIsAccessible && !overrideOffline) { |  | ||||||
|         s.dismiss() |  | ||||||
|     } |  | ||||||
|     return if(overrideOffline) overrideOffline else networkIsAccessible |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun isNetworkAvailable(context: Context): Boolean { |  | ||||||
|     val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager |  | ||||||
|  |  | ||||||
|      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |  | ||||||
|          val network = connectivityManager.activeNetwork ?: return false |  | ||||||
|          val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false |  | ||||||
|  |  | ||||||
|          return when { |  | ||||||
|              networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true |  | ||||||
|              networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true |  | ||||||
|              networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true |  | ||||||
|              networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true |  | ||||||
|              else -> false |  | ||||||
|          } |  | ||||||
|     } else { |  | ||||||
|         val network = connectivityManager.activeNetworkInfo ?: return false |  | ||||||
|          return network.isConnectedOrConnecting |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,73 +0,0 @@ | |||||||
| package apps.amine.bou.readerforselfoss.utils.persistence |  | ||||||
|  |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Item |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Source |  | ||||||
| import apps.amine.bou.readerforselfoss.api.selfoss.Tag |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity |  | ||||||
| import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity |  | ||||||
|  |  | ||||||
| fun TagEntity.toView(): Tag = |  | ||||||
|         Tag( |  | ||||||
|             this.tag, |  | ||||||
|             this.color, |  | ||||||
|             this.unread |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
| fun SourceEntity.toView(): Source = |  | ||||||
|         Source( |  | ||||||
|             this.id, |  | ||||||
|             this.title, |  | ||||||
|             SelfossTagType(this.tags), |  | ||||||
|             this.spout, |  | ||||||
|             this.error, |  | ||||||
|             this.icon |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
| fun Source.toEntity(): SourceEntity = |  | ||||||
|         SourceEntity( |  | ||||||
|             this.id, |  | ||||||
|             this.getTitleDecoded(), |  | ||||||
|             this.tags.tags, |  | ||||||
|             this.spout, |  | ||||||
|             this.error, |  | ||||||
|             this.icon.orEmpty() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
| fun Tag.toEntity(): TagEntity = |  | ||||||
|         TagEntity( |  | ||||||
|             this.tag, |  | ||||||
|             this.color, |  | ||||||
|             this.unread |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
| fun ItemEntity.toView(): Item = |  | ||||||
|         Item( |  | ||||||
|             this.id, |  | ||||||
|             this.datetime, |  | ||||||
|             this.title, |  | ||||||
|             this.content, |  | ||||||
|             this.unread, |  | ||||||
|             this.starred, |  | ||||||
|             this.thumbnail, |  | ||||||
|             this.icon, |  | ||||||
|             this.link, |  | ||||||
|             this.sourcetitle, |  | ||||||
|             SelfossTagType(this.tags) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
| fun Item.toEntity(): ItemEntity = |  | ||||||
|     ItemEntity( |  | ||||||
|         this.id, |  | ||||||
|         this.datetime, |  | ||||||
|         this.getTitleDecoded(), |  | ||||||
|         this.content, |  | ||||||
|         this.unread, |  | ||||||
|         this.starred, |  | ||||||
|         this.thumbnail, |  | ||||||
|         this.icon, |  | ||||||
|         this.link, |  | ||||||
|         this.getSourceTitle(), |  | ||||||
|         this.tags.tags |  | ||||||
|     ) |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <!-- Copyright 2015 Google Inc. All Rights Reserved. |  | ||||||
|      Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
|      you may not use this file except in compliance with the License. |  | ||||||
|      You may obtain a copy of the License at |  | ||||||
|          http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
|      Unless required by applicable law or agreed to in writing, software |  | ||||||
|      distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
|      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
|      See the License for the specific language governing permissions and |  | ||||||
|      limitations under the License. |  | ||||||
| --> |  | ||||||
| <set xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <translate android:fromXDelta="100%p" android:toXDelta="0" |  | ||||||
|                android:duration="@android:integer/config_mediumAnimTime"/> |  | ||||||
| </set> |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <!-- Copyright 2015 Google Inc. All Rights Reserved. |  | ||||||
|      Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
|      you may not use this file except in compliance with the License. |  | ||||||
|      You may obtain a copy of the License at |  | ||||||
|          http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
|      Unless required by applicable law or agreed to in writing, software |  | ||||||
|      distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
|      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
|      See the License for the specific language governing permissions and |  | ||||||
|      limitations under the License. |  | ||||||
| --> |  | ||||||
| <set xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <translate android:fromXDelta="0" android:toXDelta="-100%p" |  | ||||||
|                android:duration="@android:integer/config_mediumAnimTime"/> |  | ||||||
| </set> |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <selector xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <item android:state_selected="true" |  | ||||||
|         android:color="@color/red"/> |  | ||||||
|  |  | ||||||
|     <item android:state_selected="false" |  | ||||||
|         android:color="?android:attr/textColorPrimary" /> |  | ||||||
| </selector> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|         android:width="24dp" |  | ||||||
|         android:height="24dp" |  | ||||||
|         android:viewportWidth="24.0" |  | ||||||
|         android:viewportHeight="24.0"> |  | ||||||
|     <path |  | ||||||
|         android:fillColor="#FFFFFFFF" |  | ||||||
|         android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|         android:width="24dp" |  | ||||||
|         android:height="24dp" |  | ||||||
|         android:viewportWidth="24.0" |  | ||||||
|         android:viewportHeight="24.0"> |  | ||||||
|     <path |  | ||||||
|         android:fillColor="#FFFFFFFF" |  | ||||||
|         android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|  |  | ||||||
|     <item |  | ||||||
|         android:drawable="@color/ic_launcher_background"/> |  | ||||||
|  |  | ||||||
|     <item> |  | ||||||
|         <bitmap |  | ||||||
|             android:gravity="center" |  | ||||||
|             android:src="@mipmap/ic_launcher_foreground"/> |  | ||||||
|     </item> |  | ||||||
|  |  | ||||||
| </layer-list> |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 406 B | 
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24" android:viewportWidth="24" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|         android:width="24dp" |  | ||||||
|         android:height="24dp" |  | ||||||
|         android:viewportWidth="24.0" |  | ||||||
|         android:viewportHeight="24.0"> |  | ||||||
|     <path |  | ||||||
|         android:fillColor="#FF000000" |  | ||||||
|         android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|         android:width="24dp" |  | ||||||
|         android:height="24dp" |  | ||||||
|         android:viewportWidth="24.0" |  | ||||||
|         android:viewportHeight="24.0"> |  | ||||||
|     <path |  | ||||||
|         android:fillColor="#FF000000" |  | ||||||
|         android:pathData="M13,12h7v1.5h-7zM13,9.5h7L20,11h-7zM13,14.5h7L20,16h-7zM21,4L3,4c-1.1,0 -2,0.9 -2,2v13c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM21,19h-9L12,6h9v13z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|         android:width="24dp" |  | ||||||
|         android:height="24dp" |  | ||||||
|         android:viewportWidth="24.0" |  | ||||||
|         android:viewportHeight="24.0"> |  | ||||||
|     <path |  | ||||||
|         android:fillColor="#FFFFFFFF" |  | ||||||
|         android:pathData="M13,12h7v1.5h-7zM13,9.5h7L20,11h-7zM13,14.5h7L20,16h-7zM21,4L3,4c-1.1,0 -2,0.9 -2,2v13c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM21,19h-9L12,6h9v13z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|         android:width="24dp" |  | ||||||
|         android:height="24dp" |  | ||||||
|         android:viewportWidth="24.0" |  | ||||||
|         android:viewportHeight="24.0"> |  | ||||||
|     <path |  | ||||||
|         android:fillColor="#FF000000" |  | ||||||
|         android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|         android:width="24dp" |  | ||||||
|         android:height="24dp" |  | ||||||
|         android:viewportWidth="24.0" |  | ||||||
|         android:viewportHeight="24.0"> |  | ||||||
|     <path |  | ||||||
|         android:fillColor="#FF000000" |  | ||||||
|         android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="54.751434dp" android:viewportHeight="18.756023" |  | ||||||
|     android:viewportWidth="20.554007" android:width="60dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FFFFFFFF" |  | ||||||
|         android:pathData="m5.7968,14.6109c-2.7907,-2.7367 -4.4957,-4.7131 -5.018,-5.8165 -2.102,-4.4408 0.2424,-8.7943 4.7357,-8.7943 1.635,0 2.7056,0.425 3.9688,1.5755l0.7937,0.723 0.7937,-0.723c1.2631,-1.1505 2.3337,-1.5755 3.9688,-1.5755 4.4933,0 6.8377,4.3535 4.7357,8.7943 -0.5223,1.1035 -2.2274,3.0799 -5.018,5.8165 -2.3248,2.2798 -4.3409,4.1451 -4.4802,4.1451 -0.1393,0 -2.1554,-1.8653 -4.4802,-4.1451z" android:strokeWidth="0.0933392"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/> |  | ||||||
| </vector> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| <vector android:height="24dp" android:tint="#FFFFFF" |  | ||||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" |  | ||||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |  | ||||||
|     <path android:fillColor="#FF000000" android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z"/> |  | ||||||
| </vector> |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user