Compare commits
	
		
			7 Commits
		
	
	
		
			r
			...
			7d6e24f1fd
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7d6e24f1fd | |||
| 357fda2090 | |||
| 7cdb057560 | |||
| 513e5abcd1 | |||
| 864bb2d4bc | |||
| 29d99fca17 | |||
| d7d4eb3974 | 
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										558
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										558
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,558 @@
 | 
			
		||||
**1.7.x**
 | 
			
		||||
 | 
			
		||||
- Hiding tags with 0 articles
 | 
			
		||||
 | 
			
		||||
- Fixed issue with basic auth and images loading
 | 
			
		||||
 | 
			
		||||
- Added the ability to justify or left align the reader text
 | 
			
		||||
 | 
			
		||||
- Fixed #251
 | 
			
		||||
 | 
			
		||||
- Added experimental issue to set a default timeout. Should work for #238.
 | 
			
		||||
 | 
			
		||||
- Closing #220.
 | 
			
		||||
 | 
			
		||||
- Start of #238. "Add a quick shortcut to open the app on offline mode ?"
 | 
			
		||||
 | 
			
		||||
- Closes #216. Issue with selfoss version 2.19.
 | 
			
		||||
 | 
			
		||||
- Closes #179. Sync of read/unread/star/unstar items on background task or on app reload with network available.
 | 
			
		||||
 | 
			
		||||
- Closes #33. Background sync with settings.
 | 
			
		||||
 | 
			
		||||
- Closing #1. Initial article caching.
 | 
			
		||||
 | 
			
		||||
- Closing #228 by removing the list action bar. Action buttons are exclusively on the card view from now on.
 | 
			
		||||
 | 
			
		||||
- Closing #38. Only doing api calls on network available.
 | 
			
		||||
 | 
			
		||||
- Closing #298 and #287. Issues with Listview rendering
 | 
			
		||||
 | 
			
		||||
- Closing #290. Fixing back button issue in Settings
 | 
			
		||||
 | 
			
		||||
- Closing #300. Fixing issues when displaying some special characters.
 | 
			
		||||
 | 
			
		||||
- Closing #310. Some feeds don't have icons nor thumbnails.
 | 
			
		||||
 | 
			
		||||
- Closing #178. Expending images on tap.
 | 
			
		||||
 | 
			
		||||
- Closing #323. Old issue with textview not having the right color.
 | 
			
		||||
 | 
			
		||||
- Closing #324. Svg images loading crashes the app.
 | 
			
		||||
 | 
			
		||||
- Closing #322. App crashed because of svg images.
 | 
			
		||||
 | 
			
		||||
- Closing #236. New sources can be added in Selfoss 2.19.
 | 
			
		||||
 | 
			
		||||
- Dropped support for android 4, the last version supporting it is v1721030811
 | 
			
		||||
 | 
			
		||||
**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>.
 | 
			
		||||
							
								
								
									
										48
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								README.md
									
									
									
									
									
								
							@@ -1,3 +1,47 @@
 | 
			
		||||
# 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)
 | 
			
		||||
 | 
			
		||||
## Contributors (Alphabetical order) ❤️
 | 
			
		||||
 | 
			
		||||
- [@aancel](https://github.com/aancel)
 | 
			
		||||
- [@Binnette](https://github.com/Binnette)
 | 
			
		||||
- [@davidoskky](https://github.com/davidoskky)
 | 
			
		||||
- [@hectorgabucio](https://github.com/hectorgabucio)
 | 
			
		||||
- [@licaon-kter](https://github.com/licaon-kter)
 | 
			
		||||
- [@sergey-babkin](https://github.com/sergey-babkin)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
/build
 | 
			
		||||
							
								
								
									
										171
									
								
								app/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								app/build.gradle
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,171 @@
 | 
			
		||||
buildscript {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def gitVersion() {
 | 
			
		||||
    def process
 | 
			
		||||
    def maybeTagOfCurrentCommit = 'git describe --contains HEAD'.execute()
 | 
			
		||||
    if (maybeTagOfCurrentCommit.text.isEmpty()) {
 | 
			
		||||
        println "No tag on current commit. Will take the latest one."
 | 
			
		||||
        process = "git for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1".execute()
 | 
			
		||||
    } else {
 | 
			
		||||
        println "Tag found on current commit"
 | 
			
		||||
        process = 'git describe --contains HEAD'.execute()
 | 
			
		||||
    }
 | 
			
		||||
    return process.text.replaceAll("'", "").substring(1).replaceAll("\\.", "").trim()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def versionCodeFromGit() {
 | 
			
		||||
    println "version code " + gitVersion()
 | 
			
		||||
    return gitVersion().toInteger()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def versionNameFromGit() {
 | 
			
		||||
    println "version name " + gitVersion()
 | 
			
		||||
    return gitVersion()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply plugin: 'com.android.application'
 | 
			
		||||
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
 | 
			
		||||
apply plugin: 'kotlin-kapt'
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileOptions {
 | 
			
		||||
        // Flag to enable support for the new language APIs
 | 
			
		||||
        coreLibraryDesugaringEnabled true
 | 
			
		||||
 | 
			
		||||
        sourceCompatibility JavaVersion.VERSION_1_8
 | 
			
		||||
        targetCompatibility JavaVersion.VERSION_1_8
 | 
			
		||||
    }
 | 
			
		||||
    compileSdkVersion 31
 | 
			
		||||
    buildToolsVersion '31.0.0'
 | 
			
		||||
    buildFeatures {
 | 
			
		||||
        viewBinding true
 | 
			
		||||
    }
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
        applicationId "apps.amine.bou.readerforselfoss"
 | 
			
		||||
        minSdkVersion 21
 | 
			
		||||
        targetSdkVersion 31
 | 
			
		||||
        versionCode versionCodeFromGit()
 | 
			
		||||
        versionName versionNameFromGit()
 | 
			
		||||
 | 
			
		||||
        // Enabling multidex support.
 | 
			
		||||
        multiDexEnabled true
 | 
			
		||||
        lintOptions {
 | 
			
		||||
            abortOnError true
 | 
			
		||||
            disable 'InvalidPackage'
 | 
			
		||||
        }
 | 
			
		||||
        vectorDrawables.useSupportLibrary = true
 | 
			
		||||
 | 
			
		||||
        // tests
 | 
			
		||||
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 | 
			
		||||
 | 
			
		||||
        javaCompileOptions {
 | 
			
		||||
            annotationProcessorOptions {
 | 
			
		||||
                arguments = ["room.schemaLocation":
 | 
			
		||||
                                     "$projectDir/schemas".toString()]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    buildTypes {
 | 
			
		||||
        release {
 | 
			
		||||
            minifyEnabled true
 | 
			
		||||
            shrinkResources true
 | 
			
		||||
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
 | 
			
		||||
                    'proguard-rules.pro'
 | 
			
		||||
        }
 | 
			
		||||
        debug {
 | 
			
		||||
            buildConfigField "String", "LOGIN_URL", appLoginUrl
 | 
			
		||||
            buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
 | 
			
		||||
            buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    flavorDimensions "build"
 | 
			
		||||
    productFlavors {
 | 
			
		||||
        githubConfig {
 | 
			
		||||
            versionNameSuffix '-github'
 | 
			
		||||
            dimension "build"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    kotlinOptions {
 | 
			
		||||
        jvmTarget = '1.8'
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation 'androidx.preference:preference-ktx:1.1.1'
 | 
			
		||||
 | 
			
		||||
    // Testing
 | 
			
		||||
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02'
 | 
			
		||||
    androidTestImplementation 'androidx.test:runner:1.3.1-alpha02'
 | 
			
		||||
    // Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
 | 
			
		||||
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0-alpha02'
 | 
			
		||||
    // Espresso-intents for validation and stubbing of Intents
 | 
			
		||||
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0-alpha02'
 | 
			
		||||
    implementation fileTree(include: ['*.jar'], dir: 'libs')
 | 
			
		||||
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 | 
			
		||||
    // Android Support
 | 
			
		||||
    implementation "androidx.appcompat:appcompat:1.4.0-beta01"
 | 
			
		||||
    implementation 'com.google.android.material:material:1.5.0-alpha04'
 | 
			
		||||
    implementation 'androidx.recyclerview:recyclerview:1.3.0-alpha01'
 | 
			
		||||
    implementation "androidx.legacy:legacy-support-v4:$android_version"
 | 
			
		||||
    implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02'
 | 
			
		||||
    implementation "androidx.browser:browser:1.3.0"
 | 
			
		||||
    implementation "androidx.cardview:cardview:$android_version"
 | 
			
		||||
    implementation "androidx.annotation:annotation:1.2.0"
 | 
			
		||||
    implementation 'androidx.work:work-runtime-ktx:2.7.0'
 | 
			
		||||
    implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
 | 
			
		||||
    implementation 'org.jsoup:jsoup:1.14.3'
 | 
			
		||||
 | 
			
		||||
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
 | 
			
		||||
 | 
			
		||||
    //multidex
 | 
			
		||||
    implementation 'androidx.multidex:multidex:2.0.1'
 | 
			
		||||
 | 
			
		||||
    // About
 | 
			
		||||
    implementation 'com.mikepenz:aboutlibraries-core:8.9.4'
 | 
			
		||||
    implementation 'com.mikepenz:aboutlibraries:8.9.4'
 | 
			
		||||
    implementation "com.mikepenz:aboutlibraries-definitions:8.9.4"
 | 
			
		||||
 | 
			
		||||
    // Async
 | 
			
		||||
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
 | 
			
		||||
 | 
			
		||||
    // Retrofit + http logging + okhttp
 | 
			
		||||
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
 | 
			
		||||
    implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
 | 
			
		||||
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
 | 
			
		||||
    implementation 'com.burgstaller:okhttp-digest:2.5'
 | 
			
		||||
 | 
			
		||||
    // Material-ish things
 | 
			
		||||
    implementation 'com.ashokvarma.android:bottom-navigation-bar:2.2.0'
 | 
			
		||||
    implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
 | 
			
		||||
 | 
			
		||||
    // glide
 | 
			
		||||
    kapt 'com.github.bumptech.glide:compiler:4.11.0'
 | 
			
		||||
    implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
 | 
			
		||||
 | 
			
		||||
    // Drawer
 | 
			
		||||
    implementation 'com.mikepenz:materialdrawer:8.4.4'
 | 
			
		||||
 | 
			
		||||
    // Themes
 | 
			
		||||
    implementation 'com.52inc:scoops:1.0.0'
 | 
			
		||||
    implementation 'com.jaredrummler:colorpicker:1.1.0'
 | 
			
		||||
    implementation 'com.github.rubensousa:floatingtoolbar:1.5.1'
 | 
			
		||||
 | 
			
		||||
    // Pager
 | 
			
		||||
    implementation 'me.relex:circleindicator:2.1.6'
 | 
			
		||||
 | 
			
		||||
    //PhotoView
 | 
			
		||||
    implementation 'com.github.chrisbanes:PhotoView:2.3.0'
 | 
			
		||||
 | 
			
		||||
    implementation 'androidx.core:core-ktx:1.7.0-rc01'
 | 
			
		||||
 | 
			
		||||
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-rc01"
 | 
			
		||||
    implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0-rc01"
 | 
			
		||||
 | 
			
		||||
    implementation "androidx.room:room-ktx:2.4.0-beta01"
 | 
			
		||||
    kapt "androidx.room:room-compiler:2.4.0-beta01"
 | 
			
		||||
 | 
			
		||||
    implementation "android.arch.work:work-runtime-ktx:$work_version"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										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,31 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.parseDate
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
 | 
			
		||||
class DateUtilsTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun parseDateV4() {
 | 
			
		||||
 | 
			
		||||
        Config.apiVersion = 4
 | 
			
		||||
        val dateString = "2013-04-07T13:43:00+01:00"
 | 
			
		||||
 | 
			
		||||
        val milliseconds = parseDate(dateString).toEpochMilli()
 | 
			
		||||
        val correctMilliseconds : Long = 1365338580000
 | 
			
		||||
 | 
			
		||||
        assert(milliseconds == correctMilliseconds)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun parseDateV1() {
 | 
			
		||||
        Config.apiVersion = 0
 | 
			
		||||
        val dateString = "2013-04-07 13:43:00"
 | 
			
		||||
 | 
			
		||||
        val milliseconds = parseDate(dateString).toEpochMilli()
 | 
			
		||||
        val correctMilliseconds = 1365342180000
 | 
			
		||||
 | 
			
		||||
        assert(milliseconds == correctMilliseconds)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										87
									
								
								app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    package="apps.amine.bou.readerforselfoss">
 | 
			
		||||
 | 
			
		||||
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
 | 
			
		||||
    <application
 | 
			
		||||
        android:name=".MyApp"
 | 
			
		||||
        android:allowBackup="true"
 | 
			
		||||
        android:icon="@mipmap/ic_launcher"
 | 
			
		||||
        android:label="@string/app_name"
 | 
			
		||||
        android:supportsRtl="true"
 | 
			
		||||
        android:networkSecurityConfig="@xml/network_security_config"
 | 
			
		||||
        android:theme="@style/NoBar">
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".MainActivity"
 | 
			
		||||
            android:theme="@style/SplashTheme"
 | 
			
		||||
            android:exported="true">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.MAIN" />
 | 
			
		||||
                <category android:name="android.intent.category.LAUNCHER" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.app.shortcuts"
 | 
			
		||||
                android:resource="@xml/shortcuts" />
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".LoginActivity"
 | 
			
		||||
            android:label="@string/title_activity_login">
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity android:name=".HomeActivity">
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".settings.SettingsActivity"
 | 
			
		||||
            android:label="@string/title_activity_settings"
 | 
			
		||||
            android:parentActivityName=".HomeActivity">
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.support.PARENT_ACTIVITY"
 | 
			
		||||
                android:value=".HomeActivity" />
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".SourcesActivity"
 | 
			
		||||
            android:parentActivityName=".HomeActivity">
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.support.PARENT_ACTIVITY"
 | 
			
		||||
                android:value=".HomeActivity" />
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".AddSourceActivity"
 | 
			
		||||
            android:parentActivityName=".SourcesActivity"
 | 
			
		||||
            android:exported="true">
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.support.PARENT_ACTIVITY"
 | 
			
		||||
                android:value=".SourcesActivity" />
 | 
			
		||||
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.SEND" />
 | 
			
		||||
                <category android:name="android.intent.category.DEFAULT" />
 | 
			
		||||
                <data android:mimeType="text/plain" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".ReaderActivity">
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".ImageActivity">
 | 
			
		||||
        </activity>
 | 
			
		||||
 | 
			
		||||
        <meta-data
 | 
			
		||||
            android:name="apps.amine.bou.readerforselfoss.utils.glide.SelfSignedGlideModule"
 | 
			
		||||
            android:value="GlideModule" />
 | 
			
		||||
 | 
			
		||||
        <meta-data android:name="android.webkit.WebView.MetricsOptOut"
 | 
			
		||||
            android:value="true" />
 | 
			
		||||
 | 
			
		||||
        <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
 | 
			
		||||
            android:value="true" />
 | 
			
		||||
 | 
			
		||||
        <meta-data android:name="android.max_aspect" android:value="2.1" />
 | 
			
		||||
        <meta-data
 | 
			
		||||
            android:name="preloaded_fonts"
 | 
			
		||||
            android:resource="@array/preloaded_fonts" />
 | 
			
		||||
    </application>
 | 
			
		||||
 | 
			
		||||
</manifest>
 | 
			
		||||
							
								
								
									
										
											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,268 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import androidx.constraintlayout.widget.ConstraintLayout
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.AdapterView
 | 
			
		||||
import android.widget.ArrayAdapter
 | 
			
		||||
import android.widget.EditText
 | 
			
		||||
import android.widget.ProgressBar
 | 
			
		||||
import android.widget.Spinner
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Spout
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.Toppings
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import android.graphics.PorterDuff
 | 
			
		||||
import apps.amine.bou.readerforselfoss.databinding.ActivityAddSourceBinding
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddSourceActivity : AppCompatActivity() {
 | 
			
		||||
 | 
			
		||||
    private var mSpoutsValue: String? = null
 | 
			
		||||
    private lateinit var api: SelfossApi
 | 
			
		||||
 | 
			
		||||
    private lateinit var appColors: AppColors
 | 
			
		||||
    private lateinit var binding: ActivityAddSourceBinding
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        appColors = AppColors(this@AddSourceActivity)
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        binding = ActivityAddSourceBinding.inflate(layoutInflater)
 | 
			
		||||
        val view = binding.root
 | 
			
		||||
 | 
			
		||||
        setContentView(view)
 | 
			
		||||
 | 
			
		||||
        val scoop = Scoop.getInstance()
 | 
			
		||||
        scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
 | 
			
		||||
        scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
 | 
			
		||||
 | 
			
		||||
        val drawable = binding.nameInput.background
 | 
			
		||||
        drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // TODO: clean
 | 
			
		||||
        binding.nameInput.background = drawable
 | 
			
		||||
 | 
			
		||||
        val drawable1 = binding.sourceUri.background
 | 
			
		||||
        drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
 | 
			
		||||
 | 
			
		||||
        binding.sourceUri.background = drawable1
 | 
			
		||||
 | 
			
		||||
        val drawable2 = binding.tags.background
 | 
			
		||||
        drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
 | 
			
		||||
 | 
			
		||||
        binding.tags.background = drawable2
 | 
			
		||||
 | 
			
		||||
        setSupportActionBar(binding.toolbar)
 | 
			
		||||
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
 | 
			
		||||
        supportActionBar?.setDisplayShowHomeEnabled(true)
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val prefs = PreferenceManager.getDefaultSharedPreferences(this)
 | 
			
		||||
            val settings =
 | 
			
		||||
                getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
            api = SelfossApi(
 | 
			
		||||
                this,
 | 
			
		||||
                this@AddSourceActivity,
 | 
			
		||||
                settings.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
                prefs.getString("api_timeout", "-1")!!.toLong()
 | 
			
		||||
            )
 | 
			
		||||
        } catch (e: IllegalArgumentException) {
 | 
			
		||||
            mustLoginToAddSource()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
 | 
			
		||||
 | 
			
		||||
        binding.saveBtn.setTextColor(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
        binding.saveBtn.setOnClickListener {
 | 
			
		||||
            handleSaveSource(binding.tags, binding.nameInput.text.toString(), binding.sourceUri.text.toString(), api)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        val config = Config(this)
 | 
			
		||||
 | 
			
		||||
        if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
 | 
			
		||||
            mustLoginToAddSource()
 | 
			
		||||
        } else {
 | 
			
		||||
            handleSpoutsSpinner(binding.spoutsSpinner, api, binding.progress, binding.formContainer)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleSpoutsSpinner(
 | 
			
		||||
        spoutsSpinner: Spinner,
 | 
			
		||||
        api: SelfossApi?,
 | 
			
		||||
        mProgress: ProgressBar,
 | 
			
		||||
        formContainer: ConstraintLayout
 | 
			
		||||
    ) {
 | 
			
		||||
        val spoutsKV = HashMap<String, String>()
 | 
			
		||||
        spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
 | 
			
		||||
            override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
 | 
			
		||||
                if (view != null) {
 | 
			
		||||
                    val spoutName = (view as TextView).text.toString()
 | 
			
		||||
                    mSpoutsValue = spoutsKV[spoutName]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onNothingSelected(adapterView: AdapterView<*>) {
 | 
			
		||||
                mSpoutsValue = null
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var items: Map<String, Spout>
 | 
			
		||||
        api!!.spouts().enqueue(object : Callback<Map<String, Spout>> {
 | 
			
		||||
            override fun onResponse(
 | 
			
		||||
                call: Call<Map<String, Spout>>,
 | 
			
		||||
                response: Response<Map<String, Spout>>
 | 
			
		||||
            ) {
 | 
			
		||||
                if (response.body() != null) {
 | 
			
		||||
                    items = response.body()!!
 | 
			
		||||
 | 
			
		||||
                    val itemsStrings = items.map { it.value.name }
 | 
			
		||||
                    for ((key, value) in items) {
 | 
			
		||||
                        spoutsKV[value.name] = key
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    mProgress.visibility = View.GONE
 | 
			
		||||
                    formContainer.visibility = View.VISIBLE
 | 
			
		||||
 | 
			
		||||
                    val spinnerArrayAdapter =
 | 
			
		||||
                        ArrayAdapter(
 | 
			
		||||
                            this@AddSourceActivity,
 | 
			
		||||
                            android.R.layout.simple_spinner_item,
 | 
			
		||||
                            itemsStrings
 | 
			
		||||
                        )
 | 
			
		||||
                    spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
 | 
			
		||||
                    spoutsSpinner.adapter = spinnerArrayAdapter
 | 
			
		||||
                } else {
 | 
			
		||||
                    handleProblemWithSpouts()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
 | 
			
		||||
                handleProblemWithSpouts()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private fun handleProblemWithSpouts() {
 | 
			
		||||
                Toast.makeText(
 | 
			
		||||
                    this@AddSourceActivity,
 | 
			
		||||
                    R.string.cant_get_spouts,
 | 
			
		||||
                    Toast.LENGTH_SHORT
 | 
			
		||||
                ).show()
 | 
			
		||||
                mProgress.visibility = View.GONE
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun maybeGetDetailsFromIntentSharing(
 | 
			
		||||
        intent: Intent,
 | 
			
		||||
        sourceUri: EditText,
 | 
			
		||||
        nameInput: EditText
 | 
			
		||||
    ) {
 | 
			
		||||
        if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
 | 
			
		||||
            sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
 | 
			
		||||
            nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun mustLoginToAddSource() {
 | 
			
		||||
        Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
 | 
			
		||||
        val i = Intent(this, LoginActivity::class.java)
 | 
			
		||||
        startActivity(i)
 | 
			
		||||
        finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) {
 | 
			
		||||
 | 
			
		||||
        val sourceDetailsUnavailable =
 | 
			
		||||
            title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
 | 
			
		||||
 | 
			
		||||
        when {
 | 
			
		||||
            sourceDetailsUnavailable -> {
 | 
			
		||||
                Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
 | 
			
		||||
            }
 | 
			
		||||
            PreferenceManager.getDefaultSharedPreferences(this).getInt("apiVersionMajor", 0) > 1 -> {
 | 
			
		||||
                val tagList = tags.text.toString().split(",").map { it.trim() }
 | 
			
		||||
                api.createSourceApi2(
 | 
			
		||||
                    title,
 | 
			
		||||
                    url,
 | 
			
		||||
                    mSpoutsValue!!,
 | 
			
		||||
                    tagList,
 | 
			
		||||
                    ""
 | 
			
		||||
                ).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                    override fun onResponse(
 | 
			
		||||
                        call: Call<SuccessResponse>,
 | 
			
		||||
                        response: Response<SuccessResponse>
 | 
			
		||||
                    ) {
 | 
			
		||||
                        if (response.body() != null && response.body()!!.isSuccess) {
 | 
			
		||||
                            finish()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Toast.makeText(
 | 
			
		||||
                                this@AddSourceActivity,
 | 
			
		||||
                                R.string.cant_create_source,
 | 
			
		||||
                                Toast.LENGTH_SHORT
 | 
			
		||||
                            ).show()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                        Toast.makeText(
 | 
			
		||||
                            this@AddSourceActivity,
 | 
			
		||||
                            R.string.cant_create_source,
 | 
			
		||||
                            Toast.LENGTH_SHORT
 | 
			
		||||
                        ).show()
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
            else -> {
 | 
			
		||||
                api.createSource(
 | 
			
		||||
                    title,
 | 
			
		||||
                    url,
 | 
			
		||||
                    mSpoutsValue!!,
 | 
			
		||||
                    tags.text.toString(),
 | 
			
		||||
                    ""
 | 
			
		||||
                ).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                    override fun onResponse(
 | 
			
		||||
                        call: Call<SuccessResponse>,
 | 
			
		||||
                        response: Response<SuccessResponse>
 | 
			
		||||
                    ) {
 | 
			
		||||
                        if (response.body() != null && response.body()!!.isSuccess) {
 | 
			
		||||
                            finish()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Toast.makeText(
 | 
			
		||||
                                this@AddSourceActivity,
 | 
			
		||||
                                R.string.cant_create_source,
 | 
			
		||||
                                Toast.LENGTH_SHORT
 | 
			
		||||
                            ).show()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                        Toast.makeText(
 | 
			
		||||
                            this@AddSourceActivity,
 | 
			
		||||
                            R.string.cant_create_source,
 | 
			
		||||
                            Toast.LENGTH_SHORT
 | 
			
		||||
                        ).show()
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1309
									
								
								app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1309
									
								
								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,294 @@
 | 
			
		||||
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 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.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.loginView.visibility = visi
 | 
			
		||||
            binding.passwordView.visibility = visi
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.withHttpLogin.setOnCheckedChangeListener { _, b ->
 | 
			
		||||
            isWithHTTPLogin = !isWithHTTPLogin
 | 
			
		||||
            val visi: Int = if (b) View.VISIBLE else View.GONE
 | 
			
		||||
 | 
			
		||||
            binding.httpLoginView.visibility = visi
 | 
			
		||||
            binding.httpPasswordView.visibility = visi
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleBaseUrlFail() {
 | 
			
		||||
        if (intent.getBooleanExtra("baseUrlFail", false)) {
 | 
			
		||||
            val alertDialog = AlertDialog.Builder(this).create()
 | 
			
		||||
            alertDialog.setTitle(getString(R.string.warning_wrong_url))
 | 
			
		||||
            alertDialog.setMessage(getString(R.string.base_url_error))
 | 
			
		||||
            alertDialog.setButton(
 | 
			
		||||
                AlertDialog.BUTTON_NEUTRAL,
 | 
			
		||||
                "OK"
 | 
			
		||||
            ) { dialog, _ -> dialog.dismiss() }
 | 
			
		||||
            alertDialog.show()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun goToMain() {
 | 
			
		||||
        val intent = Intent(this, HomeActivity::class.java)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
        finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun attemptLogin() {
 | 
			
		||||
 | 
			
		||||
        // Reset errors.
 | 
			
		||||
        binding.urlView.error = null
 | 
			
		||||
        binding.loginView.error = null
 | 
			
		||||
        binding.httpLoginView.error = null
 | 
			
		||||
        binding.passwordView.error = null
 | 
			
		||||
        binding.httpPasswordView.error = null
 | 
			
		||||
 | 
			
		||||
        // Store values at the time of the login attempt.
 | 
			
		||||
        val url = binding.urlView.text.toString()
 | 
			
		||||
        val login = binding.loginView.text.toString()
 | 
			
		||||
        val httpLogin = binding.httpLoginView.text.toString()
 | 
			
		||||
        val password = binding.passwordView.text.toString()
 | 
			
		||||
        val httpPassword = binding.httpPasswordView.text.toString()
 | 
			
		||||
 | 
			
		||||
        var cancel = false
 | 
			
		||||
        var focusView: View? = null
 | 
			
		||||
 | 
			
		||||
        if (!url.isBaseUrlValid(this@LoginActivity)) {
 | 
			
		||||
            binding.urlView.error = getString(R.string.login_url_problem)
 | 
			
		||||
            focusView = binding.urlView
 | 
			
		||||
            cancel = true
 | 
			
		||||
            inValidCount++
 | 
			
		||||
            if (inValidCount == 3) {
 | 
			
		||||
                val alertDialog = AlertDialog.Builder(this).create()
 | 
			
		||||
                alertDialog.setTitle(getString(R.string.warning_wrong_url))
 | 
			
		||||
                alertDialog.setMessage(getString(R.string.text_wrong_url))
 | 
			
		||||
                alertDialog.setButton(
 | 
			
		||||
                    AlertDialog.BUTTON_NEUTRAL,
 | 
			
		||||
                    "OK"
 | 
			
		||||
                ) { dialog, _ -> dialog.dismiss() }
 | 
			
		||||
                alertDialog.show()
 | 
			
		||||
                inValidCount = 0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isWithLogin) {
 | 
			
		||||
            if (TextUtils.isEmpty(password)) {
 | 
			
		||||
                binding.passwordView.error = getString(R.string.error_invalid_password)
 | 
			
		||||
                focusView = binding.passwordView
 | 
			
		||||
                cancel = true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (TextUtils.isEmpty(login)) {
 | 
			
		||||
                binding.loginView.error = getString(R.string.error_field_required)
 | 
			
		||||
                focusView = binding.loginView
 | 
			
		||||
                cancel = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isWithHTTPLogin) {
 | 
			
		||||
            if (TextUtils.isEmpty(httpPassword)) {
 | 
			
		||||
                binding.httpPasswordView.error = getString(R.string.error_invalid_password)
 | 
			
		||||
                focusView = binding.httpPasswordView
 | 
			
		||||
                cancel = true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (TextUtils.isEmpty(httpLogin)) {
 | 
			
		||||
                binding.httpLoginView.error = getString(R.string.error_field_required)
 | 
			
		||||
                focusView = binding.httpLoginView
 | 
			
		||||
                cancel = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cancel) {
 | 
			
		||||
            focusView?.requestFocus()
 | 
			
		||||
        } else {
 | 
			
		||||
            showProgress(true)
 | 
			
		||||
 | 
			
		||||
            editor.putString("url", url)
 | 
			
		||||
            editor.putString("login", login)
 | 
			
		||||
            editor.putString("httpUserName", httpLogin)
 | 
			
		||||
            editor.putString("password", password)
 | 
			
		||||
            editor.putString("httpPassword", httpPassword)
 | 
			
		||||
            editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert)
 | 
			
		||||
            editor.apply()
 | 
			
		||||
 | 
			
		||||
            val api = SelfossApi(
 | 
			
		||||
                this,
 | 
			
		||||
                this@LoginActivity,
 | 
			
		||||
                isWithSelfSignedCert,
 | 
			
		||||
                -1L
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) {
 | 
			
		||||
                api.login().enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                    private fun preferenceError(t: Throwable) {
 | 
			
		||||
                        editor.remove("url")
 | 
			
		||||
                        editor.remove("login")
 | 
			
		||||
                        editor.remove("httpUserName")
 | 
			
		||||
                        editor.remove("password")
 | 
			
		||||
                        editor.remove("httpPassword")
 | 
			
		||||
                        editor.apply()
 | 
			
		||||
                        binding.urlView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        binding.loginView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        binding.passwordView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        binding.httpLoginView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        binding.httpPasswordView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        showProgress(false)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onResponse(
 | 
			
		||||
                        call: Call<SuccessResponse>,
 | 
			
		||||
                        response: Response<SuccessResponse>
 | 
			
		||||
                    ) {
 | 
			
		||||
                        if (response.body() != null && response.body()!!.isSuccess) {
 | 
			
		||||
                            goToMain()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            preferenceError(Exception("No response body..."))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                        preferenceError(t)
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            } else {
 | 
			
		||||
                showProgress(false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun showProgress(show: Boolean) {
 | 
			
		||||
        val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
 | 
			
		||||
 | 
			
		||||
        binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
 | 
			
		||||
        binding.loginForm
 | 
			
		||||
            .animate()
 | 
			
		||||
            .setDuration(shortAnimTime.toLong())
 | 
			
		||||
            .alpha(
 | 
			
		||||
                if (show) 0F else 1F
 | 
			
		||||
            ).setListener(object : AnimatorListenerAdapter() {
 | 
			
		||||
            override fun onAnimationEnd(animation: Animator) {
 | 
			
		||||
                binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
 | 
			
		||||
        binding.loginProgress
 | 
			
		||||
            .animate()
 | 
			
		||||
            .setDuration(shortAnimTime.toLong())
 | 
			
		||||
            .alpha(
 | 
			
		||||
                if (show) 1F else 0F
 | 
			
		||||
            ).setListener(object : AnimatorListenerAdapter() {
 | 
			
		||||
            override fun onAnimationEnd(animation: Animator) {
 | 
			
		||||
                binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
 | 
			
		||||
        menuInflater.inflate(R.menu.login_menu, menu)
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        return when (item.itemId) {
 | 
			
		||||
            R.id.about -> {
 | 
			
		||||
                LibsBuilder()
 | 
			
		||||
                    .withAboutIconShown(true)
 | 
			
		||||
                    .withAboutVersionShown(true)
 | 
			
		||||
                    .start(this)
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
            else -> super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.databinding.ActivityMainBinding
 | 
			
		||||
 | 
			
		||||
class MainActivity : AppCompatActivity() {
 | 
			
		||||
    private lateinit var binding: ActivityMainBinding
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        binding = ActivityMainBinding.inflate(layoutInflater)
 | 
			
		||||
        val view = binding.root
 | 
			
		||||
        setContentView(view)
 | 
			
		||||
 | 
			
		||||
        val intent = Intent(this, LoginActivity::class.java)
 | 
			
		||||
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
        finish()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										101
									
								
								app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.app.NotificationChannel
 | 
			
		||||
import android.app.NotificationManager
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import androidx.multidex.MultiDexApplication
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.request.RequestOptions
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
 | 
			
		||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
 | 
			
		||||
import java.util.UUID.randomUUID
 | 
			
		||||
 | 
			
		||||
class MyApp : MultiDexApplication() {
 | 
			
		||||
    private lateinit var config: Config
 | 
			
		||||
 | 
			
		||||
    override fun onCreate() {
 | 
			
		||||
        super.onCreate()
 | 
			
		||||
        config = Config(baseContext)
 | 
			
		||||
 | 
			
		||||
        val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
        if (prefs.getString("unique_id", "")!!.isEmpty()) {
 | 
			
		||||
            val editor = prefs.edit()
 | 
			
		||||
            editor.putString("unique_id", randomUUID().toString())
 | 
			
		||||
            editor.apply()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        initDrawerImageLoader()
 | 
			
		||||
 | 
			
		||||
        initTheme()
 | 
			
		||||
 | 
			
		||||
        tryToHandleBug()
 | 
			
		||||
 | 
			
		||||
        handleNotificationChannels()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleNotificationChannels() {
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 | 
			
		||||
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
 | 
			
		||||
 | 
			
		||||
            val name = getString(R.string.notification_channel_sync)
 | 
			
		||||
            val importance = NotificationManager.IMPORTANCE_LOW
 | 
			
		||||
            val mChannel = NotificationChannel(Config.syncChannelId, name, importance)
 | 
			
		||||
 | 
			
		||||
            val newItemsChannelname = getString(R.string.new_items_channel_sync)
 | 
			
		||||
            val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
 | 
			
		||||
            val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
 | 
			
		||||
 | 
			
		||||
            notificationManager.createNotificationChannel(mChannel)
 | 
			
		||||
            notificationManager.createNotificationChannel(newItemsChannelmChannel)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun initDrawerImageLoader() {
 | 
			
		||||
        DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
 | 
			
		||||
            override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
 | 
			
		||||
                Glide.with(imageView?.context)
 | 
			
		||||
                    .loadMaybeBasicAuth(config, uri.toString())
 | 
			
		||||
                    .apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
 | 
			
		||||
                    .into(imageView)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun cancel(imageView: ImageView) {
 | 
			
		||||
                Glide.with(imageView?.context).clear(imageView)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun placeholder(ctx: Context, tag: String?): Drawable {
 | 
			
		||||
                return baseContext.resources.getDrawable(R.mipmap.ic_launcher)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun initTheme() {
 | 
			
		||||
        Scoop.waffleCone()
 | 
			
		||||
            .addFlavor(getString(R.string.default_theme), R.style.NoBar, true)
 | 
			
		||||
            .addFlavor(getString(R.string.default_dark_theme), R.style.NoBarDark, false)
 | 
			
		||||
            .setSharedPreferences(PreferenceManager.getDefaultSharedPreferences(this))
 | 
			
		||||
            .initialize()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun tryToHandleBug() {
 | 
			
		||||
        val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
 | 
			
		||||
 | 
			
		||||
        Thread.setDefaultUncaughtExceptionHandler { thread, e ->
 | 
			
		||||
            if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any {
 | 
			
		||||
                    it.toString().contains("android.view.ViewDebug")
 | 
			
		||||
                }) {
 | 
			
		||||
                Unit
 | 
			
		||||
            } else {
 | 
			
		||||
                oldHandler.uncaughtException(thread, e)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,281 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.graphics.PorterDuff
 | 
			
		||||
import android.graphics.PorterDuffColorFilter
 | 
			
		||||
import android.graphics.drawable.ColorDrawable
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.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 androidx.room.Room
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding
 | 
			
		||||
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.Toppings
 | 
			
		||||
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.SharedItems
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.toggleStar
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import me.relex.circleindicator.CircleIndicator
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
        if (willAddToFavorite) {
 | 
			
		||||
            toolbarMenu.findItem(R.id.star).icon.colorFilter = PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
 | 
			
		||||
        } else {
 | 
			
		||||
            toolbarMenu.findItem(R.id.star).icon.colorFilter = PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        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[position])
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun readItem(item: Item) {
 | 
			
		||||
        if (markOnScroll) {
 | 
			
		||||
                SharedItems.readItem(applicationContext, api, db, item)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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.star -> {
 | 
			
		||||
                if (allItems[binding.pager.currentItem].starred) {
 | 
			
		||||
                    SharedItems.unstarItem(
 | 
			
		||||
                        this@ReaderActivity,
 | 
			
		||||
                        api,
 | 
			
		||||
                        db,
 | 
			
		||||
                        allItems[binding.pager.currentItem]
 | 
			
		||||
                    )
 | 
			
		||||
                    afterUnsave()
 | 
			
		||||
                } else {
 | 
			
		||||
                    SharedItems.starItem(
 | 
			
		||||
                        this@ReaderActivity,
 | 
			
		||||
                        api,
 | 
			
		||||
                        db,
 | 
			
		||||
                        allItems[binding.pager.currentItem]
 | 
			
		||||
                    )
 | 
			
		||||
                    afterSave()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            R.id.align_left -> {
 | 
			
		||||
                editor.putInt("text_align", ALIGN_LEFT)
 | 
			
		||||
                editor.apply()
 | 
			
		||||
                alignmentMenu(true)
 | 
			
		||||
                refreshFragment()
 | 
			
		||||
            }
 | 
			
		||||
            R.id.align_justify -> {
 | 
			
		||||
                editor.putInt("text_align", JUSTIFY)
 | 
			
		||||
                editor.apply()
 | 
			
		||||
                alignmentMenu(false)
 | 
			
		||||
                refreshFragment()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return super.onOptionsItemSelected(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshFragment() {
 | 
			
		||||
        finish()
 | 
			
		||||
        overridePendingTransition(0, 0)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
        overridePendingTransition(0, 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        var allItems: ArrayList<Item> = ArrayList()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.res.ColorStateList
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Source
 | 
			
		||||
import apps.amine.bou.readerforselfoss.databinding.ActivitySourcesBinding
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.Toppings
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
 | 
			
		||||
class SourcesActivity : AppCompatActivity() {
 | 
			
		||||
 | 
			
		||||
    private lateinit var appColors: AppColors
 | 
			
		||||
    private lateinit var binding: ActivitySourcesBinding
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        appColors = AppColors(this@SourcesActivity)
 | 
			
		||||
 | 
			
		||||
        val scoop = Scoop.getInstance()
 | 
			
		||||
        scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
 | 
			
		||||
        scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        binding = ActivitySourcesBinding.inflate(layoutInflater)
 | 
			
		||||
        val view = binding.root
 | 
			
		||||
 | 
			
		||||
        setContentView(view)
 | 
			
		||||
 | 
			
		||||
        setSupportActionBar(binding.toolbar)
 | 
			
		||||
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
 | 
			
		||||
        supportActionBar?.setDisplayShowHomeEnabled(true)
 | 
			
		||||
 | 
			
		||||
        binding.fab.rippleColor = appColors.colorAccentDark
 | 
			
		||||
        binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStop() {
 | 
			
		||||
        super.onStop()
 | 
			
		||||
        binding.recyclerView.clearOnScrollListeners()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        val mLayoutManager = LinearLayoutManager(this)
 | 
			
		||||
 | 
			
		||||
        val settings =
 | 
			
		||||
            getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
        val prefs = PreferenceManager.getDefaultSharedPreferences(this)
 | 
			
		||||
 | 
			
		||||
        val api = SelfossApi(
 | 
			
		||||
            this,
 | 
			
		||||
            this@SourcesActivity,
 | 
			
		||||
            settings.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
            prefs.getString("api_timeout", "-1")!!.toLong()
 | 
			
		||||
        )
 | 
			
		||||
        var items: ArrayList<Source> = ArrayList()
 | 
			
		||||
 | 
			
		||||
        binding.recyclerView.setHasFixedSize(true)
 | 
			
		||||
        binding.recyclerView.layoutManager = mLayoutManager
 | 
			
		||||
 | 
			
		||||
        if (this@SourcesActivity.isNetworkAccessible(binding.recyclerView)) {
 | 
			
		||||
            api.sources.enqueue(object : Callback<List<Source>> {
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                    call: Call<List<Source>>,
 | 
			
		||||
                    response: Response<List<Source>>
 | 
			
		||||
                ) {
 | 
			
		||||
                    if (response.body() != null && response.body()!!.isNotEmpty()) {
 | 
			
		||||
                        items = response.body() as ArrayList<Source>
 | 
			
		||||
                    }
 | 
			
		||||
                    val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
 | 
			
		||||
                    binding.recyclerView.adapter = mAdapter
 | 
			
		||||
                    mAdapter.notifyDataSetChanged()
 | 
			
		||||
                    if (items.isEmpty()) {
 | 
			
		||||
                        Toast.makeText(
 | 
			
		||||
                            this@SourcesActivity,
 | 
			
		||||
                            R.string.nothing_here,
 | 
			
		||||
                            Toast.LENGTH_SHORT
 | 
			
		||||
                        ).show()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(call: Call<List<Source>>, t: Throwable) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        this@SourcesActivity,
 | 
			
		||||
                        R.string.cant_get_sources,
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.fab.setOnClickListener {
 | 
			
		||||
            startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,153 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.adapters
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.ImageView.ScaleType
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.databinding.CardItemBinding
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.SharedItems
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.shareLink
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
 | 
			
		||||
import com.amulyakhare.textdrawable.TextDrawable
 | 
			
		||||
import com.amulyakhare.textdrawable.util.ColorGenerator
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
 | 
			
		||||
class ItemCardAdapter(
 | 
			
		||||
    override val app: Activity,
 | 
			
		||||
    override var items: ArrayList<Item>,
 | 
			
		||||
    override val api: SelfossApi,
 | 
			
		||||
    override val db: AppDatabase,
 | 
			
		||||
    private val helper: CustomTabActivityHelper,
 | 
			
		||||
    private val internalBrowser: Boolean,
 | 
			
		||||
    private val articleViewer: Boolean,
 | 
			
		||||
    private val fullHeightCards: Boolean,
 | 
			
		||||
    override val appColors: AppColors,
 | 
			
		||||
    override val userIdentifier: String,
 | 
			
		||||
    override val config: Config,
 | 
			
		||||
    override val updateItems: (ArrayList<Item>) -> Unit
 | 
			
		||||
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
    private val generator: ColorGenerator = ColorGenerator.MATERIAL
 | 
			
		||||
    private val imageMaxHeight: Int =
 | 
			
		||||
        c.resources.getDimension(R.dimen.card_image_max_height).toInt()
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
 | 
			
		||||
        val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
        return ViewHolder(binding)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
 | 
			
		||||
        with(holder) {
 | 
			
		||||
            val itm = items[position]
 | 
			
		||||
 | 
			
		||||
            binding.favButton.isSelected = itm.starred
 | 
			
		||||
            binding.title.text = itm.getTitleDecoded()
 | 
			
		||||
 | 
			
		||||
            binding.title.setOnTouchListener(LinkOnTouchListener())
 | 
			
		||||
 | 
			
		||||
            binding.title.setLinkTextColor(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
            binding.sourceTitleAndDate.text = itm.sourceAndDateText()
 | 
			
		||||
 | 
			
		||||
            if (!fullHeightCards) {
 | 
			
		||||
                binding.itemImage.maxHeight = imageMaxHeight
 | 
			
		||||
                binding.itemImage.scaleType = ScaleType.CENTER_CROP
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (itm.getThumbnail(c).isEmpty()) {
 | 
			
		||||
                binding.itemImage.visibility = View.GONE
 | 
			
		||||
                Glide.with(c).clear(binding.itemImage)
 | 
			
		||||
                binding.itemImage.setImageDrawable(null)
 | 
			
		||||
            } else {
 | 
			
		||||
                binding.itemImage.visibility = View.VISIBLE
 | 
			
		||||
                c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (itm.getIcon(c).isEmpty()) {
 | 
			
		||||
                val color = generator.getColor(itm.getSourceTitle())
 | 
			
		||||
 | 
			
		||||
                val drawable =
 | 
			
		||||
                        TextDrawable
 | 
			
		||||
                                .builder()
 | 
			
		||||
                                .round()
 | 
			
		||||
                                .build(itm.getSourceTitle().toTextDrawableString(c), color)
 | 
			
		||||
                binding.sourceImage.setImageDrawable(drawable)
 | 
			
		||||
            } else {
 | 
			
		||||
                c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int {
 | 
			
		||||
        return items.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
 | 
			
		||||
        init {
 | 
			
		||||
            handleClickListeners()
 | 
			
		||||
            handleCustomTabActions()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun handleClickListeners() {
 | 
			
		||||
 | 
			
		||||
            binding.favButton.setOnClickListener {
 | 
			
		||||
                val item = items[bindingAdapterPosition]
 | 
			
		||||
                if (isNetworkAvailable(c)) {
 | 
			
		||||
                    if (item.starred) {
 | 
			
		||||
                        SharedItems.unstarItem(c, api, db, item)
 | 
			
		||||
                        item.starred = false
 | 
			
		||||
                        binding.favButton.isSelected = false
 | 
			
		||||
                    } else {
 | 
			
		||||
                        SharedItems.starItem(c, api, db, item)
 | 
			
		||||
                        item.starred = true
 | 
			
		||||
                        binding.favButton.isSelected = true
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                binding.shareBtn.setOnClickListener {
 | 
			
		||||
                    val item = items[bindingAdapterPosition]
 | 
			
		||||
                    c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                binding.browserBtn.setOnClickListener {
 | 
			
		||||
                    c.openInBrowserAsNewTask(items[bindingAdapterPosition])
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        private fun handleCustomTabActions() {
 | 
			
		||||
            val customTabsIntent = c.buildCustomTabsIntent()
 | 
			
		||||
            helper.bindCustomTabsService(app)
 | 
			
		||||
 | 
			
		||||
            binding.root.setOnClickListener {
 | 
			
		||||
                c.openItemUrl(
 | 
			
		||||
                    items,
 | 
			
		||||
                    bindingAdapterPosition,
 | 
			
		||||
                    items[bindingAdapterPosition].getLinkDecoded(),
 | 
			
		||||
                    customTabsIntent,
 | 
			
		||||
                    internalBrowser,
 | 
			
		||||
                    articleViewer,
 | 
			
		||||
                    app
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,105 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.adapters
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.databinding.ListItemBinding
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
 | 
			
		||||
import com.amulyakhare.textdrawable.TextDrawable
 | 
			
		||||
import com.amulyakhare.textdrawable.util.ColorGenerator
 | 
			
		||||
import kotlin.collections.ArrayList
 | 
			
		||||
 | 
			
		||||
class ItemListAdapter(
 | 
			
		||||
    override val app: Activity,
 | 
			
		||||
    override var items: ArrayList<Item>,
 | 
			
		||||
    override val api: SelfossApi,
 | 
			
		||||
    override val db: AppDatabase,
 | 
			
		||||
    private val helper: CustomTabActivityHelper,
 | 
			
		||||
    private val internalBrowser: Boolean,
 | 
			
		||||
    private val articleViewer: Boolean,
 | 
			
		||||
    override val userIdentifier: String,
 | 
			
		||||
    override val appColors: AppColors,
 | 
			
		||||
    override val config: Config,
 | 
			
		||||
    override val updateItems: (ArrayList<Item>) -> Unit
 | 
			
		||||
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
 | 
			
		||||
    private val generator: ColorGenerator = ColorGenerator.MATERIAL
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
 | 
			
		||||
        val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
        return ViewHolder(binding)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
 | 
			
		||||
        with(holder) {
 | 
			
		||||
            val itm = items[position]
 | 
			
		||||
 | 
			
		||||
            binding.title.text = itm.getTitleDecoded()
 | 
			
		||||
 | 
			
		||||
            binding.title.setOnTouchListener(LinkOnTouchListener())
 | 
			
		||||
 | 
			
		||||
            binding.title.setLinkTextColor(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
            binding.sourceTitleAndDate.text = itm.sourceAndDateText()
 | 
			
		||||
 | 
			
		||||
            if (itm.getThumbnail(c).isEmpty()) {
 | 
			
		||||
 | 
			
		||||
                if (itm.getIcon(c).isEmpty()) {
 | 
			
		||||
                    val color = generator.getColor(itm.getSourceTitle())
 | 
			
		||||
 | 
			
		||||
                    val drawable =
 | 
			
		||||
                            TextDrawable
 | 
			
		||||
                                    .builder()
 | 
			
		||||
                                    .round()
 | 
			
		||||
                                    .build(itm.getSourceTitle().toTextDrawableString(c), color)
 | 
			
		||||
 | 
			
		||||
                    binding.itemImage.setImageDrawable(drawable)
 | 
			
		||||
                } else {
 | 
			
		||||
                    c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int = items.size
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
 | 
			
		||||
 | 
			
		||||
        init {
 | 
			
		||||
            handleCustomTabActions()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun handleCustomTabActions() {
 | 
			
		||||
            val customTabsIntent = c.buildCustomTabsIntent()
 | 
			
		||||
            helper.bindCustomTabsService(app)
 | 
			
		||||
 | 
			
		||||
            binding.root.setOnClickListener {
 | 
			
		||||
                c.openItemUrl(
 | 
			
		||||
                    items,
 | 
			
		||||
                    bindingAdapterPosition,
 | 
			
		||||
                    items[bindingAdapterPosition].getLinkDecoded(),
 | 
			
		||||
                    customTabsIntent,
 | 
			
		||||
                    internalBrowser,
 | 
			
		||||
                    articleViewer,
 | 
			
		||||
                    app
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,119 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.adapters
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.SharedItems
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
 | 
			
		||||
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
 | 
			
		||||
    abstract var items: ArrayList<Item>
 | 
			
		||||
    abstract val api: SelfossApi
 | 
			
		||||
    abstract val db: AppDatabase
 | 
			
		||||
    abstract val userIdentifier: String
 | 
			
		||||
    abstract val app: Activity
 | 
			
		||||
    abstract val appColors: AppColors
 | 
			
		||||
    abstract val config: Config
 | 
			
		||||
    abstract val updateItems: (ArrayList<Item>) -> Unit
 | 
			
		||||
 | 
			
		||||
    fun updateAllItems() {
 | 
			
		||||
        items = SharedItems.focusedItems
 | 
			
		||||
        notifyDataSetChanged()
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun unmarkSnackbar(i: Item, position: Int) {
 | 
			
		||||
        val s = Snackbar
 | 
			
		||||
            .make(
 | 
			
		||||
                app.findViewById(R.id.coordLayout),
 | 
			
		||||
                R.string.marked_as_read,
 | 
			
		||||
                Snackbar.LENGTH_LONG
 | 
			
		||||
            )
 | 
			
		||||
            .setAction(R.string.undo_string) {
 | 
			
		||||
                SharedItems.unreadItem(app, api, db, i)
 | 
			
		||||
                if (SharedItems.displayedItems == "unread") {
 | 
			
		||||
                    addItemAtIndex(i, position)
 | 
			
		||||
                } else {
 | 
			
		||||
                    notifyItemChanged(position)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        val view = s.view
 | 
			
		||||
        val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
 | 
			
		||||
        tv.setTextColor(Color.WHITE)
 | 
			
		||||
        s.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun markSnackbar(position: Int) {
 | 
			
		||||
        val s = Snackbar
 | 
			
		||||
            .make(
 | 
			
		||||
                app.findViewById(R.id.coordLayout),
 | 
			
		||||
                R.string.marked_as_unread,
 | 
			
		||||
                Snackbar.LENGTH_LONG
 | 
			
		||||
            )
 | 
			
		||||
            .setAction(R.string.undo_string) {
 | 
			
		||||
                SharedItems.readItem(app, api, db, items[position])
 | 
			
		||||
                items = SharedItems.focusedItems
 | 
			
		||||
                if (SharedItems.displayedItems == "unread") {
 | 
			
		||||
                    notifyItemRemoved(position)
 | 
			
		||||
                    updateItems(items)
 | 
			
		||||
                } else {
 | 
			
		||||
                    notifyItemChanged(position)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        val view = s.view
 | 
			
		||||
        val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
 | 
			
		||||
        tv.setTextColor(Color.WHITE)
 | 
			
		||||
        s.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun handleItemAtIndex(position: Int) {
 | 
			
		||||
        if (SharedItems.unreadItemStatusAtIndex(position)) {
 | 
			
		||||
            readItemAtIndex(position)
 | 
			
		||||
        } else {
 | 
			
		||||
            unreadItemAtIndex(position)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun readItemAtIndex(position: Int) {
 | 
			
		||||
        val i = items[position]
 | 
			
		||||
        SharedItems.readItem(app, api, db, i)
 | 
			
		||||
        if (SharedItems.displayedItems == "unread") {
 | 
			
		||||
            items.remove(i)
 | 
			
		||||
            notifyItemRemoved(position)
 | 
			
		||||
            updateItems(items)
 | 
			
		||||
        } else {
 | 
			
		||||
            notifyItemChanged(position)
 | 
			
		||||
        }
 | 
			
		||||
        unmarkSnackbar(i, position)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun unreadItemAtIndex(position: Int) {
 | 
			
		||||
        SharedItems.unreadItem(app, api, db, items[position])
 | 
			
		||||
        notifyItemChanged(position)
 | 
			
		||||
        markSnackbar(position)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addItemAtIndex(item: Item, position: Int) {
 | 
			
		||||
        items.add(position, item)
 | 
			
		||||
        notifyItemInserted(position)
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addItemsAtEnd(newItems: List<Item>) {
 | 
			
		||||
        val oldSize = items.size
 | 
			
		||||
        items.addAll(newItems)
 | 
			
		||||
        notifyItemRangeInserted(oldSize, newItems.size)
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,245 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.selfoss
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.SharedItems
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
 | 
			
		||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
 | 
			
		||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
 | 
			
		||||
import com.burgstaller.okhttp.DispatchingAuthenticator
 | 
			
		||||
import com.burgstaller.okhttp.basic.BasicAuthenticator
 | 
			
		||||
import com.burgstaller.okhttp.digest.CachingAuthenticator
 | 
			
		||||
import com.burgstaller.okhttp.digest.Credentials
 | 
			
		||||
import com.burgstaller.okhttp.digest.DigestAuthenticator
 | 
			
		||||
import com.google.gson.GsonBuilder
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
 | 
			
		||||
import okhttp3.ResponseBody.Companion.toResponseBody
 | 
			
		||||
import okhttp3.logging.HttpLoggingInterceptor
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Retrofit
 | 
			
		||||
import retrofit2.converter.gson.GsonConverterFactory
 | 
			
		||||
import java.net.SocketTimeoutException
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
class SelfossApi(
 | 
			
		||||
    c: Context,
 | 
			
		||||
    callingActivity: Activity?,
 | 
			
		||||
    isWithSelfSignedCert: Boolean,
 | 
			
		||||
    timeout: Long
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    private lateinit var service: SelfossService
 | 
			
		||||
    private val config: Config = Config(c)
 | 
			
		||||
    private val userName: String
 | 
			
		||||
    private val password: String
 | 
			
		||||
 | 
			
		||||
    fun OkHttpClient.Builder.maybeWithSelfSigned(isWithSelfSignedCert: Boolean): OkHttpClient.Builder =
 | 
			
		||||
        if (isWithSelfSignedCert) {
 | 
			
		||||
            getUnsafeHttpClient()
 | 
			
		||||
        } else {
 | 
			
		||||
            this
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    fun OkHttpClient.Builder.maybeWithSettingsTimeout(timeout: Long): OkHttpClient.Builder =
 | 
			
		||||
        if (timeout != -1L) {
 | 
			
		||||
            this.readTimeout(timeout, TimeUnit.SECONDS)
 | 
			
		||||
                .connectTimeout(timeout, TimeUnit.SECONDS)
 | 
			
		||||
        } else {
 | 
			
		||||
            this
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    fun Credentials.createAuthenticator(): DispatchingAuthenticator =
 | 
			
		||||
        DispatchingAuthenticator.Builder()
 | 
			
		||||
            .with("digest", DigestAuthenticator(this))
 | 
			
		||||
            .with("basic", BasicAuthenticator(this))
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean, timeout: Long): OkHttpClient.Builder {
 | 
			
		||||
        val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
 | 
			
		||||
        return OkHttpClient
 | 
			
		||||
            .Builder()
 | 
			
		||||
            .maybeWithSettingsTimeout(timeout)
 | 
			
		||||
            .maybeWithSelfSigned(isWithSelfSignedCert)
 | 
			
		||||
            .authenticator(CachingAuthenticatorDecorator(this, authCache))
 | 
			
		||||
            .addInterceptor(AuthenticationCacheInterceptor(authCache))
 | 
			
		||||
            .addInterceptor(object: Interceptor {
 | 
			
		||||
                override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
                    val request: Request = chain.request()
 | 
			
		||||
                    val response: Response = chain.proceed(request)
 | 
			
		||||
 | 
			
		||||
                    if (response.code == 408) {
 | 
			
		||||
                        return response
 | 
			
		||||
                    }
 | 
			
		||||
                    return response
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        userName = config.userLogin
 | 
			
		||||
        password = config.userPassword
 | 
			
		||||
 | 
			
		||||
        val authenticator =
 | 
			
		||||
            Credentials(
 | 
			
		||||
                config.httpUserLogin,
 | 
			
		||||
                config.httpUserPassword
 | 
			
		||||
            ).createAuthenticator()
 | 
			
		||||
 | 
			
		||||
        val gson =
 | 
			
		||||
            GsonBuilder()
 | 
			
		||||
                .registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
 | 
			
		||||
                .registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter())
 | 
			
		||||
                .setLenient()
 | 
			
		||||
                .create()
 | 
			
		||||
 | 
			
		||||
        val logging = HttpLoggingInterceptor()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        logging.level = HttpLoggingInterceptor.Level.NONE
 | 
			
		||||
        val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout)
 | 
			
		||||
 | 
			
		||||
        val timeoutCode = 504
 | 
			
		||||
        httpClient
 | 
			
		||||
                .addInterceptor { chain ->
 | 
			
		||||
                    val res = chain.proceed(chain.request())
 | 
			
		||||
                    if (res.code == timeoutCode) {
 | 
			
		||||
                        throw SocketTimeoutException("timeout")
 | 
			
		||||
                    }
 | 
			
		||||
                    res
 | 
			
		||||
                }
 | 
			
		||||
                .addInterceptor(logging)
 | 
			
		||||
                .addInterceptor { chain ->
 | 
			
		||||
                    val request = chain.request()
 | 
			
		||||
                    try {
 | 
			
		||||
                        chain.proceed(request)
 | 
			
		||||
                    } catch (e: SocketTimeoutException) {
 | 
			
		||||
                        Response.Builder()
 | 
			
		||||
                                .code(timeoutCode)
 | 
			
		||||
                                .protocol(Protocol.HTTP_2)
 | 
			
		||||
                                .body("".toResponseBody("text/plain".toMediaTypeOrNull()))
 | 
			
		||||
                                .message("")
 | 
			
		||||
                                .request(request)
 | 
			
		||||
                                .build()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val retrofit =
 | 
			
		||||
                Retrofit
 | 
			
		||||
                    .Builder()
 | 
			
		||||
                    .baseUrl(config.baseUrl)
 | 
			
		||||
                    .client(httpClient.build())
 | 
			
		||||
                    .addConverterFactory(GsonConverterFactory.create(gson))
 | 
			
		||||
                    .build()
 | 
			
		||||
            service = retrofit.create(SelfossService::class.java)
 | 
			
		||||
        } catch (e: IllegalArgumentException) {
 | 
			
		||||
            if (callingActivity != null) {
 | 
			
		||||
                Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun login(): Call<SuccessResponse> =
 | 
			
		||||
        service.loginToSelfoss(config.userLogin, config.userPassword)
 | 
			
		||||
 | 
			
		||||
    suspend fun readItems(
 | 
			
		||||
        itemsNumber: Int,
 | 
			
		||||
        offset: Int
 | 
			
		||||
    ): retrofit2.Response<List<Item>> =
 | 
			
		||||
        getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
 | 
			
		||||
 | 
			
		||||
    suspend fun newItems(
 | 
			
		||||
        itemsNumber: Int,
 | 
			
		||||
        offset: Int
 | 
			
		||||
    ): retrofit2.Response<List<Item>> =
 | 
			
		||||
        getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
 | 
			
		||||
 | 
			
		||||
    suspend fun starredItems(
 | 
			
		||||
        itemsNumber: Int,
 | 
			
		||||
        offset: Int
 | 
			
		||||
    ): retrofit2.Response<List<Item>> =
 | 
			
		||||
        getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
 | 
			
		||||
 | 
			
		||||
    fun allItems(): Call<List<Item>> =
 | 
			
		||||
        service.allItems(userName, password)
 | 
			
		||||
 | 
			
		||||
    suspend fun allNewItems(): retrofit2.Response<List<Item>> =
 | 
			
		||||
            getItems("unread", null, null, null, 200, 0)
 | 
			
		||||
 | 
			
		||||
    suspend fun allReadItems(): retrofit2.Response<List<Item>> =
 | 
			
		||||
            getItems("read", null, null, null, 200, 0)
 | 
			
		||||
 | 
			
		||||
    suspend fun allStarredItems(): retrofit2.Response<List<Item>> =
 | 
			
		||||
        getItems("read", null, null, null, 200, 0)
 | 
			
		||||
 | 
			
		||||
    private suspend fun getItems(
 | 
			
		||||
        type: String,
 | 
			
		||||
        tag: String?,
 | 
			
		||||
        sourceId: Long?,
 | 
			
		||||
        search: String?,
 | 
			
		||||
        items: Int,
 | 
			
		||||
        offset: Int
 | 
			
		||||
    ): retrofit2.Response<List<Item>> =
 | 
			
		||||
        service.getItems(type, tag, sourceId, search, null, userName, password, items, offset)
 | 
			
		||||
 | 
			
		||||
    suspend fun updateItems(
 | 
			
		||||
        updatedSince: String
 | 
			
		||||
    ): retrofit2.Response<List<Item>> =
 | 
			
		||||
        service.getItems("read", null, null, null, updatedSince, userName, password, 200, 0)
 | 
			
		||||
 | 
			
		||||
    fun markItem(itemId: String): Call<SuccessResponse> =
 | 
			
		||||
        service.markAsRead(itemId, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun unmarkItem(itemId: String): Call<SuccessResponse> =
 | 
			
		||||
        service.unmarkAsRead(itemId, userName, password)
 | 
			
		||||
 | 
			
		||||
    suspend fun readAll(ids: List<String>): SuccessResponse =
 | 
			
		||||
        service.markAllAsRead(ids, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun starrItem(itemId: String): Call<SuccessResponse> =
 | 
			
		||||
        service.starr(itemId, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun unstarrItem(itemId: String): Call<SuccessResponse> =
 | 
			
		||||
        service.unstarr(itemId, userName, password)
 | 
			
		||||
 | 
			
		||||
    suspend fun stats(): retrofit2.Response<Stats> = service.stats(userName, password)
 | 
			
		||||
 | 
			
		||||
    val tags: Call<List<Tag>>
 | 
			
		||||
        get() = service.tags(userName, password)
 | 
			
		||||
 | 
			
		||||
    fun update(): Call<String> =
 | 
			
		||||
        service.update(userName, password)
 | 
			
		||||
 | 
			
		||||
    val apiVersion: Call<ApiVersion>
 | 
			
		||||
        get() = service.version()
 | 
			
		||||
 | 
			
		||||
    val sources: Call<List<Source>>
 | 
			
		||||
        get() = service.sources(userName, password)
 | 
			
		||||
 | 
			
		||||
    fun deleteSource(id: String): Call<SuccessResponse> =
 | 
			
		||||
        service.deleteSource(id, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun spouts(): Call<Map<String, Spout>> =
 | 
			
		||||
        service.spouts(userName, password)
 | 
			
		||||
 | 
			
		||||
    fun createSource(
 | 
			
		||||
        title: String,
 | 
			
		||||
        url: String,
 | 
			
		||||
        spout: String,
 | 
			
		||||
        tags: String,
 | 
			
		||||
        filter: String
 | 
			
		||||
    ): Call<SuccessResponse> =
 | 
			
		||||
        service.createSource(title, url, spout, tags, filter, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun createSourceApi2(
 | 
			
		||||
        title: String,
 | 
			
		||||
        url: String,
 | 
			
		||||
        spout: String,
 | 
			
		||||
        tags: List<String>,
 | 
			
		||||
        filter: String
 | 
			
		||||
    ): Call<SuccessResponse> =
 | 
			
		||||
        service.createSourceApi2(title, url, spout, tags, filter, userName, password)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,134 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.selfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.SharedItems
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
 | 
			
		||||
suspend fun getAndStoreAllItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) {
 | 
			
		||||
    if (isNetworkAvailable(context)) {
 | 
			
		||||
        launch {
 | 
			
		||||
            try {
 | 
			
		||||
                enqueueArticles(api.allNewItems(), db, true)
 | 
			
		||||
            } catch (e: Throwable) {}
 | 
			
		||||
        }
 | 
			
		||||
        launch {
 | 
			
		||||
            try {
 | 
			
		||||
                enqueueArticles(api.allReadItems(), db, false)
 | 
			
		||||
            } catch (e: Throwable) {}
 | 
			
		||||
        }
 | 
			
		||||
        launch {
 | 
			
		||||
            try {
 | 
			
		||||
                enqueueArticles(api.allStarredItems(), db, false)
 | 
			
		||||
            } catch (e: Throwable) {}
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        launch { SharedItems.updateDatabase(db) }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun updateItems(context: Context, api: SelfossApi, db: AppDatabase) = coroutineScope {
 | 
			
		||||
    if (isNetworkAvailable(context)) {
 | 
			
		||||
        launch {
 | 
			
		||||
            try {
 | 
			
		||||
                enqueueArticles(api.updateItems(SharedItems.items[0].datetime), db, true)
 | 
			
		||||
            } catch (e: Throwable) {}
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun refreshFocusedItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) {
 | 
			
		||||
    if (isNetworkAvailable(context)) {
 | 
			
		||||
        val response = when (SharedItems.displayedItems) {
 | 
			
		||||
            "read" -> api.readItems(200, 0)
 | 
			
		||||
            "unread" -> api.newItems(200, 0)
 | 
			
		||||
            "starred" -> api.starredItems(200, 0)
 | 
			
		||||
            else -> api.readItems(200, 0)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (response.isSuccessful) {
 | 
			
		||||
            SharedItems.refreshFocusedItems(response.body() as ArrayList<Item>)
 | 
			
		||||
            SharedItems.updateDatabase(db)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun getReadItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
 | 
			
		||||
    if (isNetworkAvailable(context)) {
 | 
			
		||||
            try {
 | 
			
		||||
                enqueueArticles(api.readItems( 200, offset), db, false)
 | 
			
		||||
                SharedItems.fetchedAll = true
 | 
			
		||||
                SharedItems.updateDatabase(db)
 | 
			
		||||
            } catch (e: Throwable) {}
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun getUnreadItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
 | 
			
		||||
    if (isNetworkAvailable(context)) {
 | 
			
		||||
        try {
 | 
			
		||||
            if (!SharedItems.fetchedUnread) {
 | 
			
		||||
                SharedItems.clearDBItems(db)
 | 
			
		||||
            }
 | 
			
		||||
            enqueueArticles(api.newItems(200, offset), db, false)
 | 
			
		||||
            SharedItems.fetchedUnread = true
 | 
			
		||||
        } catch (e: Throwable) {}
 | 
			
		||||
    }
 | 
			
		||||
    SharedItems.updateDatabase(db)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun getStarredItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
 | 
			
		||||
    if (isNetworkAvailable(context)) {
 | 
			
		||||
        try {
 | 
			
		||||
            enqueueArticles(api.starredItems(200, offset), db, false)
 | 
			
		||||
            SharedItems.fetchedStarred = true
 | 
			
		||||
            SharedItems.updateDatabase(db)
 | 
			
		||||
        } catch (e: Throwable) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun readAll(context: Context, api: SelfossApi, db: AppDatabase): Boolean {
 | 
			
		||||
    var success = false
 | 
			
		||||
    if (isNetworkAvailable(context)) {
 | 
			
		||||
        try {
 | 
			
		||||
            val ids = SharedItems.focusedItems.map { it.id }
 | 
			
		||||
            if (ids.isNotEmpty()) {
 | 
			
		||||
                val result = api.readAll(ids)
 | 
			
		||||
                SharedItems.readItems(db, ids)
 | 
			
		||||
                success = result.isSuccess
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Throwable) {}
 | 
			
		||||
    }
 | 
			
		||||
    return success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun reloadBadges(context: Context, api: SelfossApi) = withContext(Dispatchers.IO) {
 | 
			
		||||
    if (isNetworkAvailable(context)) {
 | 
			
		||||
        try {
 | 
			
		||||
            val response = api.stats()
 | 
			
		||||
 | 
			
		||||
            if (response.isSuccessful) {
 | 
			
		||||
                val badges = response.body()
 | 
			
		||||
                SharedItems.badgeUnread = badges!!.unread
 | 
			
		||||
                SharedItems.badgeAll = badges.total
 | 
			
		||||
                SharedItems.badgeStarred = badges.starred
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Throwable) {}
 | 
			
		||||
    } else {
 | 
			
		||||
        SharedItems.computeBadges()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun enqueueArticles(response: Response<List<Item>>, db: AppDatabase, clearDatabase: Boolean) {
 | 
			
		||||
        if (response.isSuccessful) {
 | 
			
		||||
            if (clearDatabase) {
 | 
			
		||||
                CoroutineScope(Dispatchers.IO).launch {
 | 
			
		||||
                    SharedItems.clearDBItems(db)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            val allItems = response.body() as ArrayList<Item>
 | 
			
		||||
            SharedItems.appendNewItems(allItems)
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,251 @@
 | 
			
		||||
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
 | 
			
		||||
) {
 | 
			
		||||
    fun getTitleDecoded(): String {
 | 
			
		||||
        return Html.fromHtml(tag).toString()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SuccessResponse(@SerializedName("success") val success: Boolean) {
 | 
			
		||||
    val isSuccess: Boolean
 | 
			
		||||
        get() = success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Stats(
 | 
			
		||||
    @SerializedName("total") val total: Int,
 | 
			
		||||
    @SerializedName("unread") val unread: Int,
 | 
			
		||||
    @SerializedName("starred") val starred: Int
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
data class Spout(
 | 
			
		||||
    @SerializedName("name") val name: String,
 | 
			
		||||
    @SerializedName("description") val description: String
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
data class ApiVersion(
 | 
			
		||||
        @SerializedName("version") val version: String?,
 | 
			
		||||
        @SerializedName("apiversion") val apiversion: String?
 | 
			
		||||
) {
 | 
			
		||||
    fun getApiMajorVersion() : Int {
 | 
			
		||||
        var versionNumber = 0
 | 
			
		||||
        if (apiversion != null) {
 | 
			
		||||
            versionNumber = apiversion.substringBefore(".").toInt()
 | 
			
		||||
        }
 | 
			
		||||
        return versionNumber
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class Source(
 | 
			
		||||
    @SerializedName("id") val id: String,
 | 
			
		||||
    @SerializedName("title") val title: String,
 | 
			
		||||
    @SerializedName("tags") val tags: SelfossTagType,
 | 
			
		||||
    @SerializedName("spout") val spout: String,
 | 
			
		||||
    @SerializedName("error") val error: String,
 | 
			
		||||
    @SerializedName("icon") val icon: String
 | 
			
		||||
) {
 | 
			
		||||
    var config: Config? = null
 | 
			
		||||
 | 
			
		||||
    fun getIcon(app: Context): String {
 | 
			
		||||
        if (config == null) {
 | 
			
		||||
            config = Config(app)
 | 
			
		||||
        }
 | 
			
		||||
        return constructUrl(config, "favicons", icon)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getTitleDecoded(): String {
 | 
			
		||||
        return Html.fromHtml(title).toString()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class Item(
 | 
			
		||||
    @SerializedName("id") val id: String,
 | 
			
		||||
    @SerializedName("datetime") val datetime: String,
 | 
			
		||||
    @SerializedName("title") val title: String,
 | 
			
		||||
    @SerializedName("content") val content: String,
 | 
			
		||||
    @SerializedName("unread") var unread: Boolean,
 | 
			
		||||
    @SerializedName("starred") var starred: Boolean,
 | 
			
		||||
    @SerializedName("thumbnail") val thumbnail: String?,
 | 
			
		||||
    @SerializedName("icon") val icon: String?,
 | 
			
		||||
    @SerializedName("link") val link: String,
 | 
			
		||||
    @SerializedName("sourcetitle") val sourcetitle: String,
 | 
			
		||||
    @SerializedName("tags") val tags: SelfossTagType
 | 
			
		||||
) : Parcelable {
 | 
			
		||||
 | 
			
		||||
    var config: Config? = null
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        @JvmField val CREATOR: Parcelable.Creator<Item> = object : Parcelable.Creator<Item> {
 | 
			
		||||
            override fun createFromParcel(source: Parcel): Item = Item(source)
 | 
			
		||||
            override fun newArray(size: Int): Array<Item?> = arrayOfNulls(size)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(source: Parcel) : this(
 | 
			
		||||
        id = source.readString().orEmpty(),
 | 
			
		||||
        datetime = source.readString().orEmpty(),
 | 
			
		||||
        title = source.readString().orEmpty(),
 | 
			
		||||
        content = source.readString().orEmpty(),
 | 
			
		||||
        unread = 0.toByte() != source.readByte(),
 | 
			
		||||
        starred = 0.toByte() != source.readByte(),
 | 
			
		||||
        thumbnail = source.readString(),
 | 
			
		||||
        icon = source.readString(),
 | 
			
		||||
        link = source.readString().orEmpty(),
 | 
			
		||||
        sourcetitle = source.readString().orEmpty(),
 | 
			
		||||
        tags = if (source.readParcelable<SelfossTagType>(ClassLoader.getSystemClassLoader()) != null) source.readParcelable(ClassLoader.getSystemClassLoader())!! else SelfossTagType("")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun describeContents() = 0
 | 
			
		||||
 | 
			
		||||
    override fun writeToParcel(dest: Parcel, flags: Int) {
 | 
			
		||||
        dest.writeString(id)
 | 
			
		||||
        dest.writeString(datetime)
 | 
			
		||||
        dest.writeString(title)
 | 
			
		||||
        dest.writeString(content)
 | 
			
		||||
        dest.writeByte((if (unread) 1 else 0))
 | 
			
		||||
        dest.writeByte((if (starred) 1 else 0))
 | 
			
		||||
        dest.writeString(thumbnail)
 | 
			
		||||
        dest.writeString(icon)
 | 
			
		||||
        dest.writeString(link)
 | 
			
		||||
        dest.writeString(sourcetitle)
 | 
			
		||||
        dest.writeParcelable(tags, flags)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getIcon(app: Context): String {
 | 
			
		||||
        if (config == null) {
 | 
			
		||||
            config = Config(app)
 | 
			
		||||
        }
 | 
			
		||||
        return constructUrl(config, "favicons", icon)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getThumbnail(app: Context): String {
 | 
			
		||||
        if (config == null) {
 | 
			
		||||
            config = Config(app)
 | 
			
		||||
        }
 | 
			
		||||
        return constructUrl(config, "thumbnails", thumbnail)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getImages() : ArrayList<String> {
 | 
			
		||||
        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,141 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.selfoss
 | 
			
		||||
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import retrofit2.http.DELETE
 | 
			
		||||
import retrofit2.http.Field
 | 
			
		||||
import retrofit2.http.FormUrlEncoded
 | 
			
		||||
import retrofit2.http.GET
 | 
			
		||||
import retrofit2.http.Headers
 | 
			
		||||
import retrofit2.http.POST
 | 
			
		||||
import retrofit2.http.Path
 | 
			
		||||
import retrofit2.http.Query
 | 
			
		||||
 | 
			
		||||
internal interface SelfossService {
 | 
			
		||||
 | 
			
		||||
    @GET("login")
 | 
			
		||||
    fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @GET("items")
 | 
			
		||||
    suspend fun getItems(
 | 
			
		||||
        @Query("type") type: String,
 | 
			
		||||
        @Query("tag") tag: String?,
 | 
			
		||||
        @Query("source") source: Long?,
 | 
			
		||||
        @Query("search") search: String?,
 | 
			
		||||
        @Query("updatedsince") updatedSince: String?,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String,
 | 
			
		||||
        @Query("items") items: Int,
 | 
			
		||||
        @Query("offset") offset: Int
 | 
			
		||||
    ): Response<List<Item>>
 | 
			
		||||
 | 
			
		||||
    @GET("items")
 | 
			
		||||
    fun allItems(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<List<Item>>
 | 
			
		||||
 | 
			
		||||
    @Headers("Content-Type: application/x-www-form-urlencoded")
 | 
			
		||||
    @POST("mark/{id}")
 | 
			
		||||
    fun markAsRead(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @Headers("Content-Type: application/x-www-form-urlencoded")
 | 
			
		||||
    @POST("unmark/{id}")
 | 
			
		||||
    fun unmarkAsRead(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @FormUrlEncoded
 | 
			
		||||
    @POST("mark")
 | 
			
		||||
    suspend fun markAllAsRead(
 | 
			
		||||
        @Field("ids[]") ids: List<String>,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): SuccessResponse
 | 
			
		||||
 | 
			
		||||
    @Headers("Content-Type: application/x-www-form-urlencoded")
 | 
			
		||||
    @POST("starr/{id}")
 | 
			
		||||
    fun starr(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @Headers("Content-Type: application/x-www-form-urlencoded")
 | 
			
		||||
    @POST("unstarr/{id}")
 | 
			
		||||
    fun unstarr(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @GET("stats")
 | 
			
		||||
    suspend fun stats(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Response<Stats>
 | 
			
		||||
 | 
			
		||||
    @GET("tags")
 | 
			
		||||
    fun tags(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<List<Tag>>
 | 
			
		||||
 | 
			
		||||
    @GET("update")
 | 
			
		||||
    fun update(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<String>
 | 
			
		||||
 | 
			
		||||
    @GET("sources/spouts")
 | 
			
		||||
    fun spouts(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<Map<String, Spout>>
 | 
			
		||||
 | 
			
		||||
    @GET("sources/list")
 | 
			
		||||
    fun sources(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<List<Source>>
 | 
			
		||||
 | 
			
		||||
    @GET("api/about")
 | 
			
		||||
    fun version(): Call<ApiVersion>
 | 
			
		||||
 | 
			
		||||
    @DELETE("source/{id}")
 | 
			
		||||
    fun deleteSource(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @FormUrlEncoded
 | 
			
		||||
    @POST("source")
 | 
			
		||||
    fun createSource(
 | 
			
		||||
        @Field("title") title: String,
 | 
			
		||||
        @Field("url") url: String,
 | 
			
		||||
        @Field("spout") spout: String,
 | 
			
		||||
        @Field("tags") tags: String,
 | 
			
		||||
        @Field("filter") filter: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @FormUrlEncoded
 | 
			
		||||
    @POST("source")
 | 
			
		||||
    fun createSourceApi2(
 | 
			
		||||
        @Field("title") title: String,
 | 
			
		||||
        @Field("url") url: String,
 | 
			
		||||
        @Field("spout") spout: String,
 | 
			
		||||
        @Field("tags[]") tags: List<String>,
 | 
			
		||||
        @Field("filter") filter: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
}
 | 
			
		||||
@@ -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,169 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.background
 | 
			
		||||
 | 
			
		||||
import android.app.NotificationManager
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
 | 
			
		||||
import androidx.core.app.NotificationCompat.PRIORITY_LOW
 | 
			
		||||
import androidx.room.Room
 | 
			
		||||
import androidx.work.Worker
 | 
			
		||||
import androidx.work.WorkerParameters
 | 
			
		||||
import apps.amine.bou.readerforselfoss.MainActivity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.getAndStoreAllItems
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.SharedItems
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import java.util.*
 | 
			
		||||
import kotlin.concurrent.schedule
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
 | 
			
		||||
    lateinit var db: AppDatabase
 | 
			
		||||
 | 
			
		||||
override fun doWork(): Result {
 | 
			
		||||
    val settings =
 | 
			
		||||
        this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
    val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
 | 
			
		||||
    val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
 | 
			
		||||
    if (periodicRefresh) {
 | 
			
		||||
        val api = SelfossApi(
 | 
			
		||||
            this.context,
 | 
			
		||||
            null,
 | 
			
		||||
            settings.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
            sharedPref.getString("api_timeout", "-1")!!.toLong()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if (isNetworkAvailable(context)) {
 | 
			
		||||
 | 
			
		||||
            CoroutineScope(Dispatchers.IO).launch {
 | 
			
		||||
                val notificationManager =
 | 
			
		||||
                    applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 | 
			
		||||
 | 
			
		||||
                val notification =
 | 
			
		||||
                    NotificationCompat.Builder(applicationContext, Config.syncChannelId)
 | 
			
		||||
                        .setContentTitle(context.getString(R.string.loading_notification_title))
 | 
			
		||||
                        .setContentText(context.getString(R.string.loading_notification_text))
 | 
			
		||||
                        .setOngoing(true)
 | 
			
		||||
                        .setPriority(PRIORITY_LOW)
 | 
			
		||||
                        .setChannelId(Config.syncChannelId)
 | 
			
		||||
                        .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
 | 
			
		||||
 | 
			
		||||
                notificationManager.notify(1, notification.build())
 | 
			
		||||
 | 
			
		||||
                val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
 | 
			
		||||
 | 
			
		||||
                db = Room.databaseBuilder(
 | 
			
		||||
                    applicationContext,
 | 
			
		||||
                    AppDatabase::class.java, "selfoss-database"
 | 
			
		||||
                ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
 | 
			
		||||
                    .addMigrations(MIGRATION_3_4).build()
 | 
			
		||||
 | 
			
		||||
                val actions = db.actionsDao().actions()
 | 
			
		||||
 | 
			
		||||
                actions.forEach { action ->
 | 
			
		||||
                    when {
 | 
			
		||||
                        action.read -> doAndReportOnFail(
 | 
			
		||||
                            api.markItem(action.articleId),
 | 
			
		||||
                            action
 | 
			
		||||
                        )
 | 
			
		||||
                        action.unread -> doAndReportOnFail(
 | 
			
		||||
                            api.unmarkItem(action.articleId),
 | 
			
		||||
                            action
 | 
			
		||||
                        )
 | 
			
		||||
                        action.starred -> doAndReportOnFail(
 | 
			
		||||
                            api.starrItem(action.articleId),
 | 
			
		||||
                            action
 | 
			
		||||
                        )
 | 
			
		||||
                        action.unstarred -> doAndReportOnFail(
 | 
			
		||||
                            api.unstarrItem(action.articleId),
 | 
			
		||||
                            action
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                getAndStoreAllItems(context, api, db)
 | 
			
		||||
                SharedItems.updateDatabase(db)
 | 
			
		||||
                storeItems(notifyNewItems, notificationManager)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return Result.success()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) {
 | 
			
		||||
        CoroutineScope(Dispatchers.IO).launch {
 | 
			
		||||
                val apiItems = SharedItems.items
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                val newSize = apiItems.filter { it.unread }.size
 | 
			
		||||
                if (notifyNewItems && newSize > 0) {
 | 
			
		||||
 | 
			
		||||
                    val intent = Intent(context, MainActivity::class.java).apply {
 | 
			
		||||
                        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
 | 
			
		||||
                    }
 | 
			
		||||
                    val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
			
		||||
                        PendingIntent.FLAG_IMMUTABLE
 | 
			
		||||
                    } else {
 | 
			
		||||
                        0
 | 
			
		||||
                    }
 | 
			
		||||
                    val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags)
 | 
			
		||||
 | 
			
		||||
                    val newItemsNotification =
 | 
			
		||||
                        NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
 | 
			
		||||
                            .setContentTitle(context.getString(R.string.new_items_notification_title))
 | 
			
		||||
                            .setContentText(
 | 
			
		||||
                                context.getString(
 | 
			
		||||
                                    R.string.new_items_notification_text,
 | 
			
		||||
                                    newSize
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                            .setPriority(PRIORITY_DEFAULT)
 | 
			
		||||
                            .setChannelId(Config.newItemsChannelId)
 | 
			
		||||
                            .setContentIntent(pendingIntent)
 | 
			
		||||
                            .setAutoCancel(true)
 | 
			
		||||
                            .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
 | 
			
		||||
 | 
			
		||||
                    Timer("", false).schedule(4000) {
 | 
			
		||||
                        notificationManager.notify(2, newItemsNotification.build())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                apiItems.map { it.preloadImages(context) }
 | 
			
		||||
            Timer("", false).schedule(4000) {
 | 
			
		||||
                notificationManager.cancel(1)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) {
 | 
			
		||||
        call.enqueue(object : Callback<T> {
 | 
			
		||||
            override fun onResponse(
 | 
			
		||||
                call: Call<T>,
 | 
			
		||||
                response: Response<T>
 | 
			
		||||
            ) {
 | 
			
		||||
                thread {
 | 
			
		||||
                    db.actionsDao().delete(action)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onFailure(call: Call<T>, t: Throwable) {
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,591 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.fragments
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.content.res.ColorStateList
 | 
			
		||||
import android.content.res.TypedArray
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.graphics.Typeface
 | 
			
		||||
import android.graphics.drawable.ColorDrawable
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import android.view.*
 | 
			
		||||
import android.webkit.*
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import 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.databinding.FragmentArticleBinding
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.*
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import com.bumptech.glide.request.RequestOptions
 | 
			
		||||
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import java.net.MalformedURLException
 | 
			
		||||
import java.net.URL
 | 
			
		||||
import java.util.concurrent.ExecutionException
 | 
			
		||||
import kotlin.collections.ArrayList
 | 
			
		||||
 | 
			
		||||
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 = ""
 | 
			
		||||
    private var staticBar = false
 | 
			
		||||
 | 
			
		||||
    override fun onStop() {
 | 
			
		||||
        super.onStop()
 | 
			
		||||
        if (mCustomTabActivityHelper != null) {
 | 
			
		||||
            mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        appColors = AppColors(requireActivity())
 | 
			
		||||
        config = Config(requireActivity())
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        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()
 | 
			
		||||
            staticBar = prefs.getBoolean("reader_static_bar", false)
 | 
			
		||||
 | 
			
		||||
            font = prefs.getString("reader_font", "")!!
 | 
			
		||||
            if (font.isNotEmpty()) {
 | 
			
		||||
                resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
 | 
			
		||||
                typeface = try {
 | 
			
		||||
                    ResourcesCompat.getFont(requireContext(), resId)!!
 | 
			
		||||
                } catch (e: java.lang.Exception) {
 | 
			
		||||
                    // ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext())
 | 
			
		||||
                    // Just to be sure
 | 
			
		||||
                    null
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            refreshAlignment()
 | 
			
		||||
 | 
			
		||||
            val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
 | 
			
		||||
            val api = SelfossApi(
 | 
			
		||||
                requireContext(),
 | 
			
		||||
                requireActivity(),
 | 
			
		||||
                settings.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
                prefs.getString("api_timeout", "-1")!!.toLong()
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            fab = binding.fab
 | 
			
		||||
 | 
			
		||||
            fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
            fab.rippleColor = appColors.colorAccentDark
 | 
			
		||||
 | 
			
		||||
            val floatingToolbar: FloatingToolbar = binding.floatingToolbar
 | 
			
		||||
            floatingToolbar.attachFab(fab)
 | 
			
		||||
 | 
			
		||||
            floatingToolbar.background = ColorDrawable(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
            val customTabsIntent = requireActivity().buildCustomTabsIntent()
 | 
			
		||||
            mCustomTabActivityHelper = CustomTabActivityHelper()
 | 
			
		||||
            mCustomTabActivityHelper!!.bindCustomTabsService(activity)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            floatingToolbar.setClickListener(
 | 
			
		||||
                object : FloatingToolbar.ItemClickListener {
 | 
			
		||||
                    override fun onItemClick(item: MenuItem) {
 | 
			
		||||
                        when (item.itemId) {
 | 
			
		||||
                            R.id.more_action -> getContentFromMercury(customTabsIntent, 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) {
 | 
			
		||||
                                if (allItems[pageNumber.toInt()].unread) {
 | 
			
		||||
                                    SharedItems.readItem(
 | 
			
		||||
                                        context!!,
 | 
			
		||||
                                        api,
 | 
			
		||||
                                        db,
 | 
			
		||||
                                        allItems[pageNumber.toInt()]
 | 
			
		||||
                                    )
 | 
			
		||||
                                    allItems[pageNumber.toInt()].unread = false
 | 
			
		||||
                                    Toast.makeText(
 | 
			
		||||
                                        context,
 | 
			
		||||
                                        R.string.marked_as_read,
 | 
			
		||||
                                        Toast.LENGTH_LONG
 | 
			
		||||
                                    ).show()
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    SharedItems.unreadItem(
 | 
			
		||||
                                        context!!,
 | 
			
		||||
                                        api,
 | 
			
		||||
                                        db,
 | 
			
		||||
                                        allItems[pageNumber.toInt()]
 | 
			
		||||
                                    )
 | 
			
		||||
                                    allItems[pageNumber.toInt()].unread = true
 | 
			
		||||
                                    Toast.makeText(
 | 
			
		||||
                                        context,
 | 
			
		||||
                                        R.string.marked_as_unread,
 | 
			
		||||
                                        Toast.LENGTH_LONG
 | 
			
		||||
                                    ).show()
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            else -> Unit
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onItemLongClick(item: MenuItem?) {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if (staticBar) {
 | 
			
		||||
                fab.hide()
 | 
			
		||||
                floatingToolbar.show()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            binding.source.text = contentSource
 | 
			
		||||
            if (typeface != null) {
 | 
			
		||||
                binding.source.typeface = typeface
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (contentText.isEmptyOrNullOrNullString()) {
 | 
			
		||||
                getContentFromMercury(customTabsIntent, 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) {
 | 
			
		||||
                        floatingToolbar.hide()
 | 
			
		||||
                        fab.hide()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if (staticBar) {
 | 
			
		||||
                            floatingToolbar.show()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        } catch (e: InflateException) {
 | 
			
		||||
            AlertDialog.Builder(requireContext())
 | 
			
		||||
                .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
 | 
			
		||||
                .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
 | 
			
		||||
                .setPositiveButton(android.R.string.ok
 | 
			
		||||
                ) { 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) {
 | 
			
		||||
                Pair(ContextCompat.getColor(requireContext(), R.color.dark_webview_text), ContextCompat.getColor(requireContext(), R.color.dark_webview))
 | 
			
		||||
            } else {
 | 
			
		||||
                Pair(null, null)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (context != null) {
 | 
			
		||||
                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)}
 | 
			
		||||
 | 
			
		||||
        binding.webcontent.settings.layoutAlgorithm =
 | 
			
		||||
                WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
 | 
			
		||||
 | 
			
		||||
        var baseUrl: String? = null
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val itemUrl = URL(url)
 | 
			
		||||
            baseUrl = itemUrl.protocol + "://" + itemUrl.host
 | 
			
		||||
        } catch (e: MalformedURLException) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val fontName =  when (font) {
 | 
			
		||||
            getString(R.string.open_sans_font_id) -> "Open Sans"
 | 
			
		||||
            getString(R.string.roboto_font_id) -> "Roboto"
 | 
			
		||||
            else -> ""
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val fontLinkAndStyle = if (font.isNotEmpty()) {
 | 
			
		||||
            """<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
 | 
			
		||||
                |<style>
 | 
			
		||||
                |   * {
 | 
			
		||||
                |       font-family: '$fontName';
 | 
			
		||||
                |   }
 | 
			
		||||
                |</style>
 | 
			
		||||
            """.trimMargin()
 | 
			
		||||
        } else {
 | 
			
		||||
            ""
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.webcontent.loadDataWithBaseURL(
 | 
			
		||||
            baseUrl,
 | 
			
		||||
            """<html>
 | 
			
		||||
                |<head>
 | 
			
		||||
                |   <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
                |   <style>
 | 
			
		||||
                |      img {
 | 
			
		||||
                |        display: inline-block;
 | 
			
		||||
                |        height: auto;
 | 
			
		||||
                |        width: 100%;
 | 
			
		||||
                |        max-width: 100%;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      a {
 | 
			
		||||
                |        color: $stringColor !important;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      *:not(a) {
 | 
			
		||||
                |        color: $stringTextColor;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      * {
 | 
			
		||||
                |        font-size: ${fontSize}px;
 | 
			
		||||
                |        text-align: $textAlignment;
 | 
			
		||||
                |        word-break: break-word;
 | 
			
		||||
                |        overflow:hidden;
 | 
			
		||||
                |        line-height: 1.5em;
 | 
			
		||||
                |        background-color: $stringBackgroundColor;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      body, html {
 | 
			
		||||
                |        background-color: $stringBackgroundColor !important;
 | 
			
		||||
                |        border-color: $stringBackgroundColor  !important;
 | 
			
		||||
                |        padding: 0 !important;
 | 
			
		||||
                |        margin: 0 !important;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      a, pre, code {
 | 
			
		||||
                |        text-align: $textAlignment;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      pre, code {
 | 
			
		||||
                |        white-space: pre-wrap;
 | 
			
		||||
                |        width:100%;
 | 
			
		||||
                |        background-color: $stringBackgroundColor;
 | 
			
		||||
                |      }
 | 
			
		||||
                |   </style>
 | 
			
		||||
                |   $fontLinkAndStyle
 | 
			
		||||
                |</head>
 | 
			
		||||
                |<body>
 | 
			
		||||
                |   $contentText
 | 
			
		||||
                |</body>""".trimMargin(),
 | 
			
		||||
            "text/html",
 | 
			
		||||
            "utf-8",
 | 
			
		||||
            null
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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")
 | 
			
		||||
    suspend fun actions(): List<ActionEntity>
 | 
			
		||||
 | 
			
		||||
    @Insert(onConflict = OnConflictStrategy.REPLACE)
 | 
			
		||||
    fun insertAllActions(vararg actions: ActionEntity)
 | 
			
		||||
 | 
			
		||||
    @Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1")
 | 
			
		||||
    fun deleteReadActionForArticle(article_id: String)
 | 
			
		||||
 | 
			
		||||
    @Delete
 | 
			
		||||
    fun delete(action: ActionEntity)
 | 
			
		||||
}
 | 
			
		||||
@@ -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")
 | 
			
		||||
    suspend fun items(): List<ItemEntity>
 | 
			
		||||
 | 
			
		||||
    @Insert(onConflict = OnConflictStrategy.REPLACE)
 | 
			
		||||
    suspend fun insertAllItems(vararg items: ItemEntity)
 | 
			
		||||
 | 
			
		||||
    @Query("DELETE FROM items")
 | 
			
		||||
    suspend fun deleteAllItems()
 | 
			
		||||
 | 
			
		||||
    @Delete
 | 
			
		||||
    suspend fun delete(item: ItemEntity)
 | 
			
		||||
 | 
			
		||||
    @Update
 | 
			
		||||
    suspend fun updateItem(item: ItemEntity)
 | 
			
		||||
}
 | 
			
		||||
@@ -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,221 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.settings
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.text.*
 | 
			
		||||
import androidx.preference.EditTextPreference
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import android.view.*
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.core.widget.addTextChangedListener
 | 
			
		||||
import androidx.preference.Preference
 | 
			
		||||
import androidx.preference.PreferenceFragmentCompat
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.databinding.ActivitySettingsBinding
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.Toppings
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import java.lang.NumberFormatException
 | 
			
		||||
 | 
			
		||||
private const val TITLE_TAG = "settingsActivityTitle"
 | 
			
		||||
 | 
			
		||||
class SettingsActivity : AppCompatActivity(),
 | 
			
		||||
        PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("dark_theme", false)) {
 | 
			
		||||
            setTheme(R.style.NoBarDark)
 | 
			
		||||
        }
 | 
			
		||||
        val binding = ActivitySettingsBinding.inflate(layoutInflater)
 | 
			
		||||
 | 
			
		||||
        val scoop = Scoop.getInstance()
 | 
			
		||||
        scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
 | 
			
		||||
        scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
 | 
			
		||||
 | 
			
		||||
        setContentView(binding.root)
 | 
			
		||||
        if (savedInstanceState == null) {
 | 
			
		||||
            supportFragmentManager
 | 
			
		||||
                    .beginTransaction()
 | 
			
		||||
                    .replace(R.id.settings, MainPreferenceFragment())
 | 
			
		||||
                    .commit()
 | 
			
		||||
        } else {
 | 
			
		||||
            title = savedInstanceState.getCharSequence(TITLE_TAG)
 | 
			
		||||
        }
 | 
			
		||||
        supportFragmentManager.addOnBackStackChangedListener {
 | 
			
		||||
            if (supportFragmentManager.backStackEntryCount == 0) {
 | 
			
		||||
                setTitle(R.string.title_activity_settings)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSupportActionBar(binding.toolbar)
 | 
			
		||||
 | 
			
		||||
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
 | 
			
		||||
        supportActionBar?.setDisplayShowHomeEnabled(true)
 | 
			
		||||
        supportActionBar?.title = title
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSaveInstanceState(outState: Bundle) {
 | 
			
		||||
        super.onSaveInstanceState(outState)
 | 
			
		||||
        // Save current activity title so we can set it again after a configuration change
 | 
			
		||||
        outState.putCharSequence(TITLE_TAG, title)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSupportNavigateUp(): Boolean {
 | 
			
		||||
        if (supportFragmentManager.popBackStackImmediate()) {
 | 
			
		||||
            supportActionBar?.title = getText(R.string.title_activity_settings)
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        return super.onSupportNavigateUp()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPreferenceStartFragment(
 | 
			
		||||
            caller: PreferenceFragmentCompat,
 | 
			
		||||
            pref: Preference
 | 
			
		||||
    ): Boolean {
 | 
			
		||||
        // Instantiate the new Fragment
 | 
			
		||||
        val args = pref.extras
 | 
			
		||||
        val fragment = supportFragmentManager.fragmentFactory.instantiate(
 | 
			
		||||
                classLoader,
 | 
			
		||||
                pref.fragment
 | 
			
		||||
        ).apply {
 | 
			
		||||
            arguments = args
 | 
			
		||||
            setTargetFragment(caller, 0)
 | 
			
		||||
        }
 | 
			
		||||
        // Replace the existing Fragment with the new Fragment
 | 
			
		||||
        supportFragmentManager.beginTransaction()
 | 
			
		||||
                .replace(R.id.settings, fragment)
 | 
			
		||||
                .addToBackStack(null)
 | 
			
		||||
                .commit()
 | 
			
		||||
        title = pref.title
 | 
			
		||||
        supportActionBar?.title = title
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class MainPreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_main, rootKey)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class GeneralPreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_general, rootKey)
 | 
			
		||||
 | 
			
		||||
            val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
 | 
			
		||||
            editTextPreference?.setOnBindEditTextListener { editText ->
 | 
			
		||||
                editText.inputType = InputType.TYPE_CLASS_NUMBER
 | 
			
		||||
                editText.filters = arrayOf(
 | 
			
		||||
                        InputFilter { source, _, _, dest, _, _ ->
 | 
			
		||||
                            try {
 | 
			
		||||
                                val input: Int = (dest.toString() + source.toString()).toInt()
 | 
			
		||||
                                if (input in 1..200) return@InputFilter null
 | 
			
		||||
                            } catch (nfe: NumberFormatException) {
 | 
			
		||||
                                Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
 | 
			
		||||
                            }
 | 
			
		||||
                            ""
 | 
			
		||||
                        }
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_viewer, rootKey)
 | 
			
		||||
 | 
			
		||||
            val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
 | 
			
		||||
            fontSize?.setOnBindEditTextListener { editText ->
 | 
			
		||||
                editText.inputType = InputType.TYPE_CLASS_NUMBER
 | 
			
		||||
                editText.addTextChangedListener { object : TextWatcher {
 | 
			
		||||
                    override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
 | 
			
		||||
                    override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
 | 
			
		||||
                    override fun afterTextChanged(editable: Editable) {
 | 
			
		||||
                        try {
 | 
			
		||||
                            editText.textSize = editable.toString().toInt().toFloat()
 | 
			
		||||
                        } catch (e: NumberFormatException) {
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } }
 | 
			
		||||
                editText.filters = arrayOf(
 | 
			
		||||
                        InputFilter { source, _, _, dest, _, _ ->
 | 
			
		||||
                            try {
 | 
			
		||||
                                val input = (dest.toString() + source.toString()).toInt()
 | 
			
		||||
                                if (input > 0) return@InputFilter null
 | 
			
		||||
                            } catch (nfe: NumberFormatException) {
 | 
			
		||||
                            }
 | 
			
		||||
                            ""
 | 
			
		||||
                        }
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class OfflinePreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_offline, rootKey)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class ThemePreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_theme, rootKey)
 | 
			
		||||
            setHasOptionsMenu(true)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
 | 
			
		||||
            super.onCreateOptionsMenu(menu, inflater)
 | 
			
		||||
            inflater.inflate(R.menu.settings_theme, menu)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
            val id = item.itemId
 | 
			
		||||
            if (id == R.id.clear) {
 | 
			
		||||
                val pref = PreferenceManager.getDefaultSharedPreferences(activity)
 | 
			
		||||
                val editor = pref.edit()
 | 
			
		||||
                editor.remove("color_primary")
 | 
			
		||||
                editor.remove("color_primary_dark")
 | 
			
		||||
                editor.remove("color_accent")
 | 
			
		||||
                editor.remove("color_accent_dark")
 | 
			
		||||
                editor.remove("dark_theme")
 | 
			
		||||
                editor.apply()
 | 
			
		||||
                requireActivity().recreate()
 | 
			
		||||
            }
 | 
			
		||||
            return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class LinksPreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        private fun openUrl(uri: Uri?) {
 | 
			
		||||
            val browserIntent = Intent(Intent.ACTION_VIEW, uri)
 | 
			
		||||
            startActivity(browserIntent)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_links, rootKey)
 | 
			
		||||
 | 
			
		||||
            preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
 | 
			
		||||
                openUrl(Uri.parse(Config.trackerUrl))
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
 | 
			
		||||
                openUrl(Uri.parse(Config.sourceUrl))
 | 
			
		||||
                false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
 | 
			
		||||
                openUrl(Uri.parse(Config.translationUrl))
 | 
			
		||||
                false
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class ExperimentalPreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_experimental, rootKey)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.themes
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import androidx.annotation.ColorInt
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
 | 
			
		||||
class AppColors(a: Activity) {
 | 
			
		||||
 | 
			
		||||
    @ColorInt val colorPrimary: Int
 | 
			
		||||
    @ColorInt val colorPrimaryDark: Int
 | 
			
		||||
    @ColorInt val colorAccent: Int
 | 
			
		||||
    @ColorInt val colorAccentDark: Int
 | 
			
		||||
    @ColorInt val colorBackground: Int
 | 
			
		||||
    @ColorInt val textColor: Int
 | 
			
		||||
    val isDarkTheme: Boolean
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val sharedPref = PreferenceManager.getDefaultSharedPreferences(a)
 | 
			
		||||
 | 
			
		||||
        colorPrimary =
 | 
			
		||||
                sharedPref.getInt(
 | 
			
		||||
                    "color_primary",
 | 
			
		||||
                    a.resources.getColor(R.color.colorPrimary)
 | 
			
		||||
                )
 | 
			
		||||
        colorPrimaryDark =
 | 
			
		||||
                sharedPref.getInt(
 | 
			
		||||
                    "color_primary_dark",
 | 
			
		||||
                    a.resources.getColor(R.color.colorPrimaryDark)
 | 
			
		||||
                )
 | 
			
		||||
        colorAccent =
 | 
			
		||||
                sharedPref.getInt(
 | 
			
		||||
                    "color_accent",
 | 
			
		||||
                    a.resources.getColor(R.color.colorAccent)
 | 
			
		||||
                )
 | 
			
		||||
        colorAccentDark =
 | 
			
		||||
                sharedPref.getInt(
 | 
			
		||||
                    "color_accent_dark",
 | 
			
		||||
                    a.resources.getColor(R.color.colorAccentDark)
 | 
			
		||||
                )
 | 
			
		||||
        isDarkTheme =
 | 
			
		||||
                sharedPref.getBoolean(
 | 
			
		||||
                    "dark_theme",
 | 
			
		||||
                    false
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        colorBackground = if (isDarkTheme) {
 | 
			
		||||
            a.setTheme(R.style.NoBarDark)
 | 
			
		||||
            R.color.darkBackground
 | 
			
		||||
        } else {
 | 
			
		||||
            a.setTheme(R.style.NoBar)
 | 
			
		||||
            android.R.color.background_light
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        textColor = if (isDarkTheme) {
 | 
			
		||||
            R.color.white
 | 
			
		||||
        } else {
 | 
			
		||||
            R.color.grey_900
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,65 @@
 | 
			
		||||
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"
 | 
			
		||||
 | 
			
		||||
        var apiVersion = 0
 | 
			
		||||
 | 
			
		||||
        fun logoutAndRedirect(
 | 
			
		||||
            c: Context,
 | 
			
		||||
            callingActivity: Activity,
 | 
			
		||||
            editor: SharedPreferences.Editor,
 | 
			
		||||
            baseUrlFail: Boolean = false
 | 
			
		||||
        ): Boolean {
 | 
			
		||||
            editor.remove("url")
 | 
			
		||||
            editor.remove("login")
 | 
			
		||||
            editor.remove("password")
 | 
			
		||||
            editor.remove("apiVersionMajor")
 | 
			
		||||
            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,31 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.text.format.DateUtils
 | 
			
		||||
import java.time.Instant
 | 
			
		||||
import java.time.LocalDateTime
 | 
			
		||||
import java.time.OffsetDateTime
 | 
			
		||||
import java.time.ZoneOffset
 | 
			
		||||
import java.time.format.DateTimeFormatter
 | 
			
		||||
 | 
			
		||||
fun parseDate(dateString: String): Instant {
 | 
			
		||||
 | 
			
		||||
    val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
 | 
			
		||||
 | 
			
		||||
    return if (Config.apiVersion >= 4) {
 | 
			
		||||
        OffsetDateTime.parse(dateString).toInstant()
 | 
			
		||||
    } else {
 | 
			
		||||
        LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun parseRelativeDate(dateString: String): String {
 | 
			
		||||
 | 
			
		||||
    val date = parseDate(dateString)
 | 
			
		||||
 | 
			
		||||
    return " " + DateUtils.getRelativeTimeSpanString(
 | 
			
		||||
            date.toEpochMilli(),
 | 
			
		||||
            Instant.now().toEpochMilli(),
 | 
			
		||||
            DateUtils.MINUTE_IN_MILLIS,
 | 
			
		||||
            DateUtils.FORMAT_ABBREV_RELATIVE
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -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,36 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
 | 
			
		||||
 | 
			
		||||
fun String.toTextDrawableString(c: Context): String {
 | 
			
		||||
    val textDrawable = StringBuilder()
 | 
			
		||||
    for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) {
 | 
			
		||||
        try {
 | 
			
		||||
            textDrawable.append(s[0])
 | 
			
		||||
        } catch (e: StringIndexOutOfBoundsException) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return textDrawable.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Item.sourceAndDateText(): String {
 | 
			
		||||
    val formattedDate = parseRelativeDate(this.datetime)
 | 
			
		||||
 | 
			
		||||
    return this.getSourceTitle() + formattedDate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Item.toggleStar(): Item {
 | 
			
		||||
    this.starred = !this.starred
 | 
			
		||||
    return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun List<Item>.flattenTags(): List<Item> =
 | 
			
		||||
    this.flatMap {
 | 
			
		||||
        val item = it
 | 
			
		||||
        val tags: List<String> = it.tags.tags.split(",")
 | 
			
		||||
        tags.map { t ->
 | 
			
		||||
            item.copy(tags = SelfossTagType(t.trim()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -0,0 +1,203 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.ActivityNotFoundException
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.BitmapFactory
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.text.Spannable
 | 
			
		||||
import android.text.style.ClickableSpan
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsIntent
 | 
			
		||||
import android.util.Patterns
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.ReaderActivity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import okhttp3.HttpUrl
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 | 
			
		||||
 | 
			
		||||
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
 | 
			
		||||
 | 
			
		||||
    val actionIntent = Intent(Intent.ACTION_SEND)
 | 
			
		||||
    actionIntent.type = "text/plain"
 | 
			
		||||
    val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
			
		||||
        PendingIntent.FLAG_IMMUTABLE
 | 
			
		||||
    } else {
 | 
			
		||||
        0
 | 
			
		||||
    }
 | 
			
		||||
    val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
 | 
			
		||||
            this,
 | 
			
		||||
            0,
 | 
			
		||||
            actionIntent,
 | 
			
		||||
            pflags
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    val intentBuilder = CustomTabsIntent.Builder()
 | 
			
		||||
 | 
			
		||||
    // TODO: change to primary when it's possible to customize custom tabs title color
 | 
			
		||||
    //intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary));
 | 
			
		||||
    intentBuilder.setToolbarColor(resources.getColor(R.color.colorAccentDark))
 | 
			
		||||
    intentBuilder.setShowTitle(true)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    intentBuilder.setStartAnimations(
 | 
			
		||||
        this,
 | 
			
		||||
        R.anim.slide_in_right,
 | 
			
		||||
        R.anim.slide_out_left
 | 
			
		||||
    )
 | 
			
		||||
    intentBuilder.setExitAnimations(
 | 
			
		||||
        this,
 | 
			
		||||
        android.R.anim.slide_in_left,
 | 
			
		||||
        android.R.anim.slide_out_right
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val closeicon = BitmapFactory.decodeResource(resources, R.drawable.ic_close_white_24dp)
 | 
			
		||||
    intentBuilder.setCloseButtonIcon(closeicon)
 | 
			
		||||
 | 
			
		||||
    val shareLabel = this.getString(R.string.label_share)
 | 
			
		||||
    val icon = BitmapFactory.decodeResource(
 | 
			
		||||
        resources,
 | 
			
		||||
        R.drawable.ic_share_white_24dp
 | 
			
		||||
    )
 | 
			
		||||
    intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent)
 | 
			
		||||
 | 
			
		||||
    return intentBuilder.build()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Context.openItemUrlInternally(
 | 
			
		||||
    allItems: ArrayList<Item>,
 | 
			
		||||
    currentItem: Int,
 | 
			
		||||
    linkDecoded: String,
 | 
			
		||||
    customTabsIntent: CustomTabsIntent,
 | 
			
		||||
    articleViewer: Boolean,
 | 
			
		||||
    app: Activity
 | 
			
		||||
) {
 | 
			
		||||
    if (articleViewer) {
 | 
			
		||||
        ReaderActivity.allItems = allItems
 | 
			
		||||
        SharedItems.position = currentItem
 | 
			
		||||
        val intent = Intent(this, ReaderActivity::class.java)
 | 
			
		||||
        intent.putExtra("currentItem", currentItem)
 | 
			
		||||
        app.startActivity(intent)
 | 
			
		||||
    } else {
 | 
			
		||||
        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 =
 | 
			
		||||
    this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
 | 
			
		||||
 | 
			
		||||
fun String.isBaseUrlValid(ctx: Context): Boolean {
 | 
			
		||||
    val baseUrl = this.toHttpUrlOrNull()
 | 
			
		||||
    var existsAndEndsWithSlash = false
 | 
			
		||||
    if (baseUrl != null) {
 | 
			
		||||
        val pathSegments = baseUrl.pathSegments
 | 
			
		||||
        existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Context.openInBrowserAsNewTask(i: Item) {
 | 
			
		||||
    val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
    intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp())
 | 
			
		||||
    startActivity(intent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class LinkOnTouchListener: View.OnTouchListener {
 | 
			
		||||
    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
 | 
			
		||||
        var ret = false
 | 
			
		||||
        val widget: TextView = v as TextView
 | 
			
		||||
        val text: CharSequence = widget.text
 | 
			
		||||
        val stext = Spannable.Factory.getInstance().newSpannable(text)
 | 
			
		||||
 | 
			
		||||
        val action = event!!.action
 | 
			
		||||
 | 
			
		||||
        if (action == MotionEvent.ACTION_UP ||
 | 
			
		||||
            action == MotionEvent.ACTION_DOWN) {
 | 
			
		||||
            var x: Float = event.x
 | 
			
		||||
            var y: Float = event.y
 | 
			
		||||
 | 
			
		||||
            x -= widget.totalPaddingLeft
 | 
			
		||||
            y -= widget.totalPaddingTop
 | 
			
		||||
 | 
			
		||||
            x += widget.scrollX
 | 
			
		||||
            y += widget.scrollY
 | 
			
		||||
 | 
			
		||||
            val layout = widget.layout
 | 
			
		||||
            val line = layout.getLineForVertical(y.toInt())
 | 
			
		||||
            val off = layout.getOffsetForHorizontal(line, x)
 | 
			
		||||
 | 
			
		||||
            val link = stext.getSpans(off, off, ClickableSpan::class.java)
 | 
			
		||||
 | 
			
		||||
            if (link.isNotEmpty()) {
 | 
			
		||||
                if (action == MotionEvent.ACTION_UP) {
 | 
			
		||||
                    link[0].onClick(widget)
 | 
			
		||||
                }
 | 
			
		||||
                ret = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,404 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.persistence.toView
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
* Singleton class that contains the articles fetched from Selfoss, it allows sharing the items list
 | 
			
		||||
* between Activities and Fragments
 | 
			
		||||
*/
 | 
			
		||||
object SharedItems {
 | 
			
		||||
    var items: ArrayList<Item> = arrayListOf<Item>()
 | 
			
		||||
        get() {
 | 
			
		||||
            return ArrayList(field)
 | 
			
		||||
        }
 | 
			
		||||
        set(value) {
 | 
			
		||||
            field = ArrayList(value)
 | 
			
		||||
        }
 | 
			
		||||
    var focusedItems: ArrayList<Item> = arrayListOf<Item>()
 | 
			
		||||
        get() {
 | 
			
		||||
            return ArrayList(field)
 | 
			
		||||
        }
 | 
			
		||||
        set(value) {
 | 
			
		||||
            field = ArrayList(value)
 | 
			
		||||
        }
 | 
			
		||||
    var position = 0
 | 
			
		||||
        set(value) {
 | 
			
		||||
            field = when {
 | 
			
		||||
                value < 0 -> 0
 | 
			
		||||
                value > items.size -> items.size
 | 
			
		||||
                else -> value
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    var displayedItems: String = "unread"
 | 
			
		||||
        set(value) {
 | 
			
		||||
            field = when (value) {
 | 
			
		||||
                "all" -> "all"
 | 
			
		||||
                "unread" -> "unread"
 | 
			
		||||
                "read" -> "read"
 | 
			
		||||
                "starred" -> "starred"
 | 
			
		||||
                else -> "all"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    var searchFilter: String? = null
 | 
			
		||||
    var sourceIDFilter: Long? = null
 | 
			
		||||
    var sourceFilter: String? = null
 | 
			
		||||
    var tagFilter: String? = null
 | 
			
		||||
    var itemsCaching = false
 | 
			
		||||
 | 
			
		||||
    var fetchedUnread = false
 | 
			
		||||
    var fetchedAll = false
 | 
			
		||||
    var fetchedStarred = false
 | 
			
		||||
 | 
			
		||||
    var badgeUnread = -1
 | 
			
		||||
    var badgeAll = -1
 | 
			
		||||
    var badgeStarred = -1
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add new items to the SharedItems list
 | 
			
		||||
     *
 | 
			
		||||
     * The new items are considered more updated than the ones already in the list.
 | 
			
		||||
     * The old items present in the new list are discarded and replaced by the new ones.
 | 
			
		||||
     * Items are compared according to the selfoss id, which should always be unique.
 | 
			
		||||
     */
 | 
			
		||||
    fun appendNewItems(newItems: ArrayList<Item>) {
 | 
			
		||||
        var tmpItems = items
 | 
			
		||||
        if (tmpItems != newItems) {
 | 
			
		||||
            tmpItems = tmpItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList<Item>
 | 
			
		||||
            tmpItems.addAll(newItems)
 | 
			
		||||
            items = tmpItems
 | 
			
		||||
 | 
			
		||||
            sortItems()
 | 
			
		||||
            getFocusedItems()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun refreshFocusedItems(newItems: ArrayList<Item>) {
 | 
			
		||||
        val tmpItems = items
 | 
			
		||||
        tmpItems.removeAll(focusedItems)
 | 
			
		||||
 | 
			
		||||
        appendNewItems(newItems)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun clearDBItems(db: AppDatabase) {
 | 
			
		||||
        db.itemsDao().deleteAllItems()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun updateDatabase(db: AppDatabase) {
 | 
			
		||||
        if (itemsCaching) {
 | 
			
		||||
            if (items.isEmpty()) {
 | 
			
		||||
                getFromDB(db)
 | 
			
		||||
            }
 | 
			
		||||
            db.itemsDao().deleteAllItems()
 | 
			
		||||
            db.itemsDao().insertAllItems(*(items.map { it.toEntity() }).toTypedArray())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun filter() {
 | 
			
		||||
        fun filterSearch(item: Item): Boolean {
 | 
			
		||||
            return if (!searchFilter.isEmptyOrNullOrNullString()) {
 | 
			
		||||
                var matched = item.title.contains(searchFilter.toString(), true)
 | 
			
		||||
                matched = matched || item.content.contains(searchFilter.toString(), true)
 | 
			
		||||
                matched = matched || item.sourcetitle.contains(searchFilter.toString(), true)
 | 
			
		||||
                matched
 | 
			
		||||
            } else {
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var tmpItems = focusedItems
 | 
			
		||||
        if (tagFilter != null) {
 | 
			
		||||
            tmpItems = tmpItems.filter { it.tags.tags.contains(tagFilter.toString()) } as ArrayList<Item>
 | 
			
		||||
        }
 | 
			
		||||
        if (searchFilter != null) {
 | 
			
		||||
            tmpItems = tmpItems.filter { filterSearch(it) } as ArrayList<Item>
 | 
			
		||||
        }
 | 
			
		||||
        if (sourceFilter != null) {
 | 
			
		||||
            tmpItems = tmpItems.filter { it.sourcetitle == sourceFilter } as ArrayList<Item>
 | 
			
		||||
        }
 | 
			
		||||
        focusedItems = tmpItems
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getFocusedItems() {
 | 
			
		||||
        when (displayedItems) {
 | 
			
		||||
            "all" -> getAll()
 | 
			
		||||
            "unread" -> getUnRead()
 | 
			
		||||
            "read" -> getRead()
 | 
			
		||||
            "starred" -> getStarred()
 | 
			
		||||
            else -> getUnRead()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getUnRead() {
 | 
			
		||||
        displayedItems = "unread"
 | 
			
		||||
        focusedItems = items.filter { item -> item.unread } as ArrayList<Item>
 | 
			
		||||
        filter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getRead() {
 | 
			
		||||
        displayedItems = "read"
 | 
			
		||||
        focusedItems = items.filter { item -> !item.unread } as ArrayList<Item>
 | 
			
		||||
        filter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getStarred() {
 | 
			
		||||
        displayedItems = "starred"
 | 
			
		||||
        focusedItems = items.filter { item -> item.starred } as ArrayList<Item>
 | 
			
		||||
        filter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getAll() {
 | 
			
		||||
        displayedItems = "all"
 | 
			
		||||
        focusedItems = items
 | 
			
		||||
        filter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getFromDB(db: AppDatabase) {
 | 
			
		||||
        if (itemsCaching) {
 | 
			
		||||
                    val dbItems = db.itemsDao().items().map { it.toView() } as ArrayList<Item>
 | 
			
		||||
                    appendNewItems(dbItems)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun removeItemAtIndex(index: Int) {
 | 
			
		||||
        val i = focusedItems[index]
 | 
			
		||||
        val tmpItems = focusedItems
 | 
			
		||||
        tmpItems.remove(i)
 | 
			
		||||
        focusedItems = tmpItems
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addItemAtIndex(newItem: Item, index: Int) {
 | 
			
		||||
        val tmpItems = focusedItems
 | 
			
		||||
        tmpItems.add(index, newItem)
 | 
			
		||||
        focusedItems = tmpItems
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun readItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
 | 
			
		||||
        if (items.contains(item)) {
 | 
			
		||||
            position = items.indexOf(item)
 | 
			
		||||
            readItemAtPosition(app, api, db)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun readItems(db: AppDatabase, ids: List<String>) {
 | 
			
		||||
        for (id in ids) {
 | 
			
		||||
            val match = items.filter { it -> it.id == id }
 | 
			
		||||
            if (match.isNotEmpty() && match.size == 1) {
 | 
			
		||||
                position = items.indexOf(match[0])
 | 
			
		||||
                val tmpItems = items
 | 
			
		||||
                tmpItems[position].unread = false
 | 
			
		||||
                items = tmpItems
 | 
			
		||||
                resetDBItem(db)
 | 
			
		||||
                badgeUnread--
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun readItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
 | 
			
		||||
        val i = items[position]
 | 
			
		||||
 | 
			
		||||
        if (app.isNetworkAccessible(null)) {
 | 
			
		||||
            api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                        call: Call<SuccessResponse>,
 | 
			
		||||
                        response: Response<SuccessResponse>
 | 
			
		||||
                ) {
 | 
			
		||||
 | 
			
		||||
                    val tmpItems = items
 | 
			
		||||
                    tmpItems[position].unread = false
 | 
			
		||||
                    items = tmpItems
 | 
			
		||||
 | 
			
		||||
                    resetDBItem(db)
 | 
			
		||||
                    getFocusedItems()
 | 
			
		||||
                    badgeUnread--
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                            app,
 | 
			
		||||
                            app.getString(R.string.cant_mark_read),
 | 
			
		||||
                            Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        } else if (itemsCaching) {
 | 
			
		||||
            thread {
 | 
			
		||||
                db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (position > items.size) {
 | 
			
		||||
            position -= 1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun unreadItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
 | 
			
		||||
        if (items.contains(item) && !item.unread) {
 | 
			
		||||
            position = items.indexOf(item)
 | 
			
		||||
            unreadItemAtPosition(app, api, db)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun unreadItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
 | 
			
		||||
        val i = items[position]
 | 
			
		||||
 | 
			
		||||
        if (app.isNetworkAccessible(null)) {
 | 
			
		||||
            api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                    call: Call<SuccessResponse>,
 | 
			
		||||
                    response: Response<SuccessResponse>
 | 
			
		||||
                ) {
 | 
			
		||||
 | 
			
		||||
                    val tmpItems = items
 | 
			
		||||
                    tmpItems[position].unread = true
 | 
			
		||||
                    items = tmpItems
 | 
			
		||||
 | 
			
		||||
                    resetDBItem(db)
 | 
			
		||||
                    getFocusedItems()
 | 
			
		||||
                    badgeUnread++
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        app,
 | 
			
		||||
                        app.getString(R.string.cant_mark_unread),
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        } else if (itemsCaching) {
 | 
			
		||||
            thread {
 | 
			
		||||
                db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun starItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
 | 
			
		||||
        if (items.contains(item) && !item.starred) {
 | 
			
		||||
            position = items.indexOf(item)
 | 
			
		||||
            starItemAtPosition(app, api, db)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun starItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
 | 
			
		||||
        val i = items[position]
 | 
			
		||||
 | 
			
		||||
        if (app.isNetworkAccessible(null)) {
 | 
			
		||||
            api.starrItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                    call: Call<SuccessResponse>,
 | 
			
		||||
                    response: Response<SuccessResponse>
 | 
			
		||||
                ) {
 | 
			
		||||
                    val tmpItems = items
 | 
			
		||||
                    tmpItems[position].starred = true
 | 
			
		||||
                    items = tmpItems
 | 
			
		||||
 | 
			
		||||
                    resetDBItem(db)
 | 
			
		||||
                    getFocusedItems()
 | 
			
		||||
                    badgeStarred++
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(
 | 
			
		||||
                    call: Call<SuccessResponse>,
 | 
			
		||||
                    t: Throwable
 | 
			
		||||
                ) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        app,
 | 
			
		||||
                        app.getString(R.string.cant_mark_favortie),
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            thread {
 | 
			
		||||
                db.actionsDao().insertAllActions(ActionEntity(i.id, false, false, true, false))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun unstarItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
 | 
			
		||||
        if (items.contains(item) && item.starred) {
 | 
			
		||||
            position = items.indexOf(item)
 | 
			
		||||
            unstarItemAtPosition(app, api, db)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun unstarItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
 | 
			
		||||
        val i = items[position]
 | 
			
		||||
 | 
			
		||||
        if (app.isNetworkAccessible(null)) {
 | 
			
		||||
            api.unstarrItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                    call: Call<SuccessResponse>,
 | 
			
		||||
                    response: Response<SuccessResponse>
 | 
			
		||||
                ) {
 | 
			
		||||
                    val tmpItems = items
 | 
			
		||||
                    tmpItems[position].starred = false
 | 
			
		||||
                    items = tmpItems
 | 
			
		||||
 | 
			
		||||
                    resetDBItem(db)
 | 
			
		||||
                    getFocusedItems()
 | 
			
		||||
                    badgeStarred--
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(
 | 
			
		||||
                    call: Call<SuccessResponse>,
 | 
			
		||||
                    t: Throwable
 | 
			
		||||
                ) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        app,
 | 
			
		||||
                        app.getString(R.string.cant_unmark_favortie),
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            thread {
 | 
			
		||||
                db.actionsDao().insertAllActions(ActionEntity(i.id, false, false, false, true))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun resetDBItem(db: AppDatabase) {
 | 
			
		||||
        if (itemsCaching) {
 | 
			
		||||
            val i = items[position]
 | 
			
		||||
            CoroutineScope(Dispatchers.IO).launch {
 | 
			
		||||
                db.itemsDao().delete(i.toEntity())
 | 
			
		||||
                db.itemsDao().insertAllItems(i.toEntity())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun unreadItemStatusAtIndex(position: Int): Boolean {
 | 
			
		||||
        return focusedItems[position].unread
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun computeBadges() {
 | 
			
		||||
        badgeUnread = items.filter { item -> item.unread }.size
 | 
			
		||||
        badgeStarred = items.filter { item -> item.starred }.size
 | 
			
		||||
        badgeAll = items.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun sortItems() {
 | 
			
		||||
        val tmpItems = ArrayList(items.sortedByDescending { parseDate(it.datetime) })
 | 
			
		||||
        items = tmpItems
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,64 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.network
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.net.ConnectivityManager
 | 
			
		||||
import android.net.NetworkCapabilities
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
 | 
			
		||||
var snackBarShown = false
 | 
			
		||||
var view: View? = null
 | 
			
		||||
lateinit var s: Snackbar
 | 
			
		||||
 | 
			
		||||
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean {
 | 
			
		||||
    val networkIsAccessible = isNetworkAvailable(this)
 | 
			
		||||
 | 
			
		||||
    if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
 | 
			
		||||
        view = v
 | 
			
		||||
        s = Snackbar
 | 
			
		||||
            .make(
 | 
			
		||||
                v,
 | 
			
		||||
                R.string.no_network_connectivity,
 | 
			
		||||
                Snackbar.LENGTH_INDEFINITE
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        s.setAction(android.R.string.ok) {
 | 
			
		||||
            snackBarShown = false
 | 
			
		||||
            s.dismiss()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val view = s.view
 | 
			
		||||
        val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
 | 
			
		||||
        tv.setTextColor(Color.WHITE)
 | 
			
		||||
        s.show()
 | 
			
		||||
        snackBarShown = true
 | 
			
		||||
    }
 | 
			
		||||
    if (snackBarShown && networkIsAccessible && !overrideOffline) {
 | 
			
		||||
        s.dismiss()
 | 
			
		||||
    }
 | 
			
		||||
    return if(overrideOffline) overrideOffline else networkIsAccessible
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun isNetworkAvailable(context: Context): Boolean {
 | 
			
		||||
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 | 
			
		||||
 | 
			
		||||
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
			
		||||
         val network = connectivityManager.activeNetwork ?: return false
 | 
			
		||||
         val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
 | 
			
		||||
 | 
			
		||||
         return when {
 | 
			
		||||
             networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
 | 
			
		||||
             networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
 | 
			
		||||
             networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
 | 
			
		||||
             networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
 | 
			
		||||
             else -> false
 | 
			
		||||
         }
 | 
			
		||||
    } else {
 | 
			
		||||
        val network = connectivityManager.activeNetworkInfo ?: return false
 | 
			
		||||
         return network.isConnectedOrConnecting
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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>
 | 
			
		||||
							
								
								
									
										8
									
								
								app/src/main/res/color/ic_menu_heart_color.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/src/main/res/color/ic_menu_heart_color.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <item android:state_selected="true"
 | 
			
		||||
        android:color="@color/red"/>
 | 
			
		||||
 | 
			
		||||
    <item android:state_selected="false"
 | 
			
		||||
        android:color="?android:attr/textColorPrimary" />
 | 
			
		||||
</selector>
 | 
			
		||||
@@ -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>
 | 
			
		||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_baseline_white_eye_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_baseline_white_eye_24dp.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
<vector android:height="24dp" android:tint="#FFFFFF"
 | 
			
		||||
    android:viewportHeight="24" android:viewportWidth="24"
 | 
			
		||||
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										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_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>
 | 
			
		||||
@@ -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_heart_60dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_menu_heart_60dp.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
<vector android:height="54.751434dp" android:viewportHeight="18.756023"
 | 
			
		||||
    android:viewportWidth="20.554007" android:width="60dp" xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <path android:fillColor="#FFFFFFFF"
 | 
			
		||||
        android:pathData="m5.7968,14.6109c-2.7907,-2.7367 -4.4957,-4.7131 -5.018,-5.8165 -2.102,-4.4408 0.2424,-8.7943 4.7357,-8.7943 1.635,0 2.7056,0.425 3.9688,1.5755l0.7937,0.723 0.7937,-0.723c1.2631,-1.1505 2.3337,-1.5755 3.9688,-1.5755 4.4933,0 6.8377,4.3535 4.7357,8.7943 -0.5223,1.1035 -2.2274,3.0799 -5.018,5.8165 -2.3248,2.2798 -4.3409,4.1451 -4.4802,4.1451 -0.1393,0 -2.1554,-1.8653 -4.4802,-4.1451z" android:strokeWidth="0.0933392"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										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>
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user