Compare commits
	
		
			6 Commits
		
	
	
		
			r
			...
			1c57435f54
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1c57435f54 | |||
| 4bb20a75d7 | |||
| 1f20e19a97 | |||
| 9b372a45ce | |||
| f4f8503037 | |||
| 5b70ae138e | 
							
								
								
									
										73
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | # 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
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | ### 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
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | ## 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 | ||||||
							
								
								
									
										554
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										554
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,554 @@ | |||||||
|  | **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. | ||||||
|  |  | ||||||
|  | **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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										674
									
								
								LICENSE.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,674 @@ | |||||||
|  |                     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>. | ||||||
							
								
								
									
										39
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,3 +1,38 @@ | |||||||
| # Multiplatform version [here](https://gitea.amine-louveau.fr/Amine_L/ReaderForSelfoss-multiplatform) | # ReaderForSelfoss **(Only available from F-Droid)** | ||||||
|  |  | ||||||
| # Original moved  [here](https://gitea.amine-louveau.fr/Amine_L/ReaderforSelfoss) | [](https://crowdin.com/project/readerforselfoss) | ||||||
|  |  | ||||||
|  | It's an RSS Reader for Android, that **only** works with [Selfoss](https://selfoss.aditu.de/) | ||||||
|  |  | ||||||
|  | **The project is not dead at all.**  | ||||||
|  |  | ||||||
|  | I still want to work on it, but for the last few months, I didn't have that much time to do so.  | ||||||
|  |  | ||||||
|  | If you are a developer, don't hesitate to help with PRs. | ||||||
|  |  | ||||||
|  | If you are a user, you can still create new issues. I'll fix them when I can. | ||||||
|  |  | ||||||
|  | <a href="https://f-droid.org/packages/apps.amine.bou.readerforselfoss"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a> | ||||||
|  |  | ||||||
|  | ## Screen captures | ||||||
|  |  | ||||||
|  | <img src="res//fr-card.png?raw=true" alt="card view" width="400"/> <img src="res//fr-list.png?raw=true" alt="list view" width="400"/> | ||||||
|  |  | ||||||
|  | ## Like my app ? | ||||||
|  |  | ||||||
|  | <a href="https://www.buymeacoffee.com/aminecmi" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/lato-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a> | ||||||
|  |  | ||||||
|  | ## Want to help ? | ||||||
|  |  | ||||||
|  | 1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/). | ||||||
|  |  | ||||||
|  | 2. Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md). | ||||||
|  |  | ||||||
|  | 3. Build the project by following [these steps](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide) | ||||||
|  |  | ||||||
|  | ## Useful links | ||||||
|  |  | ||||||
|  | - [Check what changed](https://github.com/aminecmi/ReaderforSelfoss/blob/master/CHANGELOG.md) | ||||||
|  | - [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1) | ||||||
|  | - [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues) | ||||||
|  | - [Help translation the app](https://crowdin.com/project/readerforselfoss) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | /build | ||||||
							
								
								
									
										154
									
								
								app/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								app/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | |||||||
|  | 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 { | ||||||
|  |         sourceCompatibility JavaVersion.VERSION_1_8 | ||||||
|  |         targetCompatibility JavaVersion.VERSION_1_8 | ||||||
|  |     } | ||||||
|  |     compileSdkVersion 30 | ||||||
|  |     buildToolsVersion '30.0.3' | ||||||
|  |     buildFeatures { | ||||||
|  |         viewBinding true | ||||||
|  |     } | ||||||
|  |     defaultConfig { | ||||||
|  |         applicationId "apps.amine.bou.readerforselfoss" | ||||||
|  |         minSdkVersion 16 | ||||||
|  |         targetSdkVersion 30 | ||||||
|  |         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 false | ||||||
|  |             proguardFiles getDefaultProguardFile('proguard-android.txt'), | ||||||
|  |                     'proguard-rules.pro' | ||||||
|  |         } | ||||||
|  |         debug { | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     flavorDimensions "build" | ||||||
|  |     productFlavors { | ||||||
|  |         githubConfig { | ||||||
|  |             versionNameSuffix '-github' | ||||||
|  |             dimension "build" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | dependencies { | ||||||
|  |     // 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') | ||||||
|  |     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | ||||||
|  |     // Android Support | ||||||
|  |     implementation "androidx.appcompat:appcompat:1.3.0-alpha02" | ||||||
|  |     implementation 'com.google.android.material:material:1.3.0-beta01' | ||||||
|  |     implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01' | ||||||
|  |     implementation "androidx.legacy:legacy-support-v4:$android_version" | ||||||
|  |     implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02' | ||||||
|  |     implementation "androidx.browser:browser:1.3.0" | ||||||
|  |     implementation "androidx.cardview:cardview:$android_version" | ||||||
|  |     implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha2' | ||||||
|  |     implementation 'org.jsoup:jsoup:1.13.1' | ||||||
|  |  | ||||||
|  |     //multidex | ||||||
|  |     implementation 'androidx.multidex:multidex:2.0.1' | ||||||
|  |  | ||||||
|  |     // About | ||||||
|  |     implementation('com.mikepenz:aboutlibraries:6.2.0@aar') { | ||||||
|  |         transitive = true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Retrofit + http logging + okhttp | ||||||
|  |     implementation 'com.squareup.retrofit2:retrofit:2.3.0' | ||||||
|  |     implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0' | ||||||
|  |     implementation 'com.squareup.retrofit2:converter-gson:2.3.0' | ||||||
|  |     implementation 'com.burgstaller:okhttp-digest:1.12' | ||||||
|  |  | ||||||
|  |     // Material-ish things | ||||||
|  |     implementation 'com.ashokvarma.android:bottom-navigation-bar:2.1.0' | ||||||
|  |     implementation 'com.github.jd-alexander:LikeButton:0.2.3' | ||||||
|  |     implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' | ||||||
|  |  | ||||||
|  |     // glide | ||||||
|  |     implementation 'com.github.bumptech.glide:glide:4.1.1' | ||||||
|  |     implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1' | ||||||
|  |  | ||||||
|  |     // Drawer | ||||||
|  |     implementation 'co.zsmb:materialdrawer-kt:2.0.2' | ||||||
|  |  | ||||||
|  |     // Themes | ||||||
|  |     implementation 'com.52inc:scoops:1.0.0' | ||||||
|  |     implementation 'com.jaredrummler:colorpicker:1.0.2' | ||||||
|  |     implementation 'com.github.rubensousa:floatingtoolbar:1.5.1' | ||||||
|  |  | ||||||
|  |     // Pager | ||||||
|  |     implementation 'me.relex:circleindicator:2.0.0@aar' | ||||||
|  |  | ||||||
|  |     //PhotoView | ||||||
|  |     implementation 'com.github.chrisbanes:PhotoView:2.0.0' | ||||||
|  |  | ||||||
|  |     implementation 'androidx.core:core-ktx:1.5.0-alpha05' | ||||||
|  |  | ||||||
|  |     implementation "androidx.lifecycle:lifecycle-livedata:2.3.0-rc01" | ||||||
|  |     implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0-rc01" | ||||||
|  |  | ||||||
|  |     implementation "androidx.room:room-runtime:2.3.0-alpha04" | ||||||
|  |     kapt "androidx.room:room-compiler:2.3.0-alpha04" | ||||||
|  |  | ||||||
|  |     implementation "android.arch.work:work-runtime-ktx:$work_version" | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | # 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 | ||||||
| @@ -0,0 +1,96 @@ | |||||||
|  | { | ||||||
|  |   "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\")" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,176 @@ | |||||||
|  | { | ||||||
|  |   "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\")" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,226 @@ | |||||||
|  | { | ||||||
|  |   "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\")" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,226 @@ | |||||||
|  | { | ||||||
|  |   "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\")" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | // TODO: test source adding | ||||||
| @@ -0,0 +1,100 @@ | |||||||
|  | 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.pressKey | ||||||
|  | import androidx.test.espresso.action.ViewActions.typeText | ||||||
|  | import androidx.test.espresso.assertion.ViewAssertions.matches | ||||||
|  | import androidx.test.espresso.contrib.DrawerActions | ||||||
|  | import androidx.test.espresso.intent.Intents | ||||||
|  | import androidx.test.espresso.intent.Intents.intended | ||||||
|  | import androidx.test.espresso.intent.Intents.times | ||||||
|  | import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent | ||||||
|  | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed | ||||||
|  | import androidx.test.espresso.matcher.ViewMatchers.isRoot | ||||||
|  | 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 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()) | ||||||
|  |  | ||||||
|  |         openActionBarOverflowOrOptionsMenu(context) | ||||||
|  |  | ||||||
|  |         onView(withText(R.string.action_disconnect)).perform(click()) | ||||||
|  |  | ||||||
|  |         intended(hasComponent(LoginActivity::class.java.name), times(1)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // TODO: test articles opening and actions for cards and lists | ||||||
|  |  | ||||||
|  |     @After | ||||||
|  |     fun releaseIntents() { | ||||||
|  |         Intents.release() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,177 @@ | |||||||
|  | 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.urlLayout)).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.loginLayout)).check(matches(isHintOrErrorEnabled())) | ||||||
|  |         onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled())) | ||||||
|  |  | ||||||
|  |         onView(withId(R.id.loginView)).perform(click()).perform( | ||||||
|  |                 typeText(username), | ||||||
|  |                 closeSoftKeyboard() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled())) | ||||||
|  |  | ||||||
|  |         onView(withId(R.id.signInButton)).perform(click()) | ||||||
|  |  | ||||||
|  |         onView(withId(R.id.passwordLayout)).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.urlLayout)).check(matches(isHintOrErrorEnabled())) | ||||||
|  |         onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled())) | ||||||
|  |         onView(withId(R.id.passwordLayout)).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()) | ||||||
|  |  | ||||||
|  |         intended(hasComponent(HomeActivity::class.java.name)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @After | ||||||
|  |     fun releaseIntents() { | ||||||
|  |         Intents.release() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,66 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | 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 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 | ||||||
|  |  | ||||||
|  |     @Rule @JvmField | ||||||
|  |     val rule = ActivityTestRule(MainActivity::class.java, true, false) | ||||||
|  |  | ||||||
|  |     @Before | ||||||
|  |     fun setUp() { | ||||||
|  |         intent = Intent() | ||||||
|  |         val context = getInstrumentation().targetContext | ||||||
|  |  | ||||||
|  |         // create a SharedPreferences editor | ||||||
|  |         preferencesEditor = PreferenceManager.getDefaultSharedPreferences(context).edit() | ||||||
|  |  | ||||||
|  |         Intents.init() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun checkFirstOpenLaunchesIntro() { | ||||||
|  |         preferencesEditor.putBoolean("firstStart", true) | ||||||
|  |         preferencesEditor.commit() | ||||||
|  |  | ||||||
|  |         rule.launchActivity(intent) | ||||||
|  |  | ||||||
|  |         intended(hasComponent(MainActivity::class.java.name)) | ||||||
|  |         intended(hasComponent(LoginActivity::class.java.name), times(0)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun checkNotFirstOpenLaunchesLogin() { | ||||||
|  |         preferencesEditor.putBoolean("firstStart", false) | ||||||
|  |         preferencesEditor.commit() | ||||||
|  |  | ||||||
|  |         rule.launchActivity(intent) | ||||||
|  |  | ||||||
|  |         intended(hasComponent(MainActivity::class.java.name)) | ||||||
|  |         intended(hasComponent(LoginActivity::class.java.name)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @After | ||||||
|  |     fun releaseIntents() { | ||||||
|  |         Intents.release() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | import com.google.android.material.textfield.TextInputLayout | ||||||
|  | import androidx.test.espresso.matcher.ViewMatchers | ||||||
|  | import android.view.View | ||||||
|  | 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 TextInputLayout) { | ||||||
|  |                     return false | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return item.isHintEnabled || item.isErrorEnabled | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  | fun withMenu(id: Int, titleId: Int): Matcher<View> = | ||||||
|  |         Matchers.anyOf( | ||||||
|  |                 ViewMatchers.withId(id), | ||||||
|  |                 ViewMatchers.withText(titleId) | ||||||
|  |         ) | ||||||
							
								
								
									
										85
									
								
								app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | <?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"> | ||||||
|  |             <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"> | ||||||
|  |             <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> | ||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/ic_launcher-web.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/ic_launcher-web.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 20 KiB | 
| @@ -0,0 +1,247 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Build | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.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) | ||||||
|  |         if  (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |             scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val drawable = binding.nameInput.background | ||||||
|  |         drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         // TODO: clean | ||||||
|  |         if(Build.VERSION.SDK_INT > 16) { | ||||||
|  |             binding.nameInput.background = drawable | ||||||
|  |         } else{ | ||||||
|  |             binding.nameInput.setBackgroundDrawable(drawable) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val drawable1 = binding.sourceUri.background | ||||||
|  |         drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP) | ||||||
|  |  | ||||||
|  |         if(Build.VERSION.SDK_INT > 16) { | ||||||
|  |             binding.sourceUri.background = drawable1 | ||||||
|  |         } else{ | ||||||
|  |             binding.sourceUri.setBackgroundDrawable(drawable1) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val drawable2 = binding.tags.background | ||||||
|  |         drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP) | ||||||
|  |  | ||||||
|  |         if(Build.VERSION.SDK_INT > 16) { | ||||||
|  |             binding.tags.background = drawable2 | ||||||
|  |         } else{ | ||||||
|  |             binding.tags.setBackgroundDrawable(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 sourceDetailsAvailable = | ||||||
|  |             title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty() | ||||||
|  |  | ||||||
|  |         if (sourceDetailsAvailable) { | ||||||
|  |             Toast.makeText(this, R.string.form_not_complete, 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() | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1485
									
								
								app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1485
									
								
								app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.MenuItem | ||||||
|  | import androidx.appcompat.app.AppCompatActivity | ||||||
|  | import androidx.fragment.app.FragmentManager | ||||||
|  | import androidx.fragment.app.FragmentStatePagerAdapter | ||||||
|  | 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(supportFragmentManager) | ||||||
|  |         binding.pager.currentItem = position | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||||
|  |         when (item.itemId) { | ||||||
|  |             android.R.id.home -> { | ||||||
|  |                 onBackPressed() | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return super.onOptionsItemSelected(item) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private inner class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { | ||||||
|  |  | ||||||
|  |         override fun getCount(): Int { | ||||||
|  |             return allImages.size | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun getItem(position: Int): ImageFragment { | ||||||
|  |             return ImageFragment.newInstance(allImages[position]) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,298 @@ | |||||||
|  | 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 android.widget.Toast | ||||||
|  | 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.Config | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible | ||||||
|  | import com.mikepenz.aboutlibraries.Libs | ||||||
|  | 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 = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) | ||||||
|  |         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.loginLayout.visibility = visi | ||||||
|  |             binding.passwordLayout.visibility = visi | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.withHttpLogin.setOnCheckedChangeListener { _, b -> | ||||||
|  |             isWithHTTPLogin = !isWithHTTPLogin | ||||||
|  |             val visi: Int = if (b) View.VISIBLE else View.GONE | ||||||
|  |  | ||||||
|  |             binding.httpLoginInput.visibility = visi | ||||||
|  |             binding.httpPasswordInput.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) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.preference.PreferenceManager | ||||||
|  | import androidx.appcompat.app.AppCompatActivity | ||||||
|  | import apps.amine.bou.readerforselfoss.databinding.ActivityAddSourceBinding | ||||||
|  | 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() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										152
									
								
								app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | import android.app.NotificationChannel | ||||||
|  | import android.app.NotificationManager | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.SharedPreferences | ||||||
|  | import android.graphics.drawable.Drawable | ||||||
|  | import android.net.Uri | ||||||
|  | import android.os.Build | ||||||
|  | import android.preference.PreferenceManager | ||||||
|  | import android.widget.ImageView | ||||||
|  | import androidx.multidex.MultiDexApplication | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.ApiVersion | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi | ||||||
|  | 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 retrofit2.Call | ||||||
|  | import retrofit2.Callback | ||||||
|  | import retrofit2.Response | ||||||
|  | import java.util.UUID.randomUUID | ||||||
|  |  | ||||||
|  | var dateTimeFormatter = "yyyy-MM-dd HH:mm:ss" | ||||||
|  |  | ||||||
|  | class MyApp : MultiDexApplication() { | ||||||
|  |     private lateinit var config: Config | ||||||
|  |     private lateinit var api: SelfossApi | ||||||
|  |     private lateinit var settings: SharedPreferences | ||||||
|  |     private lateinit var sharedPref: SharedPreferences | ||||||
|  |  | ||||||
|  |     private var apiVersionMajor: Int = 0 | ||||||
|  |  | ||||||
|  |     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() | ||||||
|  |  | ||||||
|  |         sharedPref = PreferenceManager.getDefaultSharedPreferences(this) | ||||||
|  |         apiVersionMajor = sharedPref.getInt("apiVersionMajor", 0) | ||||||
|  |         settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) | ||||||
|  |  | ||||||
|  |         api = SelfossApi( | ||||||
|  |                 this, | ||||||
|  |                 null, | ||||||
|  |                 settings.getBoolean("isSelfSignedCert", false), | ||||||
|  |                 sharedPref.getString("api_timeout", "-1")!!.toLong() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         getApiMajorVersion() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getApiMajorVersion() { | ||||||
|  |         api.apiVersion.enqueue(object : Callback<ApiVersion> { | ||||||
|  |             override fun onFailure(call: Call<ApiVersion>, t: Throwable) { | ||||||
|  |                 if (apiVersionMajor >= 4) { | ||||||
|  |                     dateTimeFormatter = "yyyy-MM-dd'T'HH:mm:ssXXX" | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun onResponse(call: Call<ApiVersion>, response: Response<ApiVersion>) { | ||||||
|  |                 val version = response.body() as ApiVersion | ||||||
|  |                 apiVersionMajor = version.getApiMajorVersion() | ||||||
|  |                 sharedPref.edit().putInt("apiVersionMajor", apiVersionMajor).commit() | ||||||
|  |  | ||||||
|  |                 if (apiVersionMajor >= 4) { | ||||||
|  |                     dateTimeFormatter = "yyyy-MM-dd'T'HH:mm:ssXXX" | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,353 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.SharedPreferences | ||||||
|  | import android.graphics.drawable.ColorDrawable | ||||||
|  | import android.os.Build | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.preference.PreferenceManager | ||||||
|  | import androidx.fragment.app.FragmentManager | ||||||
|  | import androidx.fragment.app.FragmentStatePagerAdapter | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
|  | import androidx.viewpager.widget.ViewPager | ||||||
|  | import androidx.appcompat.app.AppCompatActivity | ||||||
|  | import android.view.Menu | ||||||
|  | import android.view.MenuItem | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.room.Room | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.Item | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse | ||||||
|  | import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding | ||||||
|  | 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.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.themes.AppColors | ||||||
|  | import apps.amine.bou.readerforselfoss.themes.Toppings | ||||||
|  | import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.Config | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.persistence.toEntity | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.succeeded | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.toggleStar | ||||||
|  | import com.ftinc.scoop.Scoop | ||||||
|  | import me.relex.circleindicator.CircleIndicator | ||||||
|  | import retrofit2.Call | ||||||
|  | import retrofit2.Callback | ||||||
|  | import retrofit2.Response | ||||||
|  | import kotlin.concurrent.thread | ||||||
|  |  | ||||||
|  | class ReaderActivity : AppCompatActivity() { | ||||||
|  |  | ||||||
|  |     private var markOnScroll: Boolean = false | ||||||
|  |     private var currentItem: Int = 0 | ||||||
|  |     private lateinit var userIdentifier: String | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |     val JUSTIFY = 1 | ||||||
|  |     val ALIGN_LEFT = 2 | ||||||
|  |  | ||||||
|  |     private fun showMenuItem(willAddToFavorite: Boolean) { | ||||||
|  |         toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite | ||||||
|  |         toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun canFavorite() { | ||||||
|  |         showMenuItem(true) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun canRemoveFromFavorite() { | ||||||
|  |         showMenuItem(false) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private lateinit var editor: SharedPreferences.Editor | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         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) | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |             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(supportFragmentManager, AppColors(this@ReaderActivity)) | ||||||
|  |         binding.pager.currentItem = currentItem | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onResume() { | ||||||
|  |         super.onResume() | ||||||
|  |  | ||||||
|  |         notifyAdapter() | ||||||
|  |  | ||||||
|  |         binding.pager.setPageTransformer(true, DepthPageTransformer()) | ||||||
|  |         (binding.indicator as CircleIndicator).setViewPager(binding.pager) | ||||||
|  |  | ||||||
|  |         binding.pager.addOnPageChangeListener( | ||||||
|  |             object : ViewPager.SimpleOnPageChangeListener() { | ||||||
|  |  | ||||||
|  |                 override fun onPageSelected(position: Int) { | ||||||
|  |  | ||||||
|  |                     if (allItems[position].starred) { | ||||||
|  |                         canRemoveFromFavorite() | ||||||
|  |                     } else { | ||||||
|  |                         canFavorite() | ||||||
|  |                     } | ||||||
|  |                     readItem(allItems[binding.pager.currentItem]) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun readItem(item: Item) { | ||||||
|  |         if (markOnScroll) { | ||||||
|  |             thread { | ||||||
|  |                 db.itemsDao().delete(item.toEntity()) | ||||||
|  |             } | ||||||
|  |             if (this@ReaderActivity.isNetworkAccessible(this@ReaderActivity.findViewById(R.id.reader_activity_view))) { | ||||||
|  |                 api.markItem(item.id).enqueue( | ||||||
|  |                     object : Callback<SuccessResponse> { | ||||||
|  |                         override fun onResponse( | ||||||
|  |                             call: Call<SuccessResponse>, | ||||||
|  |                             response: Response<SuccessResponse> | ||||||
|  |                         ) { | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         override fun onFailure( | ||||||
|  |                             call: Call<SuccessResponse>, | ||||||
|  |                             t: Throwable | ||||||
|  |                         ) { | ||||||
|  |                             thread { | ||||||
|  |                                 db.itemsDao().insertAllItems(item.toEntity()) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |             } else { | ||||||
|  |                 thread { | ||||||
|  |                     db.actionsDao().insertAllActions(ActionEntity(item.id, true, false, false, false)) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun notifyAdapter() { | ||||||
|  |         (binding.pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onPause() { | ||||||
|  |         super.onPause() | ||||||
|  |         if (markOnScroll) { | ||||||
|  |             binding.pager.clearOnPageChangeListeners() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onSaveInstanceState(oldInstanceState: Bundle) { | ||||||
|  |         super.onSaveInstanceState(oldInstanceState) | ||||||
|  |         oldInstanceState.clear() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) : | ||||||
|  |         FragmentStatePagerAdapter(fm) { | ||||||
|  |  | ||||||
|  |         override fun getCount(): Int { | ||||||
|  |             return allItems.size | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun getItem(position: Int): ArticleFragment { | ||||||
|  |             return ArticleFragment.newInstance(position, allItems) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun startUpdate(container: ViewGroup) { | ||||||
|  |             super.startUpdate(container) | ||||||
|  |  | ||||||
|  |             container.background = ColorDrawable( | ||||||
|  |                 ContextCompat.getColor( | ||||||
|  |                     this@ReaderActivity, | ||||||
|  |                     appColors.colorBackground | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun alignmentMenu(showJustify: Boolean) { | ||||||
|  |         toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify | ||||||
|  |         toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateOptionsMenu(menu: Menu): Boolean { | ||||||
|  |         val inflater = menuInflater | ||||||
|  |         inflater.inflate(R.menu.reader_menu, menu) | ||||||
|  |         toolbarMenu = menu | ||||||
|  |  | ||||||
|  |         if (!allItems.isEmpty() && allItems[currentItem].starred) { | ||||||
|  |             canRemoveFromFavorite() | ||||||
|  |         } else { | ||||||
|  |             canFavorite() | ||||||
|  |         } | ||||||
|  |         if (activeAlignment == JUSTIFY) { | ||||||
|  |             alignmentMenu(false) | ||||||
|  |         } else { | ||||||
|  |             alignmentMenu(true) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||||
|  |         fun afterSave() { | ||||||
|  |             allItems[binding.pager.currentItem] = | ||||||
|  |                     allItems[binding.pager.currentItem].toggleStar() | ||||||
|  |             notifyAdapter() | ||||||
|  |             canRemoveFromFavorite() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fun afterUnsave() { | ||||||
|  |             allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar() | ||||||
|  |             notifyAdapter() | ||||||
|  |             canFavorite() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         when (item.itemId) { | ||||||
|  |             android.R.id.home -> { | ||||||
|  |                 onBackPressed() | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             R.id.save -> { | ||||||
|  |                 if (this@ReaderActivity.isNetworkAccessible(null)) { | ||||||
|  |                     api.starrItem(allItems[binding.pager.currentItem].id) | ||||||
|  |                         .enqueue(object : Callback<SuccessResponse> { | ||||||
|  |                             override fun onResponse( | ||||||
|  |                                 call: Call<SuccessResponse>, | ||||||
|  |                                 response: Response<SuccessResponse> | ||||||
|  |                             ) { | ||||||
|  |                                 afterSave() | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             override fun onFailure( | ||||||
|  |                                 call: Call<SuccessResponse>, | ||||||
|  |                                 t: Throwable | ||||||
|  |                             ) { | ||||||
|  |                                 Toast.makeText( | ||||||
|  |                                     baseContext, | ||||||
|  |                                     R.string.cant_mark_favortie, | ||||||
|  |                                     Toast.LENGTH_SHORT | ||||||
|  |                                 ).show() | ||||||
|  |                             } | ||||||
|  |                         }) | ||||||
|  |                 } else { | ||||||
|  |                     thread { | ||||||
|  |                         db.actionsDao().insertAllActions(ActionEntity(allItems[binding.pager.currentItem].id, false, false, true, false)) | ||||||
|  |                         afterSave() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             R.id.unsave -> { | ||||||
|  |                 if (this@ReaderActivity.isNetworkAccessible(null)) { | ||||||
|  |                     api.unstarrItem(allItems[binding.pager.currentItem].id) | ||||||
|  |                         .enqueue(object : Callback<SuccessResponse> { | ||||||
|  |                             override fun onResponse( | ||||||
|  |                                 call: Call<SuccessResponse>, | ||||||
|  |                                 response: Response<SuccessResponse> | ||||||
|  |                             ) { | ||||||
|  |                                 afterUnsave() | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             override fun onFailure( | ||||||
|  |                                 call: Call<SuccessResponse>, | ||||||
|  |                                 t: Throwable | ||||||
|  |                             ) { | ||||||
|  |                                 Toast.makeText( | ||||||
|  |                                     baseContext, | ||||||
|  |                                     R.string.cant_unmark_favortie, | ||||||
|  |                                     Toast.LENGTH_SHORT | ||||||
|  |                                 ).show() | ||||||
|  |                             } | ||||||
|  |                         }) | ||||||
|  |                 } else { | ||||||
|  |                     thread { | ||||||
|  |                         db.actionsDao().insertAllActions(ActionEntity(allItems[binding.pager.currentItem].id, false, false, false, true)) | ||||||
|  |                         afterUnsave() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             R.id.align_left -> { | ||||||
|  |                 editor.putInt("text_align", ALIGN_LEFT) | ||||||
|  |                 editor.apply() | ||||||
|  |                 alignmentMenu(true) | ||||||
|  |                 refreshFragment() | ||||||
|  |             } | ||||||
|  |             R.id.align_justify -> { | ||||||
|  |                 editor.putInt("text_align", JUSTIFY) | ||||||
|  |                 editor.apply() | ||||||
|  |                 alignmentMenu(false) | ||||||
|  |                 refreshFragment() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return super.onOptionsItemSelected(item) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun refreshFragment() { | ||||||
|  |         finish() | ||||||
|  |         overridePendingTransition(0, 0) | ||||||
|  |         startActivity(intent) | ||||||
|  |         overridePendingTransition(0, 0) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         var allItems: ArrayList<Item> = ArrayList() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,113 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.content.res.ColorStateList | ||||||
|  | import android.os.Build | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.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.ActivityImageBinding | ||||||
|  | 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) | ||||||
|  |  | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         binding = ActivitySourcesBinding.inflate(layoutInflater) | ||||||
|  |         val view = binding.root | ||||||
|  |  | ||||||
|  |         setContentView(view) | ||||||
|  |  | ||||||
|  |         val scoop = Scoop.getInstance() | ||||||
|  |         scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar) | ||||||
|  |         if  (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |             scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         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(this@SourcesActivity.findViewById(R.id.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)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,220 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.adapters | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.Context | ||||||
|  | import androidx.cardview.widget.CardView | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.ImageView.ScaleType | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
|  | 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.databinding.CardItemBinding | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity | ||||||
|  | import apps.amine.bou.readerforselfoss.themes.AppColors | ||||||
|  | import apps.amine.bou.readerforselfoss.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.network.isNetworkAccessible | ||||||
|  | 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 | ||||||
|  | import com.like.LikeButton | ||||||
|  | import com.like.OnLikeListener | ||||||
|  | import retrofit2.Call | ||||||
|  | import retrofit2.Callback | ||||||
|  | import retrofit2.Response | ||||||
|  | import kotlin.concurrent.thread | ||||||
|  |  | ||||||
|  | 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.isLiked = itm.starred | ||||||
|  |             binding.title.text = itm.getTitleDecoded() | ||||||
|  |             binding.title.setTextColor(ContextCompat.getColor( | ||||||
|  |                     c, | ||||||
|  |                     appColors.textColor | ||||||
|  |             )) | ||||||
|  |             binding.title.setOnTouchListener(LinkOnTouchListener()) | ||||||
|  |  | ||||||
|  |             binding.title.setLinkTextColor(appColors.colorAccent) | ||||||
|  |  | ||||||
|  |             binding.sourceTitleAndDate.text = itm.sourceAndDateText() | ||||||
|  |  | ||||||
|  |             binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor( | ||||||
|  |                     c, | ||||||
|  |                     appColors.textColor | ||||||
|  |             )) | ||||||
|  |  | ||||||
|  |             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) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             binding.favButton.isLiked = itm.starred | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItemCount(): Int { | ||||||
|  |         return items.size | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) { | ||||||
|  |         init { | ||||||
|  |             binding.root.setCardBackgroundColor(appColors.cardBackgroundColor) | ||||||
|  |             handleClickListeners() | ||||||
|  |             handleCustomTabActions() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private fun handleClickListeners() { | ||||||
|  |  | ||||||
|  |             binding.favButton.setOnLikeListener(object : OnLikeListener { | ||||||
|  |                 override fun liked(likeButton: LikeButton) { | ||||||
|  |                     val (id) = items[bindingAdapterPosition] | ||||||
|  |                     if (c.isNetworkAccessible(null)) { | ||||||
|  |                         api.starrItem(id).enqueue(object : Callback<SuccessResponse> { | ||||||
|  |                             override fun onResponse( | ||||||
|  |                                 call: Call<SuccessResponse>, | ||||||
|  |                                 response: Response<SuccessResponse> | ||||||
|  |                             ) { | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             override fun onFailure( | ||||||
|  |                                 call: Call<SuccessResponse>, | ||||||
|  |                                 t: Throwable | ||||||
|  |                             ) { | ||||||
|  |                                 binding.favButton.isLiked = false | ||||||
|  |                                 Toast.makeText( | ||||||
|  |                                     c, | ||||||
|  |                                     R.string.cant_mark_favortie, | ||||||
|  |                                     Toast.LENGTH_SHORT | ||||||
|  |                                 ).show() | ||||||
|  |                             } | ||||||
|  |                         }) | ||||||
|  |                     } else { | ||||||
|  |                         thread { | ||||||
|  |                             db.actionsDao().insertAllActions(ActionEntity(id, false, false, true, false)) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 override fun unLiked(likeButton: LikeButton) { | ||||||
|  |                     val (id) = items[bindingAdapterPosition] | ||||||
|  |                     if (c.isNetworkAccessible(null)) { | ||||||
|  |                         api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> { | ||||||
|  |                             override fun onResponse( | ||||||
|  |                                 call: Call<SuccessResponse>, | ||||||
|  |                                 response: Response<SuccessResponse> | ||||||
|  |                             ) { | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             override fun onFailure( | ||||||
|  |                                 call: Call<SuccessResponse>, | ||||||
|  |                                 t: Throwable | ||||||
|  |                             ) { | ||||||
|  |                                 binding.favButton.isLiked = true | ||||||
|  |                                 Toast.makeText( | ||||||
|  |                                     c, | ||||||
|  |                                     R.string.cant_unmark_favortie, | ||||||
|  |                                     Toast.LENGTH_SHORT | ||||||
|  |                                 ).show() | ||||||
|  |                             } | ||||||
|  |                         }) | ||||||
|  |                     } else { | ||||||
|  |                         thread { | ||||||
|  |                             db.actionsDao().insertAllActions(ActionEntity(id, false, false, false, true)) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |  | ||||||
|  |             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 | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,135 @@ | |||||||
|  | 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.text.Spannable | ||||||
|  | import android.text.style.ClickableSpan | ||||||
|  | import android.util.TypedValue | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.MotionEvent | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.TextView | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
|  | 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.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.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.like.LikeButton | ||||||
|  | import com.like.OnLikeListener | ||||||
|  | import retrofit2.Call | ||||||
|  | import retrofit2.Callback | ||||||
|  | import retrofit2.Response | ||||||
|  | import java.util.* | ||||||
|  | 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.setTextColor(ContextCompat.getColor( | ||||||
|  |                     c, | ||||||
|  |                     appColors.textColor | ||||||
|  |             )) | ||||||
|  |  | ||||||
|  |             binding.title.setOnTouchListener(LinkOnTouchListener()) | ||||||
|  |  | ||||||
|  |             binding.title.setLinkTextColor(appColors.colorAccent) | ||||||
|  |  | ||||||
|  |             binding.sourceTitleAndDate.text = itm.sourceAndDateText() | ||||||
|  |  | ||||||
|  |             binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor( | ||||||
|  |                     c, | ||||||
|  |                     appColors.textColor | ||||||
|  |             )) | ||||||
|  |  | ||||||
|  |             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 | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,241 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.adapters | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.graphics.Color | ||||||
|  | import com.google.android.material.snackbar.Snackbar | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import android.widget.TextView | ||||||
|  | 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.themes.AppColors | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.Config | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.persistence.toEntity | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.succeeded | ||||||
|  | import retrofit2.Call | ||||||
|  | import retrofit2.Callback | ||||||
|  | import retrofit2.Response | ||||||
|  | import kotlin.concurrent.thread | ||||||
|  |  | ||||||
|  | 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(newItems: ArrayList<Item>) { | ||||||
|  |         items = newItems | ||||||
|  |         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) { | ||||||
|  |                 items.add(position, i) | ||||||
|  |                 thread { | ||||||
|  |                     db.itemsDao().insertAllItems(i.toEntity()) | ||||||
|  |                 } | ||||||
|  |                 notifyItemInserted(position) | ||||||
|  |                 updateItems(items) | ||||||
|  |  | ||||||
|  |                 if (app.isNetworkAccessible(null)) { | ||||||
|  |                     api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> { | ||||||
|  |                         override fun onResponse( | ||||||
|  |                             call: Call<SuccessResponse>, | ||||||
|  |                             response: Response<SuccessResponse> | ||||||
|  |                         ) { | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { | ||||||
|  |                             items.remove(i) | ||||||
|  |                             thread { | ||||||
|  |                                 db.itemsDao().delete(i.toEntity()) | ||||||
|  |                             } | ||||||
|  |                             notifyItemRemoved(position) | ||||||
|  |                             updateItems(items) | ||||||
|  |                         } | ||||||
|  |                     }) | ||||||
|  |                 } else { | ||||||
|  |                     thread { | ||||||
|  |                         db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |         val view = s.view | ||||||
|  |         val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text) | ||||||
|  |         tv.setTextColor(Color.WHITE) | ||||||
|  |         s.show() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun markSnackbar(i: Item, position: Int) { | ||||||
|  |         val s = Snackbar | ||||||
|  |             .make( | ||||||
|  |                 app.findViewById(R.id.coordLayout), | ||||||
|  |                 R.string.marked_as_unread, | ||||||
|  |                 Snackbar.LENGTH_LONG | ||||||
|  |             ) | ||||||
|  |             .setAction(R.string.undo_string) { | ||||||
|  |                 items.add(position, i) | ||||||
|  |                 thread { | ||||||
|  |                     db.itemsDao().delete(i.toEntity()) | ||||||
|  |                 } | ||||||
|  |                 notifyItemInserted(position) | ||||||
|  |                 updateItems(items) | ||||||
|  |  | ||||||
|  |                 if (app.isNetworkAccessible(null)) { | ||||||
|  |                     api.markItem(i.id).enqueue(object : Callback<SuccessResponse> { | ||||||
|  |                         override fun onResponse( | ||||||
|  |                             call: Call<SuccessResponse>, | ||||||
|  |                             response: Response<SuccessResponse> | ||||||
|  |                         ) { | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { | ||||||
|  |                             items.remove(i) | ||||||
|  |                             thread { | ||||||
|  |                                 db.itemsDao().insertAllItems(i.toEntity()) | ||||||
|  |                             } | ||||||
|  |                             notifyItemRemoved(position) | ||||||
|  |                             updateItems(items) | ||||||
|  |                         } | ||||||
|  |                     }) | ||||||
|  |                 } else { | ||||||
|  |                     thread { | ||||||
|  |                         db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |         val view = s.view | ||||||
|  |         val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text) | ||||||
|  |         tv.setTextColor(Color.WHITE) | ||||||
|  |         s.show() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun handleItemAtIndex(position: Int) { | ||||||
|  |         if (unreadItemStatusAtIndex(position)) { | ||||||
|  |             readItemAtIndex(position) | ||||||
|  |         } else { | ||||||
|  |             unreadItemAtIndex(position) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun unreadItemStatusAtIndex(position: Int): Boolean { | ||||||
|  |         return items[position].unread | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun readItemAtIndex(position: Int) { | ||||||
|  |         val i = items[position] | ||||||
|  |         items.remove(i) | ||||||
|  |         notifyItemRemoved(position) | ||||||
|  |         updateItems(items) | ||||||
|  |  | ||||||
|  |         thread { | ||||||
|  |             db.itemsDao().delete(i.toEntity()) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (app.isNetworkAccessible(null)) { | ||||||
|  |             api.markItem(i.id).enqueue(object : Callback<SuccessResponse> { | ||||||
|  |                 override fun onResponse( | ||||||
|  |                     call: Call<SuccessResponse>, | ||||||
|  |                     response: Response<SuccessResponse> | ||||||
|  |                 ) { | ||||||
|  |  | ||||||
|  |                     unmarkSnackbar(i, position) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { | ||||||
|  |                     Toast.makeText( | ||||||
|  |                         app, | ||||||
|  |                         app.getString(R.string.cant_mark_read), | ||||||
|  |                         Toast.LENGTH_SHORT | ||||||
|  |                     ).show() | ||||||
|  |                     items.add(position, i) | ||||||
|  |                     notifyItemInserted(position) | ||||||
|  |                     updateItems(items) | ||||||
|  |  | ||||||
|  |                     thread { | ||||||
|  |                         db.itemsDao().insertAllItems(i.toEntity()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         } else { | ||||||
|  |             thread { | ||||||
|  |                 db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun unreadItemAtIndex(position: Int) { | ||||||
|  |         val i = items[position] | ||||||
|  |         items.remove(i) | ||||||
|  |         notifyItemRemoved(position) | ||||||
|  |         updateItems(items) | ||||||
|  |  | ||||||
|  |         thread { | ||||||
|  |             db.itemsDao().insertAllItems(i.toEntity()) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (app.isNetworkAccessible(null)) { | ||||||
|  |             api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> { | ||||||
|  |                 override fun onResponse( | ||||||
|  |                     call: Call<SuccessResponse>, | ||||||
|  |                     response: Response<SuccessResponse> | ||||||
|  |                 ) { | ||||||
|  |  | ||||||
|  |                     markSnackbar(i, position) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { | ||||||
|  |                     Toast.makeText( | ||||||
|  |                         app, | ||||||
|  |                         app.getString(R.string.cant_mark_unread), | ||||||
|  |                         Toast.LENGTH_SHORT | ||||||
|  |                     ).show() | ||||||
|  |                     items.add(i) | ||||||
|  |                     notifyItemInserted(position) | ||||||
|  |                     updateItems(items) | ||||||
|  |  | ||||||
|  |                     thread { | ||||||
|  |                         db.itemsDao().delete(i.toEntity()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         } else { | ||||||
|  |             thread { | ||||||
|  |                 db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun addItemAtIndex(item: Item, position: Int) { | ||||||
|  |         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) | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,106 @@ | |||||||
|  | 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() | ||||||
|  |                         } | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | 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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,59 @@ | |||||||
|  | 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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | 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> | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | 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 | ||||||
|  |         } | ||||||
|  | } | ||||||
| @@ -0,0 +1,238 @@ | |||||||
|  | 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.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.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(ResponseBody.create(MediaType.parse("text/plain"), "")) | ||||||
|  |                                 .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) | ||||||
|  |  | ||||||
|  |     fun readItems( | ||||||
|  |         tag: String?, | ||||||
|  |         sourceId: Long?, | ||||||
|  |         search: String?, | ||||||
|  |         itemsNumber: Int, | ||||||
|  |         offset: Int | ||||||
|  |     ): Call<List<Item>> = | ||||||
|  |         getItems("read", tag, sourceId, search, itemsNumber, offset) | ||||||
|  |  | ||||||
|  |     fun newItems( | ||||||
|  |         tag: String?, | ||||||
|  |         sourceId: Long?, | ||||||
|  |         search: String?, | ||||||
|  |         itemsNumber: Int, | ||||||
|  |         offset: Int | ||||||
|  |     ): Call<List<Item>> = | ||||||
|  |         getItems("unread", tag, sourceId, search, itemsNumber, offset) | ||||||
|  |  | ||||||
|  |     fun starredItems( | ||||||
|  |         tag: String?, | ||||||
|  |         sourceId: Long?, | ||||||
|  |         search: String?, | ||||||
|  |         itemsNumber: Int, | ||||||
|  |         offset: Int | ||||||
|  |     ): Call<List<Item>> = | ||||||
|  |         getItems("starred", tag, sourceId, search, itemsNumber, offset) | ||||||
|  |  | ||||||
|  |     fun allItems(): Call<List<Item>> = | ||||||
|  |         service.allItems(userName, password) | ||||||
|  |  | ||||||
|  |     fun allNewItems(): Call<List<Item>> = | ||||||
|  |             getItems("unread", null, null, null, 200, 0) | ||||||
|  |  | ||||||
|  |     fun allReadItems(): Call<List<Item>> = | ||||||
|  |             getItems("read", null, null, null, 200, 0) | ||||||
|  |  | ||||||
|  |     fun allStarredItems(): Call<List<Item>> = | ||||||
|  |             getItems("read", null, null, null, 200, 0) | ||||||
|  |  | ||||||
|  |     private fun getItems( | ||||||
|  |         type: String, | ||||||
|  |         tag: String?, | ||||||
|  |         sourceId: Long?, | ||||||
|  |         search: String?, | ||||||
|  |         items: Int, | ||||||
|  |         offset: Int | ||||||
|  |     ): Call<List<Item>> = | ||||||
|  |         service.getItems(type, tag, sourceId, search, userName, password, items, offset) | ||||||
|  |  | ||||||
|  |     fun markItem(itemId: String): Call<SuccessResponse> = | ||||||
|  |         service.markAsRead(itemId, userName, password) | ||||||
|  |  | ||||||
|  |     fun unmarkItem(itemId: String): Call<SuccessResponse> = | ||||||
|  |         service.unmarkAsRead(itemId, userName, password) | ||||||
|  |  | ||||||
|  |     fun readAll(ids: List<String>): Call<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) | ||||||
|  |  | ||||||
|  |     val stats: Call<Stats> | ||||||
|  |         get() = 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) | ||||||
|  | } | ||||||
| @@ -0,0 +1,247 @@ | |||||||
|  | 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 | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | 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") val 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> { | ||||||
|  |         var allImages = ArrayList<String>() | ||||||
|  |  | ||||||
|  |         for ( image in Jsoup.parse(content).getElementsByTag("img")) { | ||||||
|  |             val url = image.attr("src") | ||||||
|  |             if (url.toLowerCase().contains(".jpg") || | ||||||
|  |                     url.toLowerCase().contains(".jpeg") || | ||||||
|  |                     url.toLowerCase().contains(".png") || | ||||||
|  |                     url.toLowerCase().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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,127 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.api.selfoss | ||||||
|  |  | ||||||
|  | import retrofit2.Call | ||||||
|  | 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") | ||||||
|  |     fun getItems( | ||||||
|  |         @Query("type") type: String, | ||||||
|  |         @Query("tag") tag: String?, | ||||||
|  |         @Query("source") source: Long?, | ||||||
|  |         @Query("search") search: String?, | ||||||
|  |         @Query("username") username: String, | ||||||
|  |         @Query("password") password: String, | ||||||
|  |         @Query("items") items: Int, | ||||||
|  |         @Query("offset") offset: Int | ||||||
|  |     ): Call<List<Item>> | ||||||
|  |  | ||||||
|  |     @GET("items") | ||||||
|  |     fun allItems( | ||||||
|  |         @Query("username") username: String, | ||||||
|  |         @Query("password") password: String | ||||||
|  |     ): Call<List<Item>> | ||||||
|  |  | ||||||
|  |     @Headers("Content-Type: application/x-www-form-urlencoded") | ||||||
|  |     @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") | ||||||
|  |     fun markAllAsRead( | ||||||
|  |         @Field("ids[]") ids: List<String>, | ||||||
|  |         @Query("username") username: String, | ||||||
|  |         @Query("password") password: String | ||||||
|  |     ): Call<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") | ||||||
|  |     fun stats( | ||||||
|  |         @Query("username") username: String, | ||||||
|  |         @Query("password") password: String | ||||||
|  |     ): Call<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> | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | 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()) | ||||||
|  |         } | ||||||
|  | } | ||||||
| @@ -0,0 +1,186 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.background | ||||||
|  |  | ||||||
|  | import android.app.NotificationManager | ||||||
|  | import android.app.PendingIntent | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.preference.PreferenceManager | ||||||
|  | import androidx.core.app.NotificationCompat | ||||||
|  | import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT | ||||||
|  | import androidx.core.app.NotificationCompat.PRIORITY_LOW | ||||||
|  | import androidx.room.Room | ||||||
|  | import androidx.work.Worker | ||||||
|  | import androidx.work.WorkerParameters | ||||||
|  | import apps.amine.bou.readerforselfoss.MainActivity | ||||||
|  | import apps.amine.bou.readerforselfoss.R | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.Item | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4 | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.Config | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.persistence.toEntity | ||||||
|  | import retrofit2.Call | ||||||
|  | import retrofit2.Callback | ||||||
|  | import retrofit2.Response | ||||||
|  | import java.util.* | ||||||
|  | import kotlin.concurrent.schedule | ||||||
|  | import kotlin.concurrent.thread | ||||||
|  |  | ||||||
|  | class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { | ||||||
|  |     lateinit var db: AppDatabase | ||||||
|  |  | ||||||
|  |     override fun doWork(): Result { | ||||||
|  |         if (context.isNetworkAccessible(null)) { | ||||||
|  |  | ||||||
|  |             val notificationManager = | ||||||
|  |                 applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||||||
|  |  | ||||||
|  |             val notification = NotificationCompat.Builder(applicationContext, Config.syncChannelId) | ||||||
|  |                 .setContentTitle(context.getString(R.string.loading_notification_title)) | ||||||
|  |                 .setContentText(context.getString(R.string.loading_notification_text)) | ||||||
|  |                 .setOngoing(true) | ||||||
|  |                 .setPriority(PRIORITY_LOW) | ||||||
|  |                 .setChannelId(Config.syncChannelId) | ||||||
|  |                 .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp) | ||||||
|  |  | ||||||
|  |             notificationManager.notify(1, notification.build()) | ||||||
|  |  | ||||||
|  |             val settings = | ||||||
|  |                 this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) | ||||||
|  |             val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context) | ||||||
|  |             val notifyNewItems = sharedPref.getBoolean("notify_new_items", false) | ||||||
|  |  | ||||||
|  |             db = Room.databaseBuilder( | ||||||
|  |                 applicationContext, | ||||||
|  |                 AppDatabase::class.java, "selfoss-database" | ||||||
|  |             ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() | ||||||
|  |  | ||||||
|  |             val api = SelfossApi( | ||||||
|  |                 this.context, | ||||||
|  |                 null, | ||||||
|  |                 settings.getBoolean("isSelfSignedCert", false), | ||||||
|  |                 sharedPref.getString("api_timeout", "-1")!!.toLong() | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             api.allNewItems().enqueue(object : Callback<List<Item>> { | ||||||
|  |                 override fun onFailure(call: Call<List<Item>>, t: Throwable) { | ||||||
|  |                     Timer("", false).schedule(4000) { | ||||||
|  |                         notificationManager.cancel(1) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 override fun onResponse( | ||||||
|  |                     call: Call<List<Item>>, | ||||||
|  |                     response: Response<List<Item>> | ||||||
|  |                 ) { | ||||||
|  |                     storeItems(response, true, notifyNewItems, notificationManager) | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             api.allReadItems().enqueue(object : Callback<List<Item>> { | ||||||
|  |                 override fun onFailure(call: Call<List<Item>>, t: Throwable) { | ||||||
|  |                     Timer("", false).schedule(4000) { | ||||||
|  |                         notificationManager.cancel(1) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 override fun onResponse( | ||||||
|  |                         call: Call<List<Item>>, | ||||||
|  |                         response: Response<List<Item>> | ||||||
|  |                 ) { | ||||||
|  |                     storeItems(response, false, notifyNewItems, notificationManager) | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             api.allStarredItems().enqueue(object : Callback<List<Item>> { | ||||||
|  |                 override fun onFailure(call: Call<List<Item>>, t: Throwable) { | ||||||
|  |                     Timer("", false).schedule(4000) { | ||||||
|  |                         notificationManager.cancel(1) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 override fun onResponse( | ||||||
|  |                         call: Call<List<Item>>, | ||||||
|  |                         response: Response<List<Item>> | ||||||
|  |                 ) { | ||||||
|  |                     storeItems(response, false, notifyNewItems, notificationManager) | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |  | ||||||
|  |             thread { | ||||||
|  |                 val actions = db.actionsDao().actions() | ||||||
|  |  | ||||||
|  |                 actions.forEach { action -> | ||||||
|  |                     when { | ||||||
|  |                         action.read -> doAndReportOnFail(api.markItem(action.articleId), action) | ||||||
|  |                         action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action) | ||||||
|  |                         action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action) | ||||||
|  |                         action.unstarred -> doAndReportOnFail( | ||||||
|  |                             api.unstarrItem(action.articleId), | ||||||
|  |                             action | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return Result.success() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun storeItems(response: Response<List<Item>>, newItems: Boolean, notifyNewItems: Boolean, notificationManager: NotificationManager) { | ||||||
|  |         thread { | ||||||
|  |             if (response.body() != null) { | ||||||
|  |                 val apiItems = (response.body() as ArrayList<Item>) | ||||||
|  |  | ||||||
|  |                 if (newItems) { | ||||||
|  |                     db.itemsDao().deleteAllItems() | ||||||
|  |                 } | ||||||
|  |                 db.itemsDao() | ||||||
|  |                         .insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray()) | ||||||
|  |  | ||||||
|  |                 val newSize = apiItems.filter { it.unread }.size | ||||||
|  |                 if (newItems && notifyNewItems && newSize > 0) { | ||||||
|  |  | ||||||
|  |                     val intent = Intent(context, MainActivity::class.java).apply { | ||||||
|  |                         flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK | ||||||
|  |                     } | ||||||
|  |                     val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0) | ||||||
|  |  | ||||||
|  |                     val newItemsNotification = NotificationCompat.Builder(applicationContext, Config.newItemsChannelId) | ||||||
|  |                             .setContentTitle(context.getString(R.string.new_items_notification_title)) | ||||||
|  |                             .setContentText(context.getString(R.string.new_items_notification_text, newSize)) | ||||||
|  |                             .setPriority(PRIORITY_DEFAULT) | ||||||
|  |                             .setChannelId(Config.newItemsChannelId) | ||||||
|  |                             .setContentIntent(pendingIntent) | ||||||
|  |                             .setAutoCancel(true) | ||||||
|  |                             .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp) | ||||||
|  |  | ||||||
|  |                     Timer("", false).schedule(4000) { | ||||||
|  |                         notificationManager.notify(2, newItemsNotification.build()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 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) { | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,596 @@ | |||||||
|  | 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.Build | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.preference.PreferenceManager | ||||||
|  | import android.view.* | ||||||
|  | import android.webkit.* | ||||||
|  | import androidx.browser.customtabs.CustomTabsIntent | ||||||
|  | import com.google.android.material.floatingactionbutton.FloatingActionButton | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
|  | import androidx.core.widget.NestedScrollView | ||||||
|  | import androidx.appcompat.app.AlertDialog | ||||||
|  | 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.api.selfoss.SuccessResponse | ||||||
|  | import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding | ||||||
|  | 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.themes.AppColors | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.Config | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent | ||||||
|  | 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.isEmptyOrNullOrNullString | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible | ||||||
|  | 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.succeeded | ||||||
|  | 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.concurrent.ExecutionException | ||||||
|  | import kotlin.collections.ArrayList | ||||||
|  | import kotlin.concurrent.thread | ||||||
|  |  | ||||||
|  | class ArticleFragment : Fragment() { | ||||||
|  |     private lateinit var pageNumber: Number | ||||||
|  |     private var fontSize: Int = 16 | ||||||
|  |     private lateinit var allItems: ArrayList<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 = "" | ||||||
|  |  | ||||||
|  |     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) | ||||||
|  |  | ||||||
|  |         pageNumber = requireArguments().getInt(ARG_POSITION) | ||||||
|  |         allItems = requireArguments().getParcelableArrayList<Item>(ARG_ITEMS) as ArrayList<Item> | ||||||
|  |  | ||||||
|  |         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 = allItems[pageNumber.toInt()].getLinkDecoded() | ||||||
|  |             contentText = allItems[pageNumber.toInt()].content | ||||||
|  |             contentTitle = allItems[pageNumber.toInt()].getTitleDecoded() | ||||||
|  |             contentImage = allItems[pageNumber.toInt()].getThumbnail(requireActivity()) | ||||||
|  |             contentSource = allItems[pageNumber.toInt()].sourceAndDateText() | ||||||
|  |             allImages = allItems[pageNumber.toInt()].getImages() | ||||||
|  |  | ||||||
|  |             prefs = PreferenceManager.getDefaultSharedPreferences(activity) | ||||||
|  |             editor = prefs.edit() | ||||||
|  |             fontSize = prefs.getString("reader_font_size", "16")!!.toInt() | ||||||
|  |  | ||||||
|  |             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, prefs) | ||||||
|  |                             R.id.share_action -> requireActivity().shareLink(url, contentTitle) | ||||||
|  |                             R.id.open_action -> requireActivity().openItemUrl( | ||||||
|  |                                 allItems, | ||||||
|  |                                 pageNumber.toInt(), | ||||||
|  |                                 url, | ||||||
|  |                                 customTabsIntent, | ||||||
|  |                                 false, | ||||||
|  |                                 false, | ||||||
|  |                                 requireActivity() | ||||||
|  |                             ) | ||||||
|  |                             R.id.unread_action -> if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) { | ||||||
|  |                                 api.unmarkItem(allItems[pageNumber.toInt()].id).enqueue( | ||||||
|  |                                     object : Callback<SuccessResponse> { | ||||||
|  |                                         override fun onResponse( | ||||||
|  |                                             call: Call<SuccessResponse>, | ||||||
|  |                                             response: Response<SuccessResponse> | ||||||
|  |                                         ) { | ||||||
|  |                                         } | ||||||
|  |  | ||||||
|  |                                         override fun onFailure( | ||||||
|  |                                             call: Call<SuccessResponse>, | ||||||
|  |                                             t: Throwable | ||||||
|  |                                         ) { | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 ) | ||||||
|  |                             } else { | ||||||
|  |                                 thread { | ||||||
|  |                                     db.actionsDao().insertAllActions(ActionEntity(allItems[pageNumber.toInt()].id, false, true, false, false)) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             else -> Unit | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     override fun onItemLongClick(item: MenuItem?) { | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             binding.source.text = contentSource | ||||||
|  |             if (typeface != null) { | ||||||
|  |                 binding.source.typeface = typeface | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (contentText.isEmptyOrNullOrNullString()) { | ||||||
|  |                 getContentFromMercury(customTabsIntent, prefs) | ||||||
|  |             } 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) { | ||||||
|  |                         fab.hide() | ||||||
|  |                     } 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 | ||||||
|  |                 ) { dialog, which -> | ||||||
|  |                     val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext()) | ||||||
|  |                     val editor = sharedPref.edit() | ||||||
|  |                     editor.putBoolean("prefer_article_viewer", false) | ||||||
|  |                     editor.commit() | ||||||
|  |                     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, | ||||||
|  |         prefs: SharedPreferences | ||||||
|  |     ) { | ||||||
|  |         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 | ||||||
|  |         val (textColor, backgroundColor) = if (appColors.isDarkTheme) { | ||||||
|  |             if (context != null) { | ||||||
|  |                 binding.webcontent.setBackgroundColor( | ||||||
|  |                     ContextCompat.getColor( | ||||||
|  |                         requireContext(), | ||||||
|  |                         R.color.dark_webview | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |                 Pair(ContextCompat.getColor(requireContext(), R.color.dark_webview_text), ContextCompat.getColor(requireContext(), R.color.dark_webview)) | ||||||
|  |             } else { | ||||||
|  |                 Pair(null, null) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             if (context != null) { | ||||||
|  |                 binding.webcontent.setBackgroundColor( | ||||||
|  |                     ContextCompat.getColor( | ||||||
|  |                         requireContext(), | ||||||
|  |                         R.color.light_webview | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |                 Pair(ContextCompat.getColor(requireContext(), R.color.light_webview_text), ContextCompat.getColor(requireContext(), R.color.light_webview)) | ||||||
|  |             } else { | ||||||
|  |                 Pair(null, null) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val stringTextColor: String = if (textColor != null) { | ||||||
|  |             String.format("#%06X", 0xFFFFFF and textColor) | ||||||
|  |         } else { | ||||||
|  |             "#000000" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val stringBackgroundColor = if (backgroundColor != null) { | ||||||
|  |             String.format("#%06X", 0xFFFFFF and backgroundColor) | ||||||
|  |         } else { | ||||||
|  |             "#FFFFFF" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         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.toLowerCase().contains(".jpg") || url.toLowerCase().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.toLowerCase().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.toLowerCase().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)} | ||||||
|  |  | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | ||||||
|  |             binding.webcontent.settings.layoutAlgorithm = | ||||||
|  |                     WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING | ||||||
|  |         } else { | ||||||
|  |             binding.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         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 | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) { | ||||||
|  |         binding.progressBar.visibility = View.GONE | ||||||
|  |         requireActivity().openItemUrl( | ||||||
|  |             allItems, | ||||||
|  |             pageNumber.toInt(), | ||||||
|  |             url, | ||||||
|  |             customTabsIntent, | ||||||
|  |             true, | ||||||
|  |             false, | ||||||
|  |             requireActivity() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val ARG_POSITION = "position" | ||||||
|  |         private const val ARG_ITEMS = "items" | ||||||
|  |  | ||||||
|  |         fun newInstance( | ||||||
|  |             position: Int, | ||||||
|  |             allItems: ArrayList<Item> | ||||||
|  |         ): ArticleFragment { | ||||||
|  |             val fragment = ArticleFragment() | ||||||
|  |             val args = Bundle() | ||||||
|  |             args.putInt(ARG_POSITION, position) | ||||||
|  |             args.putParcelableArrayList(ARG_ITEMS, allItems) | ||||||
|  |             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 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,57 @@ | |||||||
|  | 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 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.persistence.dao | ||||||
|  |  | ||||||
|  | import androidx.room.Dao | ||||||
|  | import androidx.room.Delete | ||||||
|  | import androidx.room.Insert | ||||||
|  | import androidx.room.OnConflictStrategy | ||||||
|  | import androidx.room.Query | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity | ||||||
|  |  | ||||||
|  | @Dao | ||||||
|  | interface ActionsDao { | ||||||
|  |     @Query("SELECT * FROM actions order by id asc") | ||||||
|  |     fun actions(): List<ActionEntity> | ||||||
|  |  | ||||||
|  |     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||||
|  |     fun insertAllActions(vararg actions: ActionEntity) | ||||||
|  |  | ||||||
|  |     @Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1") | ||||||
|  |     fun deleteReadActionForArticle(article_id: String) | ||||||
|  |  | ||||||
|  |     @Delete | ||||||
|  |     fun delete(action: ActionEntity) | ||||||
|  | } | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | 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) | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.persistence.dao | ||||||
|  |  | ||||||
|  | import androidx.room.Dao | ||||||
|  | import androidx.room.Delete | ||||||
|  | import androidx.room.Insert | ||||||
|  | import androidx.room.OnConflictStrategy | ||||||
|  | import androidx.room.Query | ||||||
|  | import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity | ||||||
|  | import androidx.room.Update | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @Dao | ||||||
|  | interface ItemsDao { | ||||||
|  |     @Query("SELECT * FROM items order by id desc") | ||||||
|  |     fun items(): List<ItemEntity> | ||||||
|  |  | ||||||
|  |     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||||
|  |     fun insertAllItems(vararg items: ItemEntity) | ||||||
|  |  | ||||||
|  |     @Query("DELETE FROM items") | ||||||
|  |     fun deleteAllItems() | ||||||
|  |  | ||||||
|  |     @Delete | ||||||
|  |     fun delete(item: ItemEntity) | ||||||
|  |  | ||||||
|  |     @Update | ||||||
|  |     fun updateItem(item: ItemEntity) | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | 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 | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | 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 | ||||||
|  | } | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | 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 | ||||||
|  | ) | ||||||
| @@ -0,0 +1,32 @@ | |||||||
|  | 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 | ||||||
|  | ) | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | 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") | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,142 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.settings; | ||||||
|  |  | ||||||
|  | import android.content.res.Configuration; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.preference.PreferenceActivity; | ||||||
|  | import androidx.annotation.LayoutRes; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import com.google.android.material.appbar.AppBarLayout; | ||||||
|  | import androidx.appcompat.app.ActionBar; | ||||||
|  | import androidx.appcompat.app.AppCompatDelegate; | ||||||
|  | import androidx.appcompat.widget.Toolbar; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.MenuInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.LinearLayout; | ||||||
|  |  | ||||||
|  | import com.ftinc.scoop.Scoop; | ||||||
|  |  | ||||||
|  | import apps.amine.bou.readerforselfoss.R; | ||||||
|  | import apps.amine.bou.readerforselfoss.themes.AppColors; | ||||||
|  | import apps.amine.bou.readerforselfoss.themes.Toppings; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A {@link PreferenceActivity} which implements and proxies the necessary calls | ||||||
|  |  * to be used with AppCompat. | ||||||
|  |  */ | ||||||
|  | public abstract class AppCompatPreferenceActivity extends PreferenceActivity { | ||||||
|  |  | ||||||
|  |     private AppCompatDelegate mDelegate; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|  |         new AppColors(this); | ||||||
|  |  | ||||||
|  |         getDelegate().installViewFactory(); | ||||||
|  |         getDelegate().onCreate(savedInstanceState); | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onPostCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onPostCreate(savedInstanceState); | ||||||
|  |  | ||||||
|  |         LinearLayout root = (LinearLayout) findViewById(android.R.id.list).getParent().getParent().getParent(); | ||||||
|  |         AppBarLayout bar = (AppBarLayout) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false); | ||||||
|  |         Toolbar toolbar = bar.findViewById(R.id.toolbar); | ||||||
|  |  | ||||||
|  |         Scoop scoop = Scoop.getInstance(); | ||||||
|  |         scoop.bind(this, Toppings.PRIMARY.getValue(), toolbar); | ||||||
|  |         if  (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |             scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.getValue()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         setSupportActionBar(toolbar); | ||||||
|  |         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||||
|  |         getSupportActionBar().setDisplayShowHomeEnabled(true); | ||||||
|  |  | ||||||
|  |         root.addView(bar, 0); | ||||||
|  |  | ||||||
|  |         getDelegate().onPostCreate(savedInstanceState); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     ActionBar getSupportActionBar() { | ||||||
|  |         return getDelegate().getSupportActionBar(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setSupportActionBar(@Nullable Toolbar toolbar) { | ||||||
|  |         getDelegate().setSupportActionBar(toolbar); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public MenuInflater getMenuInflater() { | ||||||
|  |         return getDelegate().getMenuInflater(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void setContentView(@LayoutRes int layoutResID) { | ||||||
|  |         getDelegate().setContentView(layoutResID); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void setContentView(View view) { | ||||||
|  |         getDelegate().setContentView(view); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void setContentView(View view, ViewGroup.LayoutParams params) { | ||||||
|  |         getDelegate().setContentView(view, params); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void addContentView(View view, ViewGroup.LayoutParams params) { | ||||||
|  |         getDelegate().addContentView(view, params); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onPostResume() { | ||||||
|  |         super.onPostResume(); | ||||||
|  |         getDelegate().onPostResume(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onTitleChanged(CharSequence title, int color) { | ||||||
|  |         super.onTitleChanged(title, color); | ||||||
|  |         getDelegate().setTitle(title); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onConfigurationChanged(Configuration newConfig) { | ||||||
|  |         super.onConfigurationChanged(newConfig); | ||||||
|  |         getDelegate().onConfigurationChanged(newConfig); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onStop() { | ||||||
|  |         super.onStop(); | ||||||
|  |         getDelegate().onStop(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onDestroy() { | ||||||
|  |         super.onDestroy(); | ||||||
|  |         getDelegate().onDestroy(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void invalidateOptionsMenu() { | ||||||
|  |         getDelegate().invalidateOptionsMenu(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private AppCompatDelegate getDelegate() { | ||||||
|  |         if (mDelegate == null) { | ||||||
|  |             mDelegate = AppCompatDelegate.create(this, null); | ||||||
|  |         } | ||||||
|  |         return mDelegate; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,345 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.settings; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import android.annotation.TargetApi; | ||||||
|  | import android.content.ClipData; | ||||||
|  | import android.content.ClipboardManager; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.content.res.Configuration; | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.preference.EditTextPreference; | ||||||
|  | import android.preference.Preference; | ||||||
|  | import android.preference.Preference.OnPreferenceChangeListener; | ||||||
|  | import android.preference.Preference.OnPreferenceClickListener; | ||||||
|  | import android.preference.PreferenceActivity; | ||||||
|  | import android.preference.PreferenceFragment; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | import android.preference.SwitchPreference; | ||||||
|  | import androidx.appcompat.app.ActionBar; | ||||||
|  | import android.text.Editable; | ||||||
|  | import android.text.InputFilter; | ||||||
|  | import android.text.Spanned; | ||||||
|  | import android.text.TextWatcher; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.view.Menu; | ||||||
|  | import android.view.MenuInflater; | ||||||
|  | import android.view.MenuItem; | ||||||
|  | import android.widget.Toast; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import apps.amine.bou.readerforselfoss.R; | ||||||
|  | import apps.amine.bou.readerforselfoss.themes.AppColors; | ||||||
|  | import apps.amine.bou.readerforselfoss.utils.Config; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A {@link PreferenceActivity} that presents a set of application settings. On | ||||||
|  |  * handset devices, settings are presented as a single list. On tablets, | ||||||
|  |  * settings are split by category, with category headers shown to the left of | ||||||
|  |  * the list of settings. | ||||||
|  |  * <p> | ||||||
|  |  * See <a href="http://developer.android.com/design/patterns/settings.html"> | ||||||
|  |  * Android Design: Settings</a> for design guidelines and the <a | ||||||
|  |  * href="http://developer.android.com/guide/topics/ui/settings.html">Settings | ||||||
|  |  * API Guide</a> for more information on developing a Settings UI. | ||||||
|  |  */ | ||||||
|  | public class SettingsActivity extends AppCompatPreferenceActivity { | ||||||
|  |     /** | ||||||
|  |      * A preference value change listener that updates the preference's summary | ||||||
|  |      * to reflect its new value. | ||||||
|  |      */ | ||||||
|  |     private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { | ||||||
|  |         @Override | ||||||
|  |         public boolean onPreferenceChange(Preference preference, Object value) { | ||||||
|  |             String stringValue = value.toString(); | ||||||
|  |             preference.setSummary(stringValue); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Helper method to determine if the device has an extra-large screen. For | ||||||
|  |      * example, 10" tablets are extra-large. | ||||||
|  |      */ | ||||||
|  |     private static boolean isXLargeTablet(Context context) { | ||||||
|  |         return (context.getResources().getConfiguration().screenLayout | ||||||
|  |                 & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Binds a preference's summary to its value. More specifically, when the | ||||||
|  |      * preference's value is changed, its summary (line of text below the | ||||||
|  |      * preference title) is updated to reflect the value. The summary is also | ||||||
|  |      * immediately updated upon calling this method. The exact display format is | ||||||
|  |      * dependent on the type of preference. | ||||||
|  |      * | ||||||
|  |      * @see #sBindPreferenceSummaryToValueListener | ||||||
|  |      */ | ||||||
|  |     private static void bindPreferenceSummaryToValue(Preference preference) { | ||||||
|  |         // Set the listener to watch for value changes. | ||||||
|  |         preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); | ||||||
|  |  | ||||||
|  |         // Trigger the listener immediately with the preference's | ||||||
|  |         // current value. | ||||||
|  |         sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, | ||||||
|  |                 PreferenceManager | ||||||
|  |                         .getDefaultSharedPreferences(preference.getContext()) | ||||||
|  |                         .getString(preference.getKey(), "")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|  |         new AppColors(this); | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         setupActionBar(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Set up the {@link android.app.ActionBar}, if the API is available. | ||||||
|  |      */ | ||||||
|  |     private void setupActionBar() { | ||||||
|  |         ActionBar actionBar = getSupportActionBar(); | ||||||
|  |         if (actionBar != null) { | ||||||
|  |             // Show the Up button in the action bar. | ||||||
|  |             actionBar.setDisplayHomeAsUpEnabled(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * {@inheritDoc} | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public boolean onIsMultiPane() { | ||||||
|  |         return isXLargeTablet(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * {@inheritDoc} | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     @TargetApi(Build.VERSION_CODES.HONEYCOMB) | ||||||
|  |     public void onBuildHeaders(List<Header> target) { | ||||||
|  |         loadHeadersFromResource(R.xml.pref_headers, target); | ||||||
|  |  | ||||||
|  |         AppColors appColors = new AppColors(this); | ||||||
|  |         if (appColors != null && appColors.isDarkTheme()) { | ||||||
|  |             for (Header header : target) { | ||||||
|  |                 tryLoadIconDark(header); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void tryLoadIconDark(Header header){ | ||||||
|  |         try{ | ||||||
|  |             if (header.fragmentArguments != null) { | ||||||
|  |                 String iconDark = header.fragmentArguments.getString("iconDark"); | ||||||
|  |                 int iconDarkId = getResources().getIdentifier(iconDark, "drawable", getPackageName()); | ||||||
|  |                 if (iconDarkId != 0) { | ||||||
|  |                     header.iconRes = iconDarkId; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Log.e("SettingsActivity", "Can not load dark icon", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This method stops fragment injection in malicious applications. | ||||||
|  |      * Make sure to deny any unknown fragments here. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     protected boolean isValidFragment(String fragmentName) { | ||||||
|  |         return PreferenceFragment.class.getName().equals(fragmentName) | ||||||
|  |                 || GeneralPreferenceFragment.class.getName().equals(fragmentName) | ||||||
|  |                 || ArticleViewerPreferenceFragment.class.getName().equals(fragmentName) | ||||||
|  |                 || OfflinePreferenceFragment.class.getName().equals(fragmentName) | ||||||
|  |                 || ExperimentalPreferenceFragment.class.getName().equals(fragmentName) | ||||||
|  |                 || LinksPreferenceFragment.class.getName().equals(fragmentName) | ||||||
|  |                 || ThemePreferenceFragment.class.getName().equals(fragmentName); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This fragment shows general preferences only. It is used when the | ||||||
|  |      * activity is showing a two-pane settings UI. | ||||||
|  |      */ | ||||||
|  |     @TargetApi(Build.VERSION_CODES.HONEYCOMB) | ||||||
|  |     public static class GeneralPreferenceFragment extends PreferenceFragment { | ||||||
|  |         @Override | ||||||
|  |         public void onCreate(Bundle savedInstanceState) { | ||||||
|  |             super.onCreate(savedInstanceState); | ||||||
|  |             addPreferencesFromResource(R.xml.pref_general); | ||||||
|  |             setHasOptionsMenu(true); | ||||||
|  |  | ||||||
|  |             EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number"); | ||||||
|  |             itemsNumber.getEditText().setFilters(new InputFilter[]{ | ||||||
|  |                     new InputFilter() { | ||||||
|  |  | ||||||
|  |                         @Override | ||||||
|  |                         public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { | ||||||
|  |                             try { | ||||||
|  |                                 int input = Integer.parseInt(dest.toString() + source.toString()); | ||||||
|  |                                 if (input <= 200 && input > 0) | ||||||
|  |                                     return null; | ||||||
|  |                             } catch (NumberFormatException nfe) { | ||||||
|  |                                 Toast.makeText(getActivity(), R.string.items_number_should_be_number, Toast.LENGTH_LONG).show(); | ||||||
|  |                             } | ||||||
|  |                             return ""; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @TargetApi(Build.VERSION_CODES.HONEYCOMB) | ||||||
|  |     public static class ArticleViewerPreferenceFragment extends PreferenceFragment { | ||||||
|  |         @Override | ||||||
|  |         public void onCreate(Bundle savedInstanceState) { | ||||||
|  |             super.onCreate(savedInstanceState); | ||||||
|  |             addPreferencesFromResource(R.xml.pref_viewer); | ||||||
|  |             setHasOptionsMenu(true); | ||||||
|  |  | ||||||
|  |             final EditTextPreference fontSize = (EditTextPreference) findPreference("reader_font_size"); | ||||||
|  |             fontSize.getEditText().addTextChangedListener(new TextWatcher() { | ||||||
|  |                 @Override | ||||||
|  |                 public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} | ||||||
|  |  | ||||||
|  |                 @Override | ||||||
|  |                 public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} | ||||||
|  |  | ||||||
|  |                 @Override | ||||||
|  |                 public void afterTextChanged(Editable editable) { | ||||||
|  |                     try { | ||||||
|  |                         fontSize.getEditText().setTextSize(Integer.parseInt(editable.toString())); | ||||||
|  |                     } catch (NumberFormatException e) {} | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |             fontSize.getEditText().setFilters(new InputFilter[]{ | ||||||
|  |                     new InputFilter() { | ||||||
|  |  | ||||||
|  |                         @Override | ||||||
|  |                         public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { | ||||||
|  |                             try { | ||||||
|  |                                 int input = Integer.parseInt(dest.toString() + source.toString()); | ||||||
|  |                                 if (input > 0) | ||||||
|  |                                     return null; | ||||||
|  |                             } catch (NumberFormatException nfe) {} | ||||||
|  |                             return ""; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This fragment shows general preferences only. It is used when the | ||||||
|  |      * activity is showing a two-pane settings UI. | ||||||
|  |      */ | ||||||
|  |     @TargetApi(Build.VERSION_CODES.HONEYCOMB) | ||||||
|  |     public static class LinksPreferenceFragment extends PreferenceFragment { | ||||||
|  |         public void openUrl(Uri uri) { | ||||||
|  |             Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri); | ||||||
|  |             startActivity(browserIntent); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public void onCreate(Bundle savedInstanceState) { | ||||||
|  |             super.onCreate(savedInstanceState); | ||||||
|  |             addPreferencesFromResource(R.xml.pref_links); | ||||||
|  |             setHasOptionsMenu(true); | ||||||
|  |  | ||||||
|  |             findPreference("trackerLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { | ||||||
|  |                 @Override | ||||||
|  |                 public boolean onPreferenceClick(Preference preference) { | ||||||
|  |                     openUrl(Uri.parse(Config.trackerUrl)); | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { | ||||||
|  |                 @Override | ||||||
|  |                 public boolean onPreferenceClick(Preference preference) { | ||||||
|  |                     openUrl(Uri.parse(Config.sourceUrl)); | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             findPreference("translation").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { | ||||||
|  |                 @Override | ||||||
|  |                 public boolean onPreferenceClick(Preference preference) { | ||||||
|  |                     openUrl(Uri.parse(Config.translationUrl)); | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @TargetApi(Build.VERSION_CODES.HONEYCOMB) | ||||||
|  |     public static class ThemePreferenceFragment extends PreferenceFragment { | ||||||
|  |         @Override | ||||||
|  |         public void onCreate(Bundle savedInstanceState) { | ||||||
|  |             super.onCreate(savedInstanceState); | ||||||
|  |             addPreferencesFromResource(R.xml.pref_theme); | ||||||
|  |             setHasOptionsMenu(true); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|  |             int id = item.getItemId(); | ||||||
|  |             if (id == R.id.clear) { | ||||||
|  |                 SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||||||
|  |                 SharedPreferences.Editor 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(); | ||||||
|  |                 getActivity().recreate(); | ||||||
|  |             } | ||||||
|  |             return super.onOptionsItemSelected(item); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||||
|  |             inflater.inflate(R.menu.settings_theme, menu); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @TargetApi(Build.VERSION_CODES.HONEYCOMB) | ||||||
|  |     public static class OfflinePreferenceFragment extends PreferenceFragment { | ||||||
|  |         @Override | ||||||
|  |         public void onCreate(Bundle savedInstanceState) { | ||||||
|  |             super.onCreate(savedInstanceState); | ||||||
|  |             addPreferencesFromResource(R.xml.pref_offline); | ||||||
|  |             setHasOptionsMenu(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @TargetApi(Build.VERSION_CODES.HONEYCOMB) | ||||||
|  |     public static class ExperimentalPreferenceFragment extends PreferenceFragment { | ||||||
|  |         @Override | ||||||
|  |         public void onCreate(Bundle savedInstanceState) { | ||||||
|  |             super.onCreate(savedInstanceState); | ||||||
|  |             addPreferencesFromResource(R.xml.pref_experimental); | ||||||
|  |             setHasOptionsMenu(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|  |         int id = item.getItemId(); | ||||||
|  |         if (id == android.R.id.home) { | ||||||
|  |             super.onBackPressed(); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         return super.onOptionsItemSelected(item); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,76 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.themes | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.Context | ||||||
|  | import android.preference.PreferenceManager | ||||||
|  | import androidx.annotation.ColorInt | ||||||
|  | import androidx.appcompat.view.ContextThemeWrapper | ||||||
|  | import android.util.TypedValue | ||||||
|  | import apps.amine.bou.readerforselfoss.R | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.ViewGroup | ||||||
|  |  | ||||||
|  | class AppColors(a: Activity) { | ||||||
|  |  | ||||||
|  |     @ColorInt val colorPrimary: Int | ||||||
|  |     @ColorInt val colorPrimaryDark: Int | ||||||
|  |     @ColorInt val colorAccent: Int | ||||||
|  |     @ColorInt val colorAccentDark: Int | ||||||
|  |     @ColorInt val cardBackgroundColor: 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) | ||||||
|  |             android.R.color.background_light | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         textColor = if (isDarkTheme) { | ||||||
|  |             R.color.md_white_1000 | ||||||
|  |         } else { | ||||||
|  |             R.color.md_grey_900 | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val wrapper = Context::class.java | ||||||
|  |         val method = wrapper!!.getMethod("getThemeResId") | ||||||
|  |         method.isAccessible = true | ||||||
|  |  | ||||||
|  |         val typedCardBackground = TypedValue() | ||||||
|  |         a.theme.resolveAttribute(R.attr.cardBackgroundColor, typedCardBackground, true) | ||||||
|  |  | ||||||
|  |         cardBackgroundColor = typedCardBackground.data | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.themes | ||||||
|  |  | ||||||
|  | enum class Toppings(val value: Int) { | ||||||
|  |     PRIMARY(1), | ||||||
|  |     PRIMARY_DARK(2), | ||||||
|  |     ACCENT(3), | ||||||
|  |     ACCENT_DARK(4) | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.transformers | ||||||
|  |  | ||||||
|  | import androidx.viewpager.widget.ViewPager | ||||||
|  | import android.view.View | ||||||
|  |  | ||||||
|  | class DepthPageTransformer : ViewPager.PageTransformer { | ||||||
|  |  | ||||||
|  |     override fun transformPage(view: View, position: Float) { | ||||||
|  |         val pageWidth = view.width | ||||||
|  |  | ||||||
|  |         when { | ||||||
|  |             position < -1 -> // [-Infinity,-1) | ||||||
|  |                 // This page is way off-screen to the left. | ||||||
|  |                 view.alpha = 0F | ||||||
|  |             position <= 0 -> { // [-1,0] | ||||||
|  |                 // Use the default slide transition when moving to the left page | ||||||
|  |                 view.alpha = 1F | ||||||
|  |                 view.translationX = 0F | ||||||
|  |                 view.scaleX = 1F | ||||||
|  |                 view.scaleY = 1F | ||||||
|  |             } | ||||||
|  |             position <= 1 -> { // (0,1] | ||||||
|  |                 // Fade the page out. | ||||||
|  |                 view.alpha = 1 - position | ||||||
|  |  | ||||||
|  |                 // Counteract the default slide transition | ||||||
|  |                 view.translationX = pageWidth * -position | ||||||
|  |  | ||||||
|  |                 // Scale the page down (between MIN_SCALE and 1) | ||||||
|  |                 val scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position)) | ||||||
|  |                 view.scaleX = scaleFactor | ||||||
|  |                 view.scaleY = scaleFactor | ||||||
|  |             } | ||||||
|  |             else -> // (1,+Infinity] | ||||||
|  |                 // This page is way off-screen to the right. | ||||||
|  |                 view.alpha = 0F | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val MIN_SCALE = 0.75f | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | 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 | ||||||
| @@ -0,0 +1,41 @@ | |||||||
|  | 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].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) | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1,62 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.utils | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.content.SharedPreferences | ||||||
|  | import apps.amine.bou.readerforselfoss.LoginActivity | ||||||
|  |  | ||||||
|  | class Config(c: Context) { | ||||||
|  |  | ||||||
|  |     val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE) | ||||||
|  |  | ||||||
|  |     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" | ||||||
|  |  | ||||||
|  |         fun logoutAndRedirect( | ||||||
|  |             c: Context, | ||||||
|  |             callingActivity: Activity, | ||||||
|  |             editor: SharedPreferences.Editor, | ||||||
|  |             baseUrlFail: Boolean = false | ||||||
|  |         ): Boolean { | ||||||
|  |             editor.remove("url") | ||||||
|  |             editor.remove("login") | ||||||
|  |             editor.remove("password") | ||||||
|  |             editor.apply() | ||||||
|  |             val intent = Intent(c, LoginActivity::class.java) | ||||||
|  |             if (baseUrlFail) { | ||||||
|  |                 intent.putExtra("baseUrlFail", baseUrlFail) | ||||||
|  |             } | ||||||
|  |             c.startActivity(intent) | ||||||
|  |             callingActivity.finish() | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | 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) | ||||||
|  |     } | ||||||
| @@ -0,0 +1,51 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.utils | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.text.format.DateUtils | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.Item | ||||||
|  | import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType | ||||||
|  | import apps.amine.bou.readerforselfoss.dateTimeFormatter | ||||||
|  | import java.text.ParseException | ||||||
|  | import java.text.SimpleDateFormat | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | fun String.toTextDrawableString(c: Context): String { | ||||||
|  |     val textDrawable = StringBuilder() | ||||||
|  |     for (s in this.split(" ".toRegex()).filter { !it.isEmpty() }.toTypedArray()) { | ||||||
|  |         try { | ||||||
|  |             textDrawable.append(s[0]) | ||||||
|  |         } catch (e: StringIndexOutOfBoundsException) { | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return textDrawable.toString() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Item.sourceAndDateText(): String { | ||||||
|  |     val formattedDate: String = try { | ||||||
|  |         " " + DateUtils.getRelativeTimeSpanString( | ||||||
|  |             SimpleDateFormat(dateTimeFormatter).parse(this.datetime).time, | ||||||
|  |             Date().time, | ||||||
|  |             DateUtils.MINUTE_IN_MILLIS, | ||||||
|  |             DateUtils.FORMAT_ABBREV_RELATIVE | ||||||
|  |         ) | ||||||
|  |     } catch (e: ParseException) { | ||||||
|  |         e.printStackTrace() | ||||||
|  |         "" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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())) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| @@ -0,0 +1,195 @@ | |||||||
|  | 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.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 | ||||||
|  |  | ||||||
|  | fun Context.buildCustomTabsIntent(): CustomTabsIntent { | ||||||
|  |  | ||||||
|  |     val actionIntent = Intent(Intent.ACTION_SEND) | ||||||
|  |     actionIntent.type = "text/plain" | ||||||
|  |     val createPendingShareIntent: PendingIntent = PendingIntent.getActivity( | ||||||
|  |         this, | ||||||
|  |         0, | ||||||
|  |         actionIntent, | ||||||
|  |         0 | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |         val intent = Intent(this, ReaderActivity::class.java) | ||||||
|  |         intent.putExtra("currentItem", currentItem) | ||||||
|  |         app.startActivity(intent) | ||||||
|  |     } else { | ||||||
|  |         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 { | ||||||
|  |             this.openItemUrlInternally( | ||||||
|  |                 allItems, | ||||||
|  |                 currentItem, | ||||||
|  |                 linkDecoded, | ||||||
|  |                 customTabsIntent, | ||||||
|  |                 articleViewer, | ||||||
|  |                 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 = | ||||||
|  |     HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches() | ||||||
|  |  | ||||||
|  | fun String.isBaseUrlValid(ctx: Context): Boolean { | ||||||
|  |     val baseUrl = HttpUrl.parse(this) | ||||||
|  |     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 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | 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() | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | 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 | ||||||
| @@ -0,0 +1,153 @@ | |||||||
|  | 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,130 @@ | |||||||
|  | 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}; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | 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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | 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(); | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | /* 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) | ||||||
|  | } | ||||||
| @@ -0,0 +1,69 @@ | |||||||
|  | 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) | ||||||
|  | } | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | 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) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,45 @@ | |||||||
|  | package apps.amine.bou.readerforselfoss.utils.network | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.graphics.Color | ||||||
|  | import android.net.ConnectivityManager | ||||||
|  | import android.net.NetworkInfo | ||||||
|  | import android.view.View | ||||||
|  | import android.widget.TextView | ||||||
|  | import apps.amine.bou.readerforselfoss.R | ||||||
|  | import com.google.android.material.snackbar.Snackbar | ||||||
|  |  | ||||||
|  | var snackBarShown = false | ||||||
|  | var view: View? = null | ||||||
|  | lateinit var s: Snackbar | ||||||
|  |  | ||||||
|  | fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean { | ||||||
|  |     val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | ||||||
|  |     val activeNetwork: NetworkInfo? = cm.activeNetworkInfo | ||||||
|  |     val networkIsAccessible = activeNetwork != null && activeNetwork.isConnectedOrConnecting | ||||||
|  |  | ||||||
|  |     if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) { | ||||||
|  |         view = v | ||||||
|  |         s = Snackbar | ||||||
|  |             .make( | ||||||
|  |                 v, | ||||||
|  |                 R.string.no_network_connectivity, | ||||||
|  |                 Snackbar.LENGTH_INDEFINITE | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         s.setAction(android.R.string.ok) { | ||||||
|  |             snackBarShown = false | ||||||
|  |             s.dismiss() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val view = s.view | ||||||
|  |         val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text) | ||||||
|  |         tv.setTextColor(Color.WHITE) | ||||||
|  |         s.show() | ||||||
|  |         snackBarShown = true | ||||||
|  |     } | ||||||
|  |     if (snackBarShown && networkIsAccessible && !overrideOffline) { | ||||||
|  |         s.dismiss() | ||||||
|  |     } | ||||||
|  |     return if(overrideOffline) overrideOffline else networkIsAccessible | ||||||
|  | } | ||||||
| @@ -0,0 +1,73 @@ | |||||||
|  | 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 | ||||||
|  |     ) | ||||||
							
								
								
									
										16
									
								
								app/src/main/res/anim/slide_in_right.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/res/anim/slide_in_right.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | <?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> | ||||||
							
								
								
									
										16
									
								
								app/src/main/res/anim/slide_out_left.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/res/anim/slide_out_left.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | <?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> | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										13
									
								
								app/src/main/res/drawable/background_splash.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/src/main/res/drawable/background_splash.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | <?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> | ||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/bg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable/bg.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 406 B | 
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_add_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_add_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_bug_report_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_bug_report_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_bug_report_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_bug_report_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_close_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_close_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_color_lens_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_color_lens_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_color_lens_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_color_lens_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_fiber_new_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_fiber_new_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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,4L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,6c0,-1.11 -0.89,-2 -2,-2zM8.5,15L7.3,15l-2.55,-3.5L4.75,15L3.5,15L3.5,9h1.25l2.5,3.5L7.25,9L8.5,9v6zM13.5,10.26L11,10.26v1.12h2.5v1.26L11,12.64v1.11h2.5L13.5,15h-4L9.5,9h4v1.26zM20.5,14c0,0.55 -0.45,1 -1,1h-4c-0.55,0 -1,-0.45 -1,-1L14.5,9h1.25v4.51h1.13L16.88,9.99h1.25v3.51h1.12L19.25,9h1.25v5z"/> | ||||||
|  | </vector> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_history_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_history_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_info_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_info_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_info_outline_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_info_outline_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_info_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_info_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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,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> | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_menu_refresh_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_menu_refresh_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_menu_search_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_menu_search_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <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="M7,11v2h10v-2L7,11zM12,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,8z"/> | ||||||
|  | </vector> | ||||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_search_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_search_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <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="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> | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user