├── .gitignore ├── LICENSE ├── README.md ├── pom.xml ├── screenshots ├── grid_article_dark.png ├── grid_article_light.png ├── grid_article_pocket_dark.png ├── grid_dark.png ├── inoreader_login_light.png ├── list_dark.png ├── list_light.png ├── list_mobilizer_light.png └── settings_light.png └── src └── main ├── deploy ├── bin │ ├── desktopintegration │ └── generate_appimage.sh ├── icons │ ├── 128x128 │ │ └── apps │ │ │ └── lecter.png │ ├── 16x16 │ │ └── apps │ │ │ └── lecter.png │ ├── 24x24 │ │ └── apps │ │ │ └── lecter.png │ ├── 256x256 │ │ └── apps │ │ │ └── lecter.png │ ├── 32x32 │ │ └── apps │ │ │ └── lecter.png │ ├── 48x48 │ │ └── apps │ │ │ └── lecter.png │ └── 64x64 │ │ └── apps │ │ └── lecter.png └── package │ ├── appimage │ └── lecter.desktop │ ├── linux │ ├── lecter.appdata.xml │ ├── lecter.desktop │ └── lecter.png │ └── windows │ ├── lecter-setup-icon_dis.bmp │ └── lecter.ico ├── java └── me │ └── bayang │ └── reader │ ├── FXMain.java │ ├── backend │ ├── UserInfo.java │ └── inoreader │ │ ├── ConnectServer.java │ │ └── FolderFeedOrder.java │ ├── components │ ├── DeletableLabel.java │ ├── ItemGridCell.java │ └── ItemListCell.java │ ├── config │ ├── AppConfig.java │ └── AppConfigurationDecoder.java │ ├── controllers │ ├── AboutPopupController.java │ ├── AddSubscriptionController.java │ ├── EditSubscriptionController.java │ ├── OauthController.java │ ├── PocketOauthController.java │ ├── PopupWebViewController.java │ ├── RssController.java │ ├── SettingsController.java │ └── ShareLinkController.java │ ├── mobilizer │ ├── MercuryMobilizer.java │ └── MercuryResult.java │ ├── rssmodels │ ├── AddResult.java │ ├── Alternate.java │ ├── Canonical.java │ ├── Categories.java │ ├── Feed.java │ ├── FoldersTagsList.java │ ├── Item.java │ ├── ItemId.java │ ├── ItemRef.java │ ├── Origin.java │ ├── Self.java │ ├── StreamContent.java │ ├── Subscription.java │ ├── SubscriptionsList.java │ ├── Summary.java │ ├── Tag.java │ ├── UnreadCounter.java │ ├── UnreadCounts.java │ └── UserInformation.java │ ├── share │ ├── Provider.java │ ├── pocket │ │ ├── PocketAccessTokenPayload.java │ │ ├── PocketAccessTokenResponse.java │ │ ├── PocketAddLinkPayload.java │ │ ├── PocketClient.java │ │ ├── PocketTokenRequestPayload.java │ │ └── PocketTokenResponse.java │ └── wallabag │ │ ├── WallabagClient.java │ │ └── WallabagCredentials.java │ ├── storage │ ├── FileConfigStorageServiceImpl.java │ ├── IStorageService.java │ └── PropertiesStorageServiceImpl.java │ ├── utils │ ├── AggregatedObservableArrayList.java │ ├── Filters.java │ ├── StringUtils.java │ └── Theme.java │ └── view │ ├── AboutPopupView.java │ ├── AddSubscriptionView.java │ ├── EditSubscriptionView.java │ ├── NoOpSplashScreen.java │ ├── OauthView.java │ ├── PocketOauthView.java │ ├── PopupWebView.java │ ├── RssView.java │ ├── SettingsView.java │ ├── ShareLinkView.java │ └── SpinnerSplashScreen.java └── resources ├── AddSubscriptionPanel.fxml ├── LoginPanel.fxml ├── UI.fxml ├── application.properties ├── banner.txt ├── css ├── application-dark-orange.css └── application.css ├── file-appender.xml ├── fxml ├── AboutPopup.fxml ├── EditSubscriptionDialog.fxml ├── ItemGridCell.fxml ├── ItemListCell.fxml ├── OauthPopUp.fxml ├── PocketOauthPopUp.fxml ├── PopupWebView.fxml ├── SettingsView.fxml └── ShareLinkPopup.fxml ├── i18n ├── translations.properties └── translations_fr.properties ├── icon.png ├── lecter_256.png ├── libs └── api-wrapper-0.0.1.jar ├── logback-spring.xml └── wallabag_logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | 4 | # Ignore Gradle GUI config 5 | gradle-app.setting 6 | 7 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 8 | !gradle-wrapper.jar 9 | 10 | # Cache of project 11 | .gradletasknamecache 12 | 13 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 14 | # gradle/wrapper/gradle-wrapper.properties 15 | 16 | 17 | *.class 18 | 19 | # BlueJ files 20 | *.ctxt 21 | 22 | # Mobile Tools for Java (J2ME) 23 | .mtj.tmp/ 24 | 25 | # Package Files # 26 | 27 | *.war 28 | *.ear 29 | 30 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 31 | hs_err_pid* 32 | 33 | 34 | 35 | # OS generated files # 36 | ###################### 37 | .DS_Store 38 | .DS_Store? 39 | ._* 40 | .Spotlight-V100 41 | .Trashes 42 | ehthumbs.db 43 | Thumbs.db 44 | 45 | 46 | # other generated things # 47 | output.txt 48 | UserInfo.dat 49 | TokenInfo.dat 50 | .idea/ 51 | streamContent.txt 52 | src/test/ 53 | config.properties 54 | user.properties 55 | token.properties 56 | Output.json 57 | **/deploy/reader_config/ 58 | **/deploy/reader_config/* 59 | **/reader_config/ 60 | icon_old.png 61 | *.streamContent.txt 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | MIT License 4 | 5 | Copyright (c) 2018 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lecter 2 | 3 | An Inoreader Desktop App 4 | Still in early stage. 5 | 6 | The app is destined to become a client for online aggregators like Inoreader, Commafeed etc... 7 | 8 | 9 | ## Layout 10 | 11 | Two layouts available : 12 | 13 | * three pane view 14 | 15 | 16 | * grid view 17 | 18 | 19 | And two themes for the moment a light one and a dark one. 20 | 21 | 22 | ## Content 23 | 24 | For each article there are three contents : 25 | 26 | * the rss feed content 27 | 28 | * the web article (from the original website) 29 | 30 | * the cleaned up/mobile content (provided by The Mercury Web Parser https://mercury.postlight.com/) 31 | 32 | The app supports internationalization, currently english and french are provided. 33 | 34 | Translations are welcome ! 35 | 36 | 37 | ## Services 38 | 39 | Concerning Inoreader, Wallabag and Pocket, authentification and login are performed through Oauth2, so this app does **NOT** know and does **NOT** keep any of your credentials. 40 | 41 | We respect your privacy. 42 | 43 | The articles can be shared to Pocket and Wallabag directly from lecter. 44 | 45 | Support for Instapaper is planned. 46 | 47 | 48 | ## Screenshots 49 | 50 | 51 | You can see the different themes and layouts below : 52 | 53 | 54 | ![grid view dark theme](screenshots/grid_dark.png) 55 | 56 | ![grid view dark theme with article](screenshots/grid_article_dark.png) 57 | 58 | ![grid view dark theme with article and pocket window](screenshots/grid_article_pocket_dark.png) 59 | 60 | ![list view dark theme with article](screenshots/list_dark.png) 61 | 62 | ![list view light theme with article](screenshots/list_light.png) 63 | 64 | ![list view light theme with mobile content article](screenshots/list_mobilizer_light.png) 65 | 66 | ![grid view light theme](screenshots/grid_article_light.png) 67 | 68 | ![settings light theme](screenshots/settings_light.png) 69 | 70 | ![inoreader login view light theme](screenshots/inoreader_login_light.png) 71 | 72 | 73 | ## Installation 74 | 75 | At the moment we provide a .deb package, an appimage and a windows exe. 76 | 77 | An appimage is a standalone, no installation, package which is supposed to run seamlessly on all Linux platforms. 78 | 79 | To use it just set executable permission to the .appimage file and double-click/execute. 80 | 81 | You will be prompted to see whether you want desktop integration or not. 82 | 83 | Building .dmg should be possible once I find a build platform. 84 | 85 | 86 | ## Currently 87 | 88 | Currently being rewritten after being forked from 89 | 90 | A renaming occured to reflect this and the app now lives its own life. 91 | 92 | 93 | ## Why the supid name ? 94 | 95 | Well all RSS apps are called Feed or Reader or a combo of both. 96 | 97 | In french the word reader is translated as lecteur and lecteur has the same pronounciation as lecter. 98 | 99 | -------------------------------------------------------------------------------- /screenshots/grid_article_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/grid_article_dark.png -------------------------------------------------------------------------------- /screenshots/grid_article_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/grid_article_light.png -------------------------------------------------------------------------------- /screenshots/grid_article_pocket_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/grid_article_pocket_dark.png -------------------------------------------------------------------------------- /screenshots/grid_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/grid_dark.png -------------------------------------------------------------------------------- /screenshots/inoreader_login_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/inoreader_login_light.png -------------------------------------------------------------------------------- /screenshots/list_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/list_dark.png -------------------------------------------------------------------------------- /screenshots/list_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/list_light.png -------------------------------------------------------------------------------- /screenshots/list_mobilizer_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/list_mobilizer_light.png -------------------------------------------------------------------------------- /screenshots/settings_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/screenshots/settings_light.png -------------------------------------------------------------------------------- /src/main/deploy/bin/generate_appimage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "in post build script $(pwd)" 4 | VERSION="$1" 5 | 6 | #cat <> ../jfx/native/lecter/app/lecter.cfg 7 | #[JVMOptions] 8 | #-Xmx1g 9 | #-Djava.net.preferIPv4Stack=true 10 | #-Dspring.config.location=classpath:application.properties,classpath:/reader_config/reader.properties,file:\${user.home}/.config/lecter/config.properties 11 | #-Dlecter.log.dir=\${user.home}/.config/lecter/ 12 | #EOT 13 | 14 | cp -R ../jfx/native/lecter/* . 15 | cp ../../src/main/deploy/bin/desktopintegration lecter.wrapper 16 | chmod +x lecter.wrapper 17 | mkdir -p usr/share/icons/hicolor/ 18 | mkdir -p usr/share/icons/default/ 19 | cp ../../src/main/deploy/package/appimage/lecter.desktop . 20 | cp ../../src/main/deploy/package/linux/lecter.png . 21 | cp -R ../../src/main/deploy/icons/* usr/share/icons/hicolor/ 22 | cp -R ../../src/main/deploy/icons/* usr/share/icons/default/ 23 | mkdir -p usr/share/metainfo/ 24 | cp ../../src/main/deploy/package/linux/lecter.appdata.xml usr/share/metainfo/ 25 | ln -s lecter.wrapper AppRun 26 | 27 | cd .. 28 | echo "downloading appimage executable" 29 | wget https://github.com/AppImage/AppImageKit/releases/download/10/appimagetool-x86_64.AppImage -O appimagetool-x86_64.AppImage 30 | chmod +x appimagetool-x86_64.AppImage 31 | echo "generating lecter appimage" 32 | ./appimagetool-x86_64.AppImage -nv lecter.AppDir lecter-$VERSION.AppImage 33 | 34 | exit 0 35 | -------------------------------------------------------------------------------- /src/main/deploy/icons/128x128/apps/lecter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/icons/128x128/apps/lecter.png -------------------------------------------------------------------------------- /src/main/deploy/icons/16x16/apps/lecter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/icons/16x16/apps/lecter.png -------------------------------------------------------------------------------- /src/main/deploy/icons/24x24/apps/lecter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/icons/24x24/apps/lecter.png -------------------------------------------------------------------------------- /src/main/deploy/icons/256x256/apps/lecter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/icons/256x256/apps/lecter.png -------------------------------------------------------------------------------- /src/main/deploy/icons/32x32/apps/lecter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/icons/32x32/apps/lecter.png -------------------------------------------------------------------------------- /src/main/deploy/icons/48x48/apps/lecter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/icons/48x48/apps/lecter.png -------------------------------------------------------------------------------- /src/main/deploy/icons/64x64/apps/lecter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/icons/64x64/apps/lecter.png -------------------------------------------------------------------------------- /src/main/deploy/package/appimage/lecter.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=lecter 3 | Comment=rss feeds reader 4 | Exec=lecter.wrapper 5 | Icon=lecter 6 | Terminal=false 7 | Type=Application 8 | Categories=Utility; 9 | StartupWMClass=me.bayang.reader.FXMain 10 | -------------------------------------------------------------------------------- /src/main/deploy/package/linux/lecter.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | lecter.desktop 4 | MIT 5 | MIT 6 | lecter 7 | RSS cloud providers client 8 | 9 |

10 | lecter is a desktop RSS reader. It can fetch feeds from cloud providers such as Inoreader. 11 |

12 |

13 | lecter supports sharing with third party providers like Pocket or Wallabag. 14 |

15 |
16 | 17 | 18 | lecter main screen 19 | https://raw.githubusercontent.com/bayang/lecter/master/screenshots/list_mobilizer_light.png 20 | 21 | 22 | https://github.com/bayang/lecter 23 |
24 | -------------------------------------------------------------------------------- /src/main/deploy/package/linux/lecter.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=lecter 3 | Comment=rss feeds reader 4 | Exec=lecter 5 | Icon=lecter 6 | Terminal=false 7 | Type=Application 8 | Categories=Utility; 9 | StartupWMClass=me.bayang.reader.FXMain 10 | -------------------------------------------------------------------------------- /src/main/deploy/package/linux/lecter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/package/linux/lecter.png -------------------------------------------------------------------------------- /src/main/deploy/package/windows/lecter-setup-icon_dis.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/package/windows/lecter-setup-icon_dis.bmp -------------------------------------------------------------------------------- /src/main/deploy/package/windows/lecter.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayang/lecter/42a25edc4c1c372189cfb0d87754f34691c33694/src/main/deploy/package/windows/lecter.ico -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/FXMain.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.ConfigurableApplicationContext; 7 | import org.springframework.context.annotation.PropertySource; 8 | 9 | import de.felixroske.jfxsupport.AbstractFxmlView; 10 | import de.felixroske.jfxsupport.AbstractJavaFxApplicationSupport; 11 | import de.felixroske.jfxsupport.GUIState; 12 | import javafx.scene.Scene; 13 | import javafx.scene.image.Image; 14 | import javafx.stage.Modality; 15 | import javafx.stage.Stage; 16 | import me.bayang.reader.controllers.ShareLinkController; 17 | import me.bayang.reader.utils.Theme; 18 | import me.bayang.reader.view.RssView; 19 | import me.bayang.reader.view.SpinnerSplashScreen; 20 | 21 | @SpringBootApplication 22 | @PropertySource(value="file:${home.dir.config}/.config/lecter/config.properties", ignoreResourceNotFound=true) 23 | public class FXMain extends AbstractJavaFxApplicationSupport { 24 | 25 | public static Stage shareLinkStage = null; 26 | public static ShareLinkController shareLinkController = null; 27 | 28 | public static ResourceBundle bundle = ResourceBundle.getBundle("i18n.translations"); 29 | 30 | public static String startupCss; 31 | 32 | public static void main(String[] args) { 33 | // launch(FXMain.class, RssView.class, new NoOpSplashScreen() ,args); 34 | launch(FXMain.class, RssView.class, new SpinnerSplashScreen(), args); 35 | } 36 | 37 | public static void createShareLinkStage() { 38 | Stage dialogStage = new Stage(); 39 | dialogStage.initModality(Modality.WINDOW_MODAL); 40 | dialogStage.initOwner(FXMain.getStage()); 41 | dialogStage.getIcons().add(new Image("icon.png")); 42 | dialogStage.setResizable(true); 43 | FXMain.shareLinkStage = dialogStage; 44 | } 45 | 46 | @Override 47 | public void beforeInitialView(Stage stage, 48 | ConfigurableApplicationContext ctx) { 49 | super.beforeInitialView(stage, ctx); 50 | final AbstractFxmlView view = ctx.getBean(RssView.class); 51 | if (GUIState.getScene() == null) { 52 | GUIState.setScene(new Scene(view.getView())); 53 | } 54 | Theme appTheme = Theme.valueOf(ctx.getEnvironment().getProperty("app.css", Theme.LIGHT.name())); 55 | startupCss = appTheme.getPath(); 56 | GUIState.getScene().getStylesheets().add(FXMain.class.getResource(startupCss).toExternalForm()); 57 | stage.setMinWidth(700); 58 | stage.setMinHeight(650); 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/backend/UserInfo.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.backend; 2 | 3 | public class UserInfo { 4 | 5 | private static String userId; 6 | private static String authString; 7 | 8 | public static void setUserId(String userId) { 9 | UserInfo.userId = userId; 10 | } 11 | 12 | public static String getUserId() { 13 | return userId; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/backend/inoreader/FolderFeedOrder.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.backend.inoreader; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.Comparator; 8 | import java.util.HashMap; 9 | import java.util.LinkedHashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.TreeMap; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.stereotype.Service; 18 | 19 | import com.fasterxml.jackson.databind.ObjectMapper; 20 | import com.google.gson.JsonArray; 21 | import com.google.gson.JsonElement; 22 | import com.google.gson.JsonObject; 23 | import com.google.gson.JsonParser; 24 | 25 | import javafx.scene.image.Image; 26 | import me.bayang.reader.backend.UserInfo; 27 | import me.bayang.reader.rssmodels.Categories; 28 | import me.bayang.reader.rssmodels.Feed; 29 | import me.bayang.reader.rssmodels.FoldersTagsList; 30 | import me.bayang.reader.rssmodels.Subscription; 31 | import me.bayang.reader.rssmodels.SubscriptionsList; 32 | import me.bayang.reader.rssmodels.Tag; 33 | 34 | /** 35 | * This class is used to get the order of the folder and the feed inside. 36 | */ 37 | @Service 38 | public class FolderFeedOrder { 39 | 40 | private static final Logger LOGGER = LoggerFactory.getLogger(FolderFeedOrder.class); 41 | 42 | @Autowired 43 | private ConnectServer connectServer; 44 | 45 | @Autowired 46 | private ObjectMapper mapper; 47 | 48 | public static Map iconMap; 49 | 50 | /** 51 | * Connect server and get the information about the order, then return a map. 52 | * UNUSED METHOD 53 | * 54 | * @return A map containing order (inoreader order). 55 | */ 56 | @Deprecated 57 | public Map> getInoreaderOrder() { 58 | String userId = UserInfo.getUserId(); 59 | //get the streamprefs 60 | String streamprefs = null; 61 | try (BufferedReader reader = connectServer.connectServer(ConnectServer.streamPreferenceListURL)) { 62 | streamprefs = reader.readLine(); 63 | if (reader != null) { 64 | reader.close(); 65 | } 66 | JsonParser parser = new JsonParser(); 67 | JsonElement element = parser.parse(streamprefs); 68 | JsonObject object = element.getAsJsonObject(); 69 | JsonArray root = object.getAsJsonObject("streamprefs").getAsJsonArray("user/" + userId + "/state/com.google/root"); 70 | String folderOrderString = root.get(0).getAsJsonObject().get("value").getAsString(); 71 | List folderOrderList = sortOrder(folderOrderString); 72 | 73 | //get the folderTagList and the subscriptionList 74 | BufferedReader folderReader = connectServer.connectServer(ConnectServer.folderTagListURL); 75 | FoldersTagsList foldersTagsList = mapper.readValue(folderReader, FoldersTagsList.class); 76 | BufferedReader subscriptionsReader = connectServer.connectServer(ConnectServer.subscriptionListURL); 77 | SubscriptionsList subscriptionsList = mapper.readValue(subscriptionsReader, SubscriptionsList.class); 78 | ConnectServer.closeReader(folderReader); 79 | ConnectServer.closeReader(subscriptionsReader); 80 | //get the icon map 81 | iconMap = new HashMap<>(); 82 | for (Subscription subscription : subscriptionsList.getSubscriptions()) { 83 | if (!subscription.getIconUrl().equals("")) { 84 | if (! iconMap.containsKey(subscription.getId())) { 85 | iconMap.put(subscription.getId(), new Image(subscription.getIconUrl(), true)); 86 | } 87 | } 88 | } 89 | //sort the folder order 90 | Map> orderMap = new LinkedHashMap<>(); 91 | orderMap.put(new Tag("All Items", null), null); 92 | orderMap.put(foldersTagsList.getTags().get(0), null); 93 | for (String s : folderOrderList) { 94 | boolean isFolder = false; 95 | for (Tag tag : foldersTagsList.getTags()) { 96 | if (s.equals(tag.getSortid())) { 97 | isFolder = true; 98 | 99 | //get the feed order in the folder 100 | List list = getFeedOrder(object, tag.getId(), subscriptionsList); 101 | orderMap.put(tag, list); 102 | break; 103 | } 104 | } 105 | if (!isFolder) { 106 | for (Subscription subscription : subscriptionsList.getSubscriptions()) { 107 | if (s.equals(subscription.getSortid())) { 108 | orderMap.put(subscription, null); 109 | break; 110 | } 111 | } 112 | } 113 | return orderMap; 114 | } 115 | } catch (IOException ioe) { 116 | LOGGER.error("error while getting inoreadr order",ioe); 117 | } 118 | return Collections.emptyMap(); 119 | } 120 | 121 | public Map> getAlphabeticalOrder() { 122 | try { 123 | //get the folderTagList and the subscriptionList 124 | BufferedReader folderReader = connectServer.connectServer(ConnectServer.folderTagListURL); 125 | FoldersTagsList foldersTagsList = mapper.readValue(folderReader, FoldersTagsList.class); 126 | // LOGGER.debug("foldersTagsList -> {}", foldersTagsList); 127 | BufferedReader subscriptionsReader = connectServer.connectServer(ConnectServer.subscriptionListURL); 128 | SubscriptionsList subscriptionsList = mapper.readValue(subscriptionsReader, SubscriptionsList.class); 129 | // LOGGER.debug("subscriptionsList -> {}", subscriptionsList); 130 | ConnectServer.closeReader(folderReader); 131 | ConnectServer.closeReader(subscriptionsReader); 132 | 133 | Comparator labelComp = new Comparator() { 134 | @Override 135 | public int compare(Feed o1, Feed o2) { 136 | if (o1 instanceof Categories && o2 instanceof Subscription) { 137 | return -1; 138 | } 139 | else if (o1 instanceof Subscription && o2 instanceof Categories) { 140 | return 1; 141 | } 142 | else { 143 | return o1.getLabel().compareToIgnoreCase(o2.getLabel()); 144 | } 145 | } 146 | }; 147 | Map> treeMap = new TreeMap<>(labelComp); 148 | 149 | //get the icon map 150 | iconMap = new HashMap<>(); 151 | //sort the folder order 152 | Map> orderMap = new LinkedHashMap<>(); 153 | orderMap.put(new Categories("All Items", "All Items"), null); 154 | orderMap.put(new Categories(foldersTagsList.getTags().get(0).getId(), "starred"), null); 155 | for (Subscription subscription : subscriptionsList.getSubscriptions()) { 156 | if (!subscription.getIconUrl().equals("")) { 157 | if (! iconMap.containsKey(subscription.getId())) { 158 | iconMap.put(subscription.getId(), new Image(subscription.getIconUrl(), true)); 159 | } 160 | } 161 | if (subscription.getCategories() != null && ! subscription.getCategories().isEmpty()) { 162 | for (Categories c : subscription.getCategories()) { 163 | if (! treeMap.containsKey(c)) { 164 | List l = new ArrayList<>(); 165 | l.add(subscription); 166 | treeMap.put(c, l); 167 | } 168 | else { 169 | treeMap.get(c).add(subscription); 170 | } 171 | } 172 | } 173 | else { 174 | treeMap.put(subscription, null); 175 | } 176 | } 177 | 178 | Comparator comp = new Comparator() { 179 | @Override 180 | public int compare(Subscription o1, Subscription o2) { 181 | return o1.getTitle().compareToIgnoreCase(o2.getTitle()); 182 | } 183 | }; 184 | for (List subs : treeMap.values()) { 185 | if (subs != null && ! subs.isEmpty()) { 186 | Collections.sort(subs, comp); 187 | } 188 | } 189 | for (Map.Entry> entry : treeMap.entrySet()) { 190 | orderMap.put(entry.getKey(), entry.getValue()); 191 | } 192 | return orderMap; 193 | } catch (IOException e) { 194 | LOGGER.error("error while loading folder & feed order", e); 195 | } 196 | return Collections.emptyMap(); 197 | } 198 | 199 | private static List sortOrder(String s) { //maybe using stream 200 | List list = new ArrayList<>(); 201 | for (int i = 0; i < s.length(); i += 8) { 202 | list.add(s.substring(i, i + 8)); 203 | } 204 | return list; 205 | } 206 | 207 | private static List getFeedOrder(JsonObject streamprefs, String folderId, SubscriptionsList subscriptionsList) { 208 | String feedOrderString = streamprefs.getAsJsonObject("streamprefs").getAsJsonArray(folderId).get(1).getAsJsonObject().get("value").getAsString(); 209 | List feedOrderList = sortOrder(feedOrderString); 210 | List list = new ArrayList<>(); //the list to return, contains the feed order 211 | for (String s : feedOrderList) { 212 | for (Subscription subscription : subscriptionsList.getSubscriptions()) { 213 | if (s.equals(subscription.getSortid())) { 214 | list.add(subscription); 215 | } 216 | } 217 | } 218 | return list; 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/components/DeletableLabel.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.components; 2 | 3 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; 4 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; 5 | import javafx.geometry.Insets; 6 | import javafx.geometry.Pos; 7 | import javafx.scene.control.Label; 8 | import javafx.scene.layout.HBox; 9 | 10 | public class DeletableLabel extends HBox { 11 | 12 | private String content; 13 | 14 | private FontAwesomeIconView deleteCross; 15 | 16 | public DeletableLabel(String content) { 17 | this.content = content; 18 | Label l = new Label(content); 19 | l.getStyleClass().add("deletable-content"); 20 | l.setPadding(new Insets(0, 5, 0, 3)); 21 | this.getChildren().add(l); 22 | FontAwesomeIconView fv = new FontAwesomeIconView(FontAwesomeIcon.TIMES, "18"); 23 | fv.getStyleClass().add("deletable-cross"); 24 | this.getChildren().add(fv); 25 | this.deleteCross = fv; 26 | this.setPrefSize(USE_COMPUTED_SIZE, USE_COMPUTED_SIZE); 27 | this.setAlignment(Pos.CENTER); 28 | this.getStyleClass().add("deletable-box"); 29 | } 30 | 31 | public FontAwesomeIconView getDeleteCross() { 32 | return deleteCross; 33 | } 34 | 35 | public void setDeleteCross(FontAwesomeIconView deleteCross) { 36 | this.deleteCross = deleteCross; 37 | } 38 | 39 | public String getContent() { 40 | return content; 41 | } 42 | 43 | public void setContent(String content) { 44 | this.content = content; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/components/ItemGridCell.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.components; 2 | 3 | import java.io.IOException; 4 | import java.time.Instant; 5 | import java.time.LocalDate; 6 | import java.time.LocalDateTime; 7 | import java.time.ZoneId; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | import org.apache.commons.text.StringEscapeUtils; 11 | import org.controlsfx.control.GridCell; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import javafx.beans.property.BooleanProperty; 16 | import javafx.beans.property.SimpleBooleanProperty; 17 | import javafx.css.PseudoClass; 18 | import javafx.fxml.FXML; 19 | import javafx.fxml.FXMLLoader; 20 | import javafx.scene.control.ContextMenu; 21 | import javafx.scene.control.Label; 22 | import javafx.scene.control.MenuItem; 23 | import javafx.scene.control.Tooltip; 24 | import javafx.scene.image.ImageView; 25 | import javafx.scene.input.MouseButton; 26 | import javafx.scene.input.MouseEvent; 27 | import javafx.scene.layout.HBox; 28 | import javafx.scene.layout.VBox; 29 | import me.bayang.reader.FXMain; 30 | import me.bayang.reader.backend.inoreader.ConnectServer; 31 | import me.bayang.reader.backend.inoreader.FolderFeedOrder; 32 | import me.bayang.reader.controllers.RssController; 33 | import me.bayang.reader.rssmodels.Item; 34 | import me.bayang.reader.utils.StringUtils; 35 | 36 | public class ItemGridCell extends GridCell { 37 | 38 | private final Logger LOGGER = LoggerFactory.getLogger(getClass().getName()); 39 | 40 | @FXML 41 | private VBox cellWrapper; 42 | 43 | @FXML 44 | private HBox firstLine; 45 | 46 | @FXML 47 | private Label fromLabel; 48 | 49 | @FXML 50 | private Label dateLabel; 51 | 52 | @FXML 53 | private Label subjectLabel; 54 | 55 | @FXML 56 | private Label contentLabel; 57 | 58 | @FXML 59 | private ImageView icon; 60 | 61 | private FXMLLoader mLLoader; 62 | 63 | private ContextMenu menu = new ContextMenu(); 64 | 65 | private ConnectServer connectServer; 66 | 67 | private RssController rssController; 68 | 69 | private Item currentItem; 70 | 71 | private static PseudoClass READ_PSEUDO_CLASS = PseudoClass.getPseudoClass("read"); 72 | 73 | BooleanProperty readProperty; 74 | 75 | public ItemGridCell(RssController rssController, ConnectServer connectServer) { 76 | super(); 77 | this.rssController = rssController; 78 | this.connectServer = connectServer; 79 | this.getStyleClass().add("readable-cell"); 80 | setOnMouseClicked(event -> { 81 | if (event.getButton() == MouseButton.PRIMARY) { 82 | this.rssController.showWebView(this.currentItem); 83 | this.rssController.markItemRead(this.currentItem, true); 84 | } 85 | }); 86 | MenuItem starItem = new MenuItem(FXMain.bundle.getString("markStarred")); 87 | MenuItem unStarItem = new MenuItem(FXMain.bundle.getString("markUnstarred")); 88 | MenuItem markItemRead = new MenuItem(FXMain.bundle.getString("markRead")); 89 | MenuItem markItemUnread = new MenuItem(FXMain.bundle.getString("markUnread")); 90 | menu.getItems().addAll(starItem, unStarItem, markItemRead, markItemUnread); 91 | 92 | starItem.setOnAction(event -> { 93 | LOGGER.debug("mark star " + this.currentItem.getDecimalId() + " "+this.currentItem.getSummary()); 94 | this.connectServer.star(this.currentItem.getDecimalId()); 95 | this.rssController.addToStarredList(currentItem); 96 | }); 97 | unStarItem.setOnAction(event -> { 98 | LOGGER.debug("unstar " + this.currentItem.getDecimalId() + " "+this.currentItem.getSummary()); 99 | this.connectServer.unStar(this.currentItem.getDecimalId()); 100 | this.rssController.removeFromStarredList(currentItem); 101 | }); 102 | markItemRead.setOnAction(event -> { 103 | LOGGER.debug("read " + this.currentItem.getDecimalId() + " "+this.currentItem.getSummary()); 104 | this.rssController.markItemRead(this.currentItem, false); 105 | }); 106 | markItemUnread.setOnAction(event -> { 107 | LOGGER.debug("unread " + currentItem.getDecimalId()); 108 | this.rssController.markItemUnread(currentItem); 109 | }); 110 | this.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> { 111 | if (e.getButton() == MouseButton.SECONDARY) { 112 | e.consume(); 113 | } 114 | }); 115 | this.readProperty = new SimpleBooleanProperty(false); 116 | this.readProperty.addListener(e -> this.pseudoClassStateChanged(READ_PSEUDO_CLASS, readProperty.get())); 117 | this.itemProperty().addListener((observable, oldItem, newItem) -> { 118 | this.updateItem(newItem, newItem == null); 119 | }); 120 | } 121 | 122 | @Override 123 | protected void updateItem(Item item, boolean empty) { 124 | super.updateItem(item, empty); 125 | this.currentItem = item; 126 | if (empty || item == null) { 127 | setText(null); 128 | setGraphic(null); 129 | } else { 130 | 131 | if (mLLoader == null) { 132 | mLLoader = new FXMLLoader( 133 | getClass().getResource("/fxml/ItemGridCell.fxml")); 134 | mLLoader.setController(this); 135 | try { 136 | mLLoader.load(); 137 | } catch (IOException e) { 138 | LOGGER.error("",e); 139 | } 140 | } 141 | readProperty.bind(item.readProperty()); 142 | 143 | //get the time style 144 | LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(item.getCrawlTimeMsec())), ZoneId.systemDefault()); 145 | String timeString = localDateTime.toLocalTime().format(DateTimeFormatter.ofPattern("kk:mm:ss")); 146 | if (!localDateTime.toLocalDate().equals(LocalDate.now())) { 147 | timeString = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")); 148 | } 149 | // LOGGER.debug("read {}", isReadProperty()); 150 | fromLabel.setText(StringEscapeUtils.unescapeHtml4(item.getOrigin().getTitle())); 151 | dateLabel.setText(timeString); 152 | subjectLabel.setWrapText(true); 153 | subjectLabel.setText(StringEscapeUtils.unescapeHtml4(item.getTitle())); 154 | contentLabel.setText(StringUtils.processContent(item.getSummary().getContent())); 155 | Tooltip t = new Tooltip(subjectLabel.getText()); 156 | this.setTooltip(t); 157 | setText(null); 158 | if (FolderFeedOrder.iconMap != null) { 159 | icon.setImage(FolderFeedOrder.iconMap.get(item.getOrigin().getStreamId())); 160 | icon.setFitWidth(20); 161 | icon.setFitHeight(20); 162 | } 163 | setGraphic(cellWrapper); 164 | setContextMenu(menu); 165 | } 166 | } 167 | 168 | public void setReadProperty(boolean read) { 169 | this.readProperty.set(read); 170 | } 171 | 172 | public boolean isReadProperty() { 173 | return this.readProperty.get(); 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/components/ItemListCell.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.components; 2 | 3 | import java.io.IOException; 4 | import java.time.Instant; 5 | import java.time.LocalDate; 6 | import java.time.LocalDateTime; 7 | import java.time.ZoneId; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | import org.apache.commons.text.StringEscapeUtils; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import com.jfoenix.controls.JFXListCell; 15 | 16 | import javafx.beans.property.BooleanProperty; 17 | import javafx.beans.property.SimpleBooleanProperty; 18 | import javafx.css.PseudoClass; 19 | import javafx.fxml.FXML; 20 | import javafx.fxml.FXMLLoader; 21 | import javafx.scene.control.ContextMenu; 22 | import javafx.scene.control.Label; 23 | import javafx.scene.control.MenuItem; 24 | import javafx.scene.control.Tooltip; 25 | import javafx.scene.image.ImageView; 26 | import javafx.scene.input.MouseButton; 27 | import javafx.scene.input.MouseEvent; 28 | import javafx.scene.layout.HBox; 29 | import javafx.scene.layout.VBox; 30 | import me.bayang.reader.FXMain; 31 | import me.bayang.reader.backend.inoreader.ConnectServer; 32 | import me.bayang.reader.backend.inoreader.FolderFeedOrder; 33 | import me.bayang.reader.controllers.RssController; 34 | import me.bayang.reader.rssmodels.Item; 35 | import me.bayang.reader.utils.StringUtils; 36 | 37 | public class ItemListCell extends JFXListCell { 38 | 39 | private final Logger LOGGER = LoggerFactory.getLogger(getClass().getName()); 40 | 41 | @FXML 42 | private VBox cellWrapper; 43 | 44 | @FXML 45 | private HBox firstLine; 46 | 47 | @FXML 48 | private Label fromLabel; 49 | 50 | @FXML 51 | private Label dateLabel; 52 | 53 | @FXML 54 | private Label subjectLabel; 55 | 56 | @FXML 57 | private Label contentLabel; 58 | 59 | @FXML 60 | private ImageView icon; 61 | 62 | private FXMLLoader mLLoader; 63 | 64 | private ContextMenu menu = new ContextMenu(); 65 | 66 | private ConnectServer connectServer; 67 | 68 | private RssController rssController; 69 | 70 | private static PseudoClass READ_PSEUDO_CLASS = PseudoClass.getPseudoClass("read"); 71 | 72 | private BooleanProperty readProperty; 73 | 74 | private Item currentItem; 75 | 76 | public ItemListCell(RssController rssController, ConnectServer connectServer) { 77 | this.connectServer = connectServer; 78 | this.rssController = rssController; 79 | this.getStyleClass().add("readable-cell"); 80 | MenuItem starItem = new MenuItem(FXMain.bundle.getString("markStarred")); 81 | MenuItem unStarItem = new MenuItem(FXMain.bundle.getString("markUnstarred")); 82 | MenuItem markItemRead = new MenuItem(FXMain.bundle.getString("markRead")); 83 | MenuItem markItemUnread = new MenuItem(FXMain.bundle.getString("markUnread")); 84 | menu.getItems().addAll(starItem, unStarItem, markItemRead, markItemUnread); 85 | 86 | starItem.setOnAction(event -> { 87 | LOGGER.debug("mark star " + getListView().getSelectionModel().getSelectedItem().getDecimalId()); 88 | this.connectServer.star(this.getListView().getSelectionModel().getSelectedItem().getDecimalId()); 89 | this.rssController.addToStarredList(this.getListView().getSelectionModel().getSelectedItem()); 90 | }); 91 | unStarItem.setOnAction(event -> { 92 | LOGGER.debug("unstar " + this.getListView().getSelectionModel().getSelectedItem().getDecimalId()); 93 | this.connectServer.unStar(this.getListView().getSelectionModel().getSelectedItem().getDecimalId()); 94 | }); 95 | markItemRead.setOnAction(event -> { 96 | LOGGER.debug("read " + currentItem.getDecimalId()); 97 | this.rssController.markItemRead(currentItem, false); 98 | }); 99 | markItemUnread.setOnAction(event -> { 100 | LOGGER.debug("unread " + currentItem.getDecimalId()); 101 | this.rssController.markItemUnread(currentItem); 102 | }); 103 | this.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> { 104 | if (e.getButton() == MouseButton.SECONDARY) { 105 | e.consume(); 106 | } 107 | }); 108 | this.readProperty = new SimpleBooleanProperty(false); 109 | this.readProperty.addListener(e -> this.pseudoClassStateChanged(READ_PSEUDO_CLASS, readProperty.get())); 110 | this.pseudoClassStateChanged(READ_PSEUDO_CLASS, readProperty.get()); 111 | } 112 | 113 | @Override 114 | protected void updateItem(Item item, boolean empty) { 115 | super.updateItem(item, empty); 116 | this.currentItem = item; 117 | this.prefWidthProperty().bind( this.getListView().widthProperty().subtract( 20 ) ); 118 | if (empty || item == null) { 119 | setText(null); 120 | setGraphic(null); 121 | } else { 122 | if (mLLoader == null) { 123 | mLLoader = new FXMLLoader( 124 | getClass().getResource("/fxml/ItemListCell.fxml")); 125 | mLLoader.setController(this); 126 | try { 127 | mLLoader.load(); 128 | } catch (IOException e) { 129 | LOGGER.error("",e); 130 | } 131 | } 132 | // setReadProperty(item.isRead()); 133 | readProperty.bind(item.readProperty()); 134 | 135 | //get the time style 136 | LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(item.getCrawlTimeMsec())), ZoneId.systemDefault()); 137 | String timeString = localDateTime.toLocalTime().format(DateTimeFormatter.ofPattern("kk:mm:ss")); 138 | if (!localDateTime.toLocalDate().equals(LocalDate.now())) { 139 | timeString = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")); 140 | } 141 | // LOGGER.debug("read {}", isReadProperty()); 142 | fromLabel.setText(StringEscapeUtils.unescapeHtml4(item.getOrigin().getTitle())); 143 | dateLabel.setText(timeString); 144 | subjectLabel.setWrapText(true); 145 | subjectLabel.setText(StringEscapeUtils.unescapeHtml4(item.getTitle())); 146 | contentLabel.setText(StringUtils.processContent(item.getSummary().getContent())); 147 | Tooltip t = new Tooltip(subjectLabel.getText()); 148 | this.setTooltip(t); 149 | setText(null); 150 | if (FolderFeedOrder.iconMap != null) { 151 | icon.setImage(FolderFeedOrder.iconMap.get(item.getOrigin().getStreamId())); 152 | icon.setFitWidth(20); 153 | icon.setFitHeight(20); 154 | } 155 | setGraphic(cellWrapper); 156 | setContextMenu(menu); 157 | } 158 | } 159 | 160 | public void setReadProperty(boolean read) { 161 | this.readProperty.set(read); 162 | } 163 | 164 | public boolean isReadProperty() { 165 | return this.readProperty.get(); 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.config; 2 | 3 | import java.io.File; 4 | import java.util.concurrent.Executor; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import org.apache.commons.configuration2.FileBasedConfiguration; 8 | import org.apache.commons.configuration2.PropertiesConfiguration; 9 | import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder; 10 | import org.apache.commons.configuration2.builder.fluent.Parameters; 11 | import org.apache.commons.lang3.SystemUtils; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.scheduling.annotation.EnableAsync; 16 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 17 | 18 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 19 | import com.fasterxml.jackson.databind.DeserializationFeature; 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import com.fasterxml.jackson.databind.SerializationFeature; 22 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 23 | import com.fasterxml.jackson.module.afterburner.AfterburnerModule; 24 | 25 | import okhttp3.OkHttpClient; 26 | 27 | @Configuration 28 | @EnableAsync 29 | public class AppConfig { 30 | 31 | public static final File appDir = SystemUtils.getUserHome(); 32 | public static File appConfigDir; 33 | // FIXME detect OS and build file path accordingly 34 | static { 35 | if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_UNIX 36 | || SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) { 37 | appConfigDir = new File(appDir, ".config/lecter"); 38 | } 39 | } 40 | 41 | @Value("${appVersion}") 42 | public String appVersion = ""; 43 | 44 | // http://www.baeldung.com/spring-async 45 | @Bean(name = "threadPoolTaskExecutor") 46 | public Executor threadPoolTaskExecutor() { 47 | ThreadPoolTaskExecutor t = new ThreadPoolTaskExecutor(); 48 | t.setMaxPoolSize(50); 49 | return t; 50 | } 51 | 52 | @Bean(name="mapper") 53 | public ObjectMapper getMapper() { 54 | ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); 55 | 56 | mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 57 | mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); 58 | 59 | mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); 60 | mapper.setSerializationInclusion(Include.NON_NULL); 61 | 62 | mapper.registerModule(new AfterburnerModule()); 63 | 64 | return mapper; 65 | } 66 | 67 | @Bean("tokenConfig") 68 | public FileBasedConfigurationBuilder tokenConfig() { 69 | Parameters params = new Parameters(); 70 | File propertiesFile = new File(appConfigDir, "token.properties"); 71 | 72 | FileBasedConfigurationBuilder builder = 73 | new FileBasedConfigurationBuilder(PropertiesConfiguration.class) 74 | .configure(params.fileBased() 75 | .setFile(propertiesFile)); 76 | builder.setAutoSave(true); 77 | return builder; 78 | } 79 | 80 | @Bean("userConfig") 81 | public FileBasedConfigurationBuilder userConfig() { 82 | Parameters params = new Parameters(); 83 | File propertiesFile = new File(appConfigDir, "user.properties"); 84 | 85 | FileBasedConfigurationBuilder builder = 86 | new FileBasedConfigurationBuilder(PropertiesConfiguration.class) 87 | .configure(params.fileBased() 88 | .setFile(propertiesFile)); 89 | builder.setAutoSave(true); 90 | return builder; 91 | } 92 | 93 | @Bean("applicationConfig") 94 | public FileBasedConfigurationBuilder applicationConfig() { 95 | Parameters params = new Parameters(); 96 | File propertiesFile = new File(appConfigDir, "config.properties"); 97 | 98 | FileBasedConfigurationBuilder builder = 99 | new FileBasedConfigurationBuilder(PropertiesConfiguration.class) 100 | .configure(params.fileBased() 101 | .setFile(propertiesFile)); 102 | builder.setAutoSave(true); 103 | return builder; 104 | } 105 | 106 | @Bean("baseOkHttpClient") 107 | public OkHttpClient okHttpClient() { 108 | return new OkHttpClient.Builder() 109 | .followRedirects(true) 110 | .followSslRedirects(true) 111 | .readTimeout(1, TimeUnit.MINUTES) 112 | .connectTimeout(1, TimeUnit.MINUTES) 113 | .build(); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/config/AppConfigurationDecoder.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.config; 2 | 3 | import javax.annotation.PostConstruct; 4 | 5 | import org.apache.commons.configuration2.ConfigurationDecoder; 6 | import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class AppConfigurationDecoder implements ConfigurationDecoder { 12 | 13 | private StandardPBEStringEncryptor encryptor; 14 | 15 | @Value("${encryptor.password}") 16 | private String encryptorPassword; 17 | 18 | @PostConstruct 19 | public void init() { 20 | encryptor = new StandardPBEStringEncryptor(); 21 | encryptor.setPassword(encryptorPassword); 22 | } 23 | 24 | @Override 25 | public String decode(String s) { 26 | return encryptor.decrypt(s); 27 | } 28 | 29 | public String encode(String in) { 30 | return encryptor.encrypt(in); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/controllers/AboutPopupController.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.controllers; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import de.felixroske.jfxsupport.FXMLController; 6 | import javafx.fxml.FXML; 7 | import javafx.scene.control.Hyperlink; 8 | import javafx.scene.control.Label; 9 | import javafx.scene.image.ImageView; 10 | import javafx.stage.Stage; 11 | import me.bayang.reader.config.AppConfig; 12 | import me.bayang.reader.utils.StringUtils; 13 | 14 | @FXMLController 15 | public class AboutPopupController { 16 | 17 | @FXML 18 | private ImageView appLogo; 19 | 20 | @FXML 21 | private Hyperlink hyperLink; 22 | 23 | @FXML 24 | private Label versionLabel; 25 | 26 | @Autowired 27 | private AppConfig appConfig; 28 | 29 | private Stage stage; 30 | 31 | @FXML 32 | public void initialize() { 33 | versionLabel.setText("Version : " + appConfig.appVersion); 34 | } 35 | 36 | @FXML 37 | public void openLink() { 38 | StringUtils.openHyperlink(hyperLink.getText()); 39 | } 40 | 41 | public Stage getStage() { 42 | return stage; 43 | } 44 | 45 | public void setStage(Stage stage) { 46 | this.stage = stage; 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/controllers/AddSubscriptionController.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.controllers; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.text.MessageFormat; 7 | import java.util.ResourceBundle; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | 13 | import de.felixroske.jfxsupport.FXMLController; 14 | import javafx.application.Platform; 15 | import javafx.fxml.FXML; 16 | import javafx.scene.control.Button; 17 | import javafx.scene.control.Label; 18 | import javafx.scene.control.TextField; 19 | import javafx.stage.Stage; 20 | import me.bayang.reader.backend.inoreader.ConnectServer; 21 | import me.bayang.reader.rssmodels.AddResult; 22 | import okhttp3.Call; 23 | import okhttp3.Callback; 24 | import okhttp3.HttpUrl; 25 | import okhttp3.Response; 26 | 27 | @FXMLController 28 | public class AddSubscriptionController { 29 | @FXML 30 | private TextField addressField; 31 | @FXML 32 | private Button addButton; 33 | @FXML 34 | private Label statusLabel; 35 | 36 | @Autowired 37 | private ConnectServer connectServer; 38 | 39 | @Autowired 40 | private ObjectMapper mapper; 41 | 42 | private ResourceBundle bundle = ResourceBundle.getBundle("i18n.translations"); 43 | 44 | private Stage stage; 45 | 46 | @FXML 47 | private void addButtonFired() { 48 | String address = addressField.getText(); 49 | statusLabel.setText(bundle.getString("addSubscriptionOngoing")); 50 | HttpUrl URL = HttpUrl.parse(ConnectServer.addSubscriptionURL); 51 | connectServer.getOkClient().newCall(connectServer.getRequest(URL.newBuilder() 52 | .setQueryParameter("quickadd", address) 53 | .build())) 54 | .enqueue(new Callback() { 55 | 56 | @Override 57 | public void onResponse(Call call, Response response) throws IOException { 58 | BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().source().inputStream(), "utf-8")); 59 | AddResult result = mapper.readValue(reader, AddResult.class); 60 | if (result.getNumResults() == 1) { 61 | Platform.runLater(() -> statusLabel.setText(MessageFormat.format(bundle.getString("successAddSubscription"), result.getStreamName()))); 62 | } else { 63 | Platform.runLater(() -> statusLabel.setText(bundle.getString("failedAddSubscription"))); 64 | } 65 | ConnectServer.closeReader(reader); 66 | } 67 | 68 | @Override 69 | public void onFailure(Call arg0, IOException arg1) { 70 | Platform.runLater(() -> statusLabel.setText(bundle.getString("failedAddSubscriptionNetwork"))); 71 | } 72 | }); 73 | } 74 | 75 | public Stage getStage() { 76 | return stage; 77 | } 78 | 79 | public void setStage(Stage stage) { 80 | this.stage = stage; 81 | } 82 | 83 | 84 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/controllers/EditSubscriptionController.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.controllers; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | 7 | import de.felixroske.jfxsupport.FXMLController; 8 | import javafx.fxml.FXML; 9 | import javafx.scene.control.Hyperlink; 10 | import javafx.scene.control.Label; 11 | import javafx.scene.control.Tooltip; 12 | import javafx.scene.image.ImageView; 13 | import javafx.scene.layout.VBox; 14 | import javafx.stage.Stage; 15 | import me.bayang.reader.backend.inoreader.ConnectServer; 16 | import me.bayang.reader.backend.inoreader.FolderFeedOrder; 17 | import me.bayang.reader.components.DeletableLabel; 18 | import me.bayang.reader.rssmodels.Categories; 19 | import me.bayang.reader.rssmodels.Subscription; 20 | import me.bayang.reader.utils.StringUtils; 21 | 22 | @FXMLController 23 | public class EditSubscriptionController { 24 | 25 | @Autowired 26 | private ConnectServer connectServer; 27 | 28 | @Autowired 29 | private RssController rssController; 30 | 31 | private Stage stage; 32 | 33 | private Subscription subscription; 34 | 35 | @FXML 36 | private Hyperlink subscriptionHtmlUrl; 37 | 38 | @FXML 39 | private Hyperlink subscriptionUrl; 40 | 41 | @FXML 42 | private Label subscriptionTitle; 43 | 44 | @FXML 45 | private ImageView subscriptionIcon; 46 | 47 | @FXML 48 | private VBox container; 49 | 50 | @FXML 51 | private VBox categoriesContainer; 52 | 53 | private static ResourceBundle bundle = ResourceBundle.getBundle("i18n.translations"); 54 | 55 | public EditSubscriptionController() { 56 | } 57 | 58 | @FXML 59 | public void onFeedUrlClicked() { 60 | StringUtils.openHyperlink(subscriptionUrl.getText()); 61 | } 62 | 63 | @FXML 64 | public void onUrlClicked() { 65 | StringUtils.openHyperlink(subscriptionHtmlUrl.getText()); 66 | } 67 | 68 | public Stage getStage() { 69 | return stage; 70 | } 71 | 72 | public void setStage(Stage stage) { 73 | this.stage = stage; 74 | } 75 | 76 | public Subscription getSubscription() { 77 | return subscription; 78 | } 79 | 80 | public void setSubscription(Subscription subscription) { 81 | this.subscription = subscription; 82 | subscriptionTitle.setText(subscription.getTitle()); 83 | subscriptionHtmlUrl.setText(subscription.getHtmlUrl()); 84 | subscriptionUrl.setText(subscription.getUrl()); 85 | if (FolderFeedOrder.iconMap != null) { 86 | subscriptionIcon.setImage(FolderFeedOrder.iconMap.get(subscription.getId())); 87 | subscriptionIcon.setFitHeight(24); 88 | subscriptionIcon.setFitWidth(24); 89 | } 90 | categoriesContainer.getChildren().clear(); 91 | for (Categories category : subscription.getCategories()) { 92 | DeletableLabel de = new DeletableLabel(category.getLabel()); 93 | if (subscription.getCategories().size() > 1 ) { 94 | Tooltip t = new Tooltip(bundle.getString("removeFromFolder")); 95 | Tooltip.install(de.getDeleteCross(), t); 96 | } 97 | else if (subscription.getCategories().size() == 1) { 98 | Tooltip t = new Tooltip(bundle.getString("unsubscribe")); 99 | Tooltip.install(de.getDeleteCross(), t); 100 | } 101 | 102 | de.getDeleteCross().setOnMouseClicked(event -> { 103 | if (subscription.getCategories().size() > 1 ) { 104 | connectServer.removeFromFolder(subscription.getId(), category.getId()); 105 | categoriesContainer.getChildren().remove(de); 106 | rssController.refreshFired(); 107 | } 108 | else if (subscription.getCategories().size() == 1) { 109 | connectServer.unsubscribe(subscription.getId()); 110 | categoriesContainer.getChildren().remove(de); 111 | rssController.refreshFired(); 112 | } 113 | }); 114 | this.categoriesContainer.getChildren().add(de); 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/controllers/OauthController.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.controllers; 2 | 3 | import java.io.IOException; 4 | import java.util.ResourceBundle; 5 | 6 | import org.dmfs.httpessentials.exceptions.ProtocolError; 7 | import org.dmfs.httpessentials.exceptions.ProtocolException; 8 | import org.dmfs.oauth2.client.OAuth2AccessToken; 9 | import org.dmfs.rfc3986.Uri; 10 | import org.dmfs.rfc3986.encoding.Precoded; 11 | import org.dmfs.rfc3986.uris.LazyUri; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | 16 | import de.felixroske.jfxsupport.FXMLController; 17 | import javafx.fxml.FXML; 18 | import javafx.scene.layout.AnchorPane; 19 | import javafx.scene.web.WebView; 20 | import javafx.stage.Stage; 21 | import me.bayang.reader.backend.inoreader.ConnectServer; 22 | 23 | @FXMLController 24 | public class OauthController { 25 | 26 | private static Logger LOGGER = LoggerFactory.getLogger(OauthController.class); 27 | 28 | @FXML 29 | private WebView oauthView; 30 | 31 | @FXML 32 | private AnchorPane oauthViewWrapper; 33 | 34 | @Autowired 35 | private ConnectServer connectServer; 36 | 37 | private OAuth2AccessToken token; 38 | 39 | private Stage stage; 40 | 41 | private static ResourceBundle bundle = ResourceBundle.getBundle("i18n.translations"); 42 | 43 | @FXML 44 | private void initialize() { 45 | 46 | try { 47 | oauthView.getEngine().load(connectServer.getAuthorizationUrl().toString()); 48 | } catch (Exception ex) { 49 | RssController.snackbarNotify(bundle.getString("connectionFailure")); 50 | LOGGER.error("failure during initialization of oauthController", ex); 51 | } 52 | oauthView.getEngine().locationProperty().addListener((observable, oldValue, newValue) -> { 53 | if (oldValue != null && newValue != null) { 54 | LOGGER.debug("{} -> {}",oldValue, newValue); 55 | if (newValue.contains("localhost:8080/reader/redirect")) { 56 | Uri redirect = new LazyUri(new Precoded(newValue)); 57 | try { 58 | token = connectServer.getGrant().withRedirect(redirect).accessToken(connectServer.getExecutor()); 59 | // LOGGER.debug("token : {} - {} - {} - {} - {}", token.accessToken(),token.expirationDate().toString(), token.refreshToken(), token.tokenType(), token.scope()); 60 | connectServer.setToken(token); 61 | connectServer.setShouldAskPermissionOrLogin(false); 62 | connectServer.fetchAndSaveUSer(); 63 | stage.close(); 64 | } catch (IOException e) { 65 | LOGGER.error("failure during oauth token fetching and processing", e); 66 | } catch (ProtocolError e) { 67 | LOGGER.error("failure during oauth token fetching and processing", e); 68 | } catch (ProtocolException e) { 69 | LOGGER.error("failure during oauth token fetching and processing", e); 70 | } 71 | } 72 | return; 73 | } 74 | if (oldValue != null) { 75 | LOGGER.debug(oldValue); 76 | } 77 | if (newValue != null) { 78 | LOGGER.debug(newValue); 79 | } 80 | }); 81 | } 82 | 83 | public ConnectServer getConnectServer() { 84 | return connectServer; 85 | } 86 | 87 | public void setConnectServer(ConnectServer connectServer) { 88 | this.connectServer = connectServer; 89 | } 90 | 91 | public Stage getStage() { 92 | return stage; 93 | } 94 | 95 | public void setStage(Stage stage) { 96 | this.stage = stage; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/controllers/PocketOauthController.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.controllers; 2 | 3 | import java.io.IOException; 4 | import java.util.ResourceBundle; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | 14 | import de.felixroske.jfxsupport.FXMLController; 15 | import javafx.fxml.FXML; 16 | import javafx.scene.layout.AnchorPane; 17 | import javafx.scene.web.WebView; 18 | import javafx.stage.Stage; 19 | import me.bayang.reader.share.pocket.PocketAccessTokenPayload; 20 | import me.bayang.reader.share.pocket.PocketAccessTokenResponse; 21 | import me.bayang.reader.share.pocket.PocketClient; 22 | import me.bayang.reader.share.pocket.PocketTokenResponse; 23 | import okhttp3.Response; 24 | 25 | @FXMLController 26 | public class PocketOauthController { 27 | 28 | private static Logger LOGGER = LoggerFactory.getLogger(PocketOauthController.class); 29 | 30 | @FXML 31 | private WebView oauthView; 32 | 33 | @FXML 34 | private AnchorPane oauthViewWrapper; 35 | 36 | @Autowired 37 | private PocketClient pocketClient; 38 | 39 | @Resource(name="mapper") 40 | private ObjectMapper mapper; 41 | 42 | private Stage stage; 43 | 44 | private static ResourceBundle bundle = ResourceBundle.getBundle("i18n.translations"); 45 | 46 | public void processLogin() { 47 | Response response = null; 48 | try { 49 | response = pocketClient.getRequestToken(); 50 | if (response.isSuccessful()) { 51 | PocketTokenResponse pr = mapper.readValue(response.body().charStream(), PocketTokenResponse.class); 52 | LOGGER.debug("pocketrequesttoken {}", pr); 53 | pocketClient.setCode(pr.getCode()); 54 | String url = pocketClient.loginApprovalUrl(pr.getCode()); 55 | LOGGER.debug("url {}", url); 56 | oauthView.getEngine().load(url); 57 | } 58 | else { 59 | RssController.snackbarNotify(bundle.getString("connectionFailure")); 60 | LOGGER.error("error header : {}", response.header("X-Error")); 61 | LOGGER.error("failure during initialization of PocketOauthController code {}", response.code()); 62 | } 63 | } catch (Exception ex) { 64 | RssController.snackbarNotify(bundle.getString("connectionFailure")); 65 | LOGGER.error("failure during initialization of oauthController", ex); 66 | } 67 | finally { 68 | try { 69 | response.close(); 70 | } 71 | catch (Exception e) { 72 | /* noop */ 73 | } 74 | } 75 | } 76 | 77 | @FXML 78 | private void initialize() { 79 | oauthView.getEngine().locationProperty().addListener((observable, oldValue, newValue) -> { 80 | // if (pocketClient.getCode() == null || pocketClient.getCode().isEmpty()) { 81 | // return; 82 | // } 83 | if (oldValue != null && newValue != null) { 84 | LOGGER.debug("{} -> {}",oldValue, newValue); 85 | if (newValue.contains(PocketClient.redirectUrlNoScheme)) { 86 | PocketAccessTokenPayload accessTokenPayload = pocketClient.getAccessTokenPayload(); 87 | if (accessTokenPayload == null) { 88 | RssController.snackbarNotify("missing informations or credentials"); 89 | } 90 | LOGGER.debug("pocketaccesstokenpayload {}", accessTokenPayload); 91 | Response accessResponse = pocketClient.getAccessToken(accessTokenPayload); 92 | if (accessResponse.isSuccessful()) { 93 | try { 94 | PocketAccessTokenResponse re = mapper.readValue(accessResponse.body().charStream(), PocketAccessTokenResponse.class); 95 | LOGGER.debug("pocketaccesstokenresponse {}", re); 96 | pocketClient.setAccessToken(re.getAccessToken()); 97 | pocketClient.setUsername(re.getUsername()); 98 | accessResponse.close(); 99 | stage.close(); 100 | } catch (IOException e) { 101 | accessResponse.close(); 102 | LOGGER.error("error processing server response",e); 103 | } 104 | } 105 | else { 106 | RssController.snackbarNotify(bundle.getString("connectionFailure")); 107 | LOGGER.error("error header : {}", accessResponse.header("X-Error")); 108 | LOGGER.error("failure during initialization of PocketOauthController code {}", accessResponse.code()); 109 | accessResponse.close(); 110 | } 111 | } 112 | return; 113 | } 114 | if (oldValue != null) { 115 | LOGGER.debug(oldValue); 116 | } 117 | if (newValue != null) { 118 | LOGGER.debug(newValue); 119 | } 120 | }); 121 | } 122 | 123 | public Stage getStage() { 124 | return stage; 125 | } 126 | 127 | public void setStage(Stage stage) { 128 | this.stage = stage; 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/controllers/PopupWebViewController.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.controllers; 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean; 4 | 5 | import javax.swing.event.HyperlinkEvent; 6 | 7 | import org.codefx.libfx.control.webview.WebViewHyperlinkListener; 8 | import org.codefx.libfx.control.webview.WebViews; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | import com.jfoenix.controls.JFXProgressBar; 14 | import com.jfoenix.controls.JFXRadioButton; 15 | 16 | import de.felixroske.jfxsupport.FXMLController; 17 | import javafx.application.Platform; 18 | import javafx.beans.binding.Bindings; 19 | import javafx.beans.value.ObservableValue; 20 | import javafx.concurrent.Task; 21 | import javafx.concurrent.Worker.State; 22 | import javafx.fxml.FXML; 23 | import javafx.scene.Scene; 24 | import javafx.scene.control.MenuItem; 25 | import javafx.scene.control.ToggleGroup; 26 | import javafx.scene.layout.VBox; 27 | import javafx.scene.web.WebView; 28 | import javafx.stage.Stage; 29 | import me.bayang.reader.FXMain; 30 | import me.bayang.reader.backend.inoreader.ConnectServer; 31 | import me.bayang.reader.mobilizer.MercuryMobilizer; 32 | import me.bayang.reader.mobilizer.MercuryResult; 33 | import me.bayang.reader.rssmodels.Item; 34 | import me.bayang.reader.share.Provider; 35 | import me.bayang.reader.share.pocket.PocketClient; 36 | import me.bayang.reader.share.wallabag.WallabagClient; 37 | import me.bayang.reader.storage.IStorageService; 38 | import me.bayang.reader.view.ShareLinkView; 39 | 40 | @FXMLController 41 | public class PopupWebViewController { 42 | 43 | private static final Logger LOGGER = LoggerFactory.getLogger(PopupWebViewController.class); 44 | 45 | @FXML 46 | private WebView popupWebView; 47 | 48 | @FXML 49 | private JFXRadioButton popupRssRadioButton; 50 | 51 | @FXML 52 | private JFXRadioButton popupWebRadioButton; 53 | 54 | @FXML 55 | private JFXRadioButton mercuryRadioButton; 56 | 57 | @FXML 58 | private JFXProgressBar progressBar; 59 | 60 | @FXML 61 | private VBox container; 62 | 63 | @FXML 64 | private MenuItem pocketShareMenu; 65 | 66 | @FXML 67 | private MenuItem wallabagShareMenu; 68 | 69 | private WebViewHyperlinkListener eventPrintingListener; 70 | 71 | private AtomicBoolean isWebViewListenerAttached = new AtomicBoolean(false); 72 | 73 | private Item currentItem; 74 | 75 | private Stage stage; 76 | 77 | @Autowired 78 | private ShareLinkView shareLinkView; 79 | 80 | @Autowired 81 | private MercuryMobilizer mercuryMobilizer; 82 | 83 | @Autowired 84 | private ConnectServer connectServer; 85 | 86 | @Autowired 87 | private PocketClient pocketClient; 88 | 89 | @Autowired 90 | private WallabagClient wallabagClient; 91 | 92 | @Autowired 93 | private IStorageService configStorage; 94 | 95 | @FXML 96 | private void initialize() { 97 | initWebView(); 98 | progressBar.setVisible(false); 99 | progressBar.prefWidthProperty().bind(container.widthProperty()); 100 | ToggleGroup toggleGroup = new ToggleGroup(); 101 | popupRssRadioButton.setToggleGroup(toggleGroup); 102 | popupWebRadioButton.setToggleGroup(toggleGroup); 103 | mercuryRadioButton.setToggleGroup(toggleGroup); 104 | popupRssRadioButton.setSelected(true); 105 | toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { 106 | if (toggleGroup.getSelectedToggle() != null) { 107 | if (popupRssRadioButton.isSelected()) { 108 | popupWebView.getEngine().loadContent(Item.processContent(currentItem.getTitle(), currentItem.getSummary().getContent())); 109 | } 110 | else if (popupWebRadioButton.isSelected()) { 111 | popupWebView.getEngine().load(currentItem.getCanonical().get(0).getHref()); 112 | } 113 | else if (mercuryRadioButton.isSelected()) { 114 | launchMercuryTask(); 115 | } 116 | } 117 | }); 118 | pocketShareMenu.disableProperty().bind(Bindings.not(configStorage.pocketEnabledProperty())); 119 | wallabagShareMenu.disableProperty().bind(Bindings.not(configStorage.wallabagEnabledProperty())); 120 | } 121 | 122 | private void initWebView() { 123 | popupWebView.setContextMenuEnabled(false); 124 | eventPrintingListener = event -> { 125 | LOGGER.debug("{}-{}",event.getURL(), event.getSource().getClass().getName()); 126 | FXMain.getAppHostServices().showDocument(event.getURL().toString()); 127 | return true; 128 | }; 129 | popupWebView.getEngine().getLoadWorker().stateProperty().addListener((ObservableValue p, State oldState, State newState) -> { 130 | if (newState == State.SUCCEEDED) { 131 | popupWebView.setDisable(false); 132 | progressBar.setVisible(false); 133 | if (popupWebView.getEngine().getTitle() != null && ! popupWebView.getEngine().getTitle().isEmpty()) { 134 | Platform.runLater(() -> { 135 | stage.setTitle(popupWebView.getEngine().getTitle()); 136 | }); 137 | } 138 | if (! isWebViewListenerAttached.get()) { 139 | isWebViewListenerAttached.set(true); 140 | WebViews.addHyperlinkListener(popupWebView, eventPrintingListener, HyperlinkEvent.EventType.ACTIVATED); 141 | } 142 | } 143 | else if (newState == State.SCHEDULED || newState == State.RUNNING) { 144 | popupWebView.setDisable(true); 145 | progressBar.setVisible(true); 146 | } 147 | else { 148 | popupWebView.setDisable(false); 149 | progressBar.setVisible(false); 150 | } 151 | }); 152 | } 153 | 154 | @FXML 155 | public void shareItemPocket() { 156 | if (! pocketClient.isConfigured()) { 157 | return; 158 | } 159 | shareItem(Provider.POCKET); 160 | } 161 | 162 | @FXML 163 | public void shareItemWallabag() { 164 | if (! wallabagClient.isConfigured()) { 165 | return; 166 | } 167 | shareItem(Provider.WALLABAG); 168 | } 169 | 170 | private void shareItem(Provider provider) { 171 | if (currentItem != null) { 172 | if (FXMain.shareLinkStage == null) { 173 | FXMain.createShareLinkStage(); 174 | Scene scene = new Scene(shareLinkView.getView()); 175 | FXMain.shareLinkStage.setScene(scene); 176 | FXMain.shareLinkController = (ShareLinkController) shareLinkView.getPresenter(); 177 | FXMain.shareLinkController.setStage(FXMain.shareLinkStage); 178 | FXMain.shareLinkController.setCurrentItem(currentItem); 179 | FXMain.shareLinkController.setCurrentProvider(provider); 180 | // Show the dialog and wait until the user closes it 181 | FXMain.shareLinkStage.showAndWait(); 182 | } 183 | else { 184 | FXMain.shareLinkController.setCurrentItem(currentItem); 185 | FXMain.shareLinkController.setCurrentProvider(provider); 186 | // Show the dialog and wait until the user closes it 187 | FXMain.shareLinkStage.showAndWait(); 188 | } 189 | } 190 | } 191 | 192 | private void launchMercuryTask() { 193 | String href = this.currentItem.getCanonical().get(0).getHref(); 194 | Task t = mercuryMobilizer.getMercuryResultTask(href); 195 | LOGGER.debug("{}", href); 196 | t.setOnSucceeded(e -> { 197 | progressBar.setVisible(false); 198 | MercuryResult m = t.getValue(); 199 | Platform.runLater(() -> { 200 | stage.setTitle(m.getTitle()); 201 | }); 202 | // LOGGER.debug("{}",m); 203 | if (m != null && ! m.getContent().isEmpty()) { 204 | String url = m.getUrl(); 205 | if (url.startsWith("/")) { 206 | url = href; 207 | } 208 | popupWebView.getEngine().loadContent(MercuryMobilizer.formatContent(m.getImageUrl(), url, m.getContent())); 209 | } 210 | }); 211 | connectServer.getTaskExecutor().submit(t); 212 | progressBar.setVisible(true); 213 | } 214 | 215 | public Item getCurrentItem() { 216 | return currentItem; 217 | } 218 | 219 | public void setCurrentItem(Item currentItem) { 220 | this.currentItem = currentItem; 221 | popupRssRadioButton.setSelected(true); 222 | popupWebView.getEngine().loadContent(Item.processContent(currentItem.getTitle(), currentItem.getSummary().getContent())); 223 | } 224 | 225 | public Stage getStage() { 226 | return stage; 227 | } 228 | 229 | public void setStage(Stage stage) { 230 | this.stage = stage; 231 | } 232 | 233 | 234 | } 235 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/controllers/SettingsController.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.controllers; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.List; 5 | import java.util.ResourceBundle; 6 | 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | import com.jfoenix.controls.JFXButton; 13 | import com.jfoenix.controls.JFXComboBox; 14 | import com.jfoenix.controls.JFXPasswordField; 15 | import com.jfoenix.controls.JFXSnackbar; 16 | import com.jfoenix.controls.JFXSnackbar.SnackbarEvent; 17 | import com.jfoenix.controls.JFXTextField; 18 | import com.jfoenix.controls.JFXToggleButton; 19 | 20 | import de.felixroske.jfxsupport.AbstractFxmlView; 21 | import de.felixroske.jfxsupport.FXMLController; 22 | import javafx.beans.binding.Bindings; 23 | import javafx.fxml.FXML; 24 | import javafx.scene.Scene; 25 | import javafx.scene.control.Label; 26 | import javafx.scene.image.Image; 27 | import javafx.scene.layout.VBox; 28 | import javafx.stage.Modality; 29 | import javafx.stage.Stage; 30 | import me.bayang.reader.FXMain; 31 | import me.bayang.reader.share.wallabag.WallabagClient; 32 | import me.bayang.reader.share.wallabag.WallabagCredentials; 33 | import me.bayang.reader.storage.IStorageService; 34 | import me.bayang.reader.utils.Theme; 35 | import me.bayang.reader.view.PocketOauthView; 36 | import me.bayang.reader.view.RssView; 37 | 38 | @FXMLController 39 | public class SettingsController { 40 | 41 | private static final Logger LOGGER = LoggerFactory.getLogger(SettingsController.class); 42 | 43 | private static ResourceBundle bundle = ResourceBundle.getBundle("i18n.translations"); 44 | 45 | @Autowired 46 | private IStorageService configStorage; 47 | 48 | @FXML 49 | private VBox settingsContainer; 50 | 51 | @FXML 52 | private JFXToggleButton layoutToggle; 53 | 54 | @FXML 55 | private JFXToggleButton pocketActivate; 56 | 57 | @FXML 58 | private JFXToggleButton wallabagActivate; 59 | 60 | @FXML 61 | private JFXButton pocketConfigure; 62 | 63 | @FXML 64 | private JFXButton backButton; 65 | 66 | @FXML 67 | private Label pocketStatus; 68 | 69 | @FXML 70 | private JFXComboBox themeComboBox; 71 | 72 | @FXML 73 | private JFXButton wallabagConnect; 74 | 75 | @FXML 76 | private JFXTextField wallabagUrlField; 77 | 78 | @FXML 79 | private JFXTextField wallabagUserField; 80 | 81 | @FXML 82 | private JFXPasswordField wallabagPasswordField; 83 | 84 | @FXML 85 | private JFXTextField wallabagClientIdField; 86 | 87 | @FXML 88 | private JFXTextField wallabagClientSecretField; 89 | 90 | @Autowired 91 | private PocketOauthView pocketOauthView; 92 | private PocketOauthController pocketOauthController; 93 | private Stage pocketOauthStage; 94 | 95 | private static JFXSnackbar snackbar; 96 | 97 | @Autowired 98 | public List views; 99 | 100 | @Autowired 101 | private WallabagClient wallabagClient; 102 | 103 | @FXML 104 | public void initialize() { 105 | for (Theme t : Theme.values()) { 106 | themeComboBox.getItems().add(t.getDisplayName()); 107 | } 108 | themeComboBox.getSelectionModel().select(configStorage.getAppTheme().getDisplayName()); 109 | pocketActivate.selectedProperty().bindBidirectional(configStorage.pocketEnabledProperty()); 110 | layoutToggle.selectedProperty().bindBidirectional(configStorage.prefersGridLayoutProperty()); 111 | pocketStatus.textProperty().bind(Bindings.when(configStorage.pocketUserProperty().isEmpty()) 112 | .then("") 113 | .otherwise(MessageFormat.format(bundle.getString("settingsShareProviderStatus"), configStorage.pocketUserProperty().getValue()))); 114 | wallabagActivate.selectedProperty().bindBidirectional(configStorage.wallabagEnabledProperty()); 115 | initWallabagFieldsBindings(); 116 | snackbar = new JFXSnackbar(settingsContainer); 117 | } 118 | 119 | public void initWallabagFieldsBindings() { 120 | wallabagClientIdField.disableProperty().bind(Bindings.not(wallabagActivate.selectedProperty())); 121 | wallabagClientSecretField.disableProperty().bind(Bindings.not(wallabagActivate.selectedProperty())); 122 | wallabagUrlField.disableProperty().bind(Bindings.not(wallabagActivate.selectedProperty())); 123 | wallabagPasswordField.disableProperty().bind(Bindings.not(wallabagActivate.selectedProperty())); 124 | wallabagUserField.disableProperty().bind(Bindings.not(wallabagActivate.selectedProperty())); 125 | 126 | } 127 | 128 | @FXML 129 | public void showMainScreen() { 130 | FXMain.showView(RssView.class); 131 | } 132 | 133 | @FXML 134 | public void showPocketOauthStage() { 135 | if (pocketOauthStage == null) { 136 | Stage dialogStage = new Stage(); 137 | dialogStage.setTitle(bundle.getString("pocketLogin")); 138 | dialogStage.initModality(Modality.WINDOW_MODAL); 139 | dialogStage.initOwner(FXMain.getStage()); 140 | dialogStage.getIcons().add(new Image("icon.png")); 141 | dialogStage.setResizable(true); 142 | Scene scene = new Scene(pocketOauthView.getView()); 143 | dialogStage.setScene(scene); 144 | this.pocketOauthStage = dialogStage; 145 | pocketOauthController = (PocketOauthController) pocketOauthView.getPresenter(); 146 | pocketOauthController.setStage(dialogStage); 147 | // Show the dialog and wait until the user closes it 148 | pocketOauthController.processLogin(); 149 | dialogStage.showAndWait(); 150 | } 151 | else { 152 | pocketOauthController.processLogin(); 153 | pocketOauthStage.showAndWait(); 154 | } 155 | } 156 | 157 | @FXML 158 | public void changeTheme() { 159 | LOGGER.debug("theme : {}", themeComboBox.getValue()); 160 | Theme theme = Theme.forDisplayName(themeComboBox.getValue()); 161 | if (theme != null) { 162 | configStorage.setAppTheme(theme); 163 | setTheme(theme); 164 | } 165 | } 166 | 167 | public void setTheme(Theme theme) { 168 | LOGGER.debug("changing theme to {}", theme.getPath()); 169 | FXMain.getScene().getStylesheets().clear(); 170 | FXMain.setUserAgentStylesheet(null); 171 | FXMain.setUserAgentStylesheet(FXMain.STYLESHEET_MODENA); 172 | FXMain.getScene().getStylesheets().add(getClass().getResource(theme.getPath()).toExternalForm()); 173 | 174 | for (AbstractFxmlView v : views) { 175 | v.getView().getStylesheets().clear(); 176 | v.getView(); 177 | } 178 | } 179 | 180 | @FXML 181 | public void wallabagConnect() { 182 | if (validateWallabagFields()) { 183 | WallabagCredentials credentials = 184 | new WallabagCredentials(wallabagUrlField.getText(), wallabagUserField.getText(), 185 | wallabagPasswordField.getText(), wallabagClientIdField.getText(), 186 | wallabagClientSecretField.getText(), null, null); 187 | if (wallabagClient.testCredentials(credentials)) { 188 | snackbarNotify(FXMain.bundle.getString("reachedServer"), 2000); 189 | } 190 | else { 191 | snackbarNotify(FXMain.bundle.getString("failedVerification"), 2500); 192 | } 193 | } 194 | else { 195 | snackbarNotify(FXMain.bundle.getString("allFieldsMandatory"), 2000); 196 | } 197 | } 198 | 199 | private boolean validateWallabagFields() { 200 | if (!StringUtils.isBlank(wallabagClientIdField.getText()) 201 | && ! StringUtils.isBlank(wallabagClientSecretField.getText()) 202 | && ! StringUtils.isBlank(wallabagPasswordField.getText()) 203 | && ! StringUtils.isBlank(wallabagUserField.getText()) 204 | && ! StringUtils.isBlank(wallabagUrlField.getText())) { 205 | return true; 206 | } 207 | return false; 208 | } 209 | 210 | public static void snackbarNotify(String msg, long duration) { 211 | if (msg != null) { 212 | snackbar.enqueue(new SnackbarEvent(msg, null, duration, false, null)); 213 | } 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/controllers/ShareLinkController.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.controllers; 2 | 3 | import java.io.IOException; 4 | import java.text.MessageFormat; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | import com.jfoenix.controls.JFXButton; 14 | import com.jfoenix.controls.JFXTextArea; 15 | import com.jfoenix.controls.JFXTextField; 16 | 17 | import de.felixroske.jfxsupport.FXMLController; 18 | import javafx.fxml.FXML; 19 | import javafx.scene.layout.FlowPane; 20 | import javafx.stage.Stage; 21 | import me.bayang.reader.FXMain; 22 | import me.bayang.reader.components.DeletableLabel; 23 | import me.bayang.reader.rssmodels.Item; 24 | import me.bayang.reader.share.Provider; 25 | import me.bayang.reader.share.pocket.PocketClient; 26 | import me.bayang.reader.share.wallabag.WallabagClient; 27 | import me.bayang.reader.view.ShareLinkView; 28 | import okhttp3.Call; 29 | import okhttp3.Callback; 30 | import okhttp3.Request; 31 | import okhttp3.Response; 32 | 33 | @FXMLController 34 | public class ShareLinkController { 35 | 36 | private static Logger LOGGER = LoggerFactory.getLogger(ShareLinkController.class); 37 | 38 | @Autowired 39 | private ShareLinkView view; 40 | 41 | @FXML 42 | private JFXTextArea linkField; 43 | 44 | @FXML 45 | private JFXTextField tagField; 46 | 47 | @FXML 48 | private JFXButton addTagButton; 49 | 50 | @FXML 51 | private FlowPane tagsContainer; 52 | 53 | @FXML 54 | private JFXButton submitButton; 55 | 56 | @Autowired 57 | private PocketClient pocketClient; 58 | 59 | @Autowired 60 | private WallabagClient wallabagClient; 61 | 62 | private Item currentItem; 63 | 64 | private Stage stage; 65 | 66 | private Provider currentProvider; 67 | 68 | @FXML 69 | public void initialize() { 70 | 71 | } 72 | 73 | private void submitToPocket() { 74 | Request r = pocketClient.addLink(linkField.getText(), formatTags()); 75 | pocketClient.getHttpClient().newCall(r).enqueue(new Callback() { 76 | @Override 77 | public void onResponse(Call call, Response response) throws IOException { 78 | if (response.code() != 200) { 79 | LOGGER.debug("Pocket : non-200 code in onResponse : {} - message : {}", response.code(), response.header("X-Error")); 80 | RssController.snackbarNotify("Message from Pocket server : " + response.header("X-Error")); 81 | } 82 | if (response.isSuccessful()) { 83 | LOGGER.debug("successfully added link to Pocket"); 84 | } 85 | try { 86 | response.close(); 87 | } 88 | catch (Exception e) { 89 | /* noop */ 90 | } 91 | } 92 | 93 | @Override 94 | public void onFailure(Call call, IOException e) { 95 | RssController.snackbarNotify("Error while sending link to Pocket"); 96 | LOGGER.error("pocket onFailure in sending", e); 97 | } 98 | }); 99 | this.stage.close(); 100 | } 101 | 102 | private void submitToWallabag() { 103 | if (! tagsContainer.getChildren().isEmpty()) { 104 | List tags = tagsContainer.getChildrenUnmodifiable().stream().map(l -> ((DeletableLabel) l).getContent()).collect(Collectors.toList()); 105 | wallabagClient.submitLink(linkField.getText(), tags); 106 | } 107 | else { 108 | wallabagClient.submitLink(linkField.getText(), Collections.emptyList()); 109 | } 110 | this.stage.close(); 111 | } 112 | 113 | @FXML 114 | public void submitLink() { 115 | LOGGER.debug("link {}, tags {}", linkField.getText(), formatTags()); 116 | if (this.getCurrentProvider() == Provider.POCKET) { 117 | submitToPocket(); 118 | } 119 | else if (this.getCurrentProvider() == Provider.WALLABAG) { 120 | submitToWallabag(); 121 | } 122 | } 123 | 124 | @FXML 125 | public void addTag() { 126 | if (! tagField.getText().isEmpty()) { 127 | DeletableLabel l = new DeletableLabel(tagField.getText()); 128 | l.getDeleteCross().setOnMouseClicked(e -> { 129 | tagsContainer.getChildren().remove(l); 130 | }); 131 | tagsContainer.getChildren().add(l); 132 | tagField.setText(""); 133 | } 134 | } 135 | 136 | private String formatTags() { 137 | if (! tagsContainer.getChildren().isEmpty()) { 138 | return tagsContainer.getChildren().stream().map(l -> ((DeletableLabel) l).getContent()).collect(Collectors.joining(",")); 139 | } 140 | else { 141 | return ""; 142 | } 143 | } 144 | 145 | public Item getCurrentItem() { 146 | return currentItem; 147 | } 148 | 149 | public void setCurrentItem(Item currentItem) { 150 | this.currentItem = currentItem; 151 | this.linkField.setText(currentItem.getCanonical().get(0).getHref()); 152 | this.tagField.setText(""); 153 | this.tagsContainer.getChildren().clear(); 154 | } 155 | 156 | public Stage getStage() { 157 | return stage; 158 | } 159 | 160 | public void setStage(Stage stage) { 161 | this.stage = stage; 162 | } 163 | 164 | public Provider getCurrentProvider() { 165 | return currentProvider; 166 | } 167 | 168 | public void setCurrentProvider(Provider currentProvider) { 169 | this.currentProvider = currentProvider; 170 | this.stage.setTitle(MessageFormat.format(FXMain.bundle.getString("shareLinkStageTitle"), currentProvider.toString())); 171 | } 172 | 173 | 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/mobilizer/MercuryMobilizer.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.mobilizer; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.util.ResourceBundle; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import javax.annotation.PostConstruct; 10 | import javax.annotation.Resource; 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.stereotype.Service; 16 | 17 | import com.fasterxml.jackson.databind.ObjectMapper; 18 | 19 | import javafx.concurrent.Task; 20 | import me.bayang.reader.backend.inoreader.ConnectServer; 21 | import me.bayang.reader.controllers.RssController; 22 | import okhttp3.HttpUrl; 23 | import okhttp3.OkHttpClient; 24 | import okhttp3.Request; 25 | import okhttp3.Response; 26 | 27 | @Service 28 | public class MercuryMobilizer { 29 | 30 | private static Logger LOGGER = LoggerFactory.getLogger(MercuryMobilizer.class); 31 | 32 | @Value("${mercury.key}") 33 | private String appKey; 34 | 35 | // https://mercury.postlight.com/parser?url= 36 | private static final String APP_URL = "https://mercury.postlight.com/parser"; 37 | 38 | private static final HttpUrl APP_HTTP_URL = HttpUrl.parse(APP_URL); 39 | 40 | private OkHttpClient okClient; 41 | 42 | private ResourceBundle bundle = ResourceBundle.getBundle("i18n.translations"); 43 | 44 | @Resource(name="mapper") 45 | private ObjectMapper mapper; 46 | 47 | @PostConstruct 48 | public void initClient() { 49 | okClient = new OkHttpClient.Builder() 50 | .followRedirects(true) 51 | .followSslRedirects(true) 52 | .readTimeout(1, TimeUnit.MINUTES) 53 | .connectTimeout(1, TimeUnit.MINUTES) 54 | .build(); 55 | } 56 | 57 | public MercuryResult getMercuryResult(String url) { 58 | HttpUrl parameterUrl = APP_HTTP_URL.newBuilder() 59 | .setQueryParameter("url", url) 60 | .build(); 61 | Request request = new Request.Builder() 62 | .url(parameterUrl) 63 | .addHeader("x-api-key", appKey) 64 | .addHeader("Content-Type", "application/json") 65 | .build(); 66 | try { 67 | Response r = okClient.newCall(request).execute(); 68 | if (r.isSuccessful()) { 69 | BufferedReader reader = new BufferedReader(new InputStreamReader(r.body().source().inputStream(), "utf-8")); 70 | MercuryResult m = mapper.readValue(reader, MercuryResult.class); 71 | ConnectServer.closeReader(reader); 72 | return m; 73 | } else { 74 | RssController.snackbarNotify(bundle.getString("connectionFailure")); 75 | r.close(); 76 | LOGGER.debug("mercury mobilizer error code {} for {}",r.code(), request.url()); 77 | } 78 | } catch (IOException e) { 79 | RssController.snackbarNotify(bundle.getString("connectionFailure")); 80 | } 81 | return null; 82 | } 83 | 84 | public Task getMercuryResultTask(String url) { 85 | Task t = new Task() { 86 | @Override 87 | protected MercuryResult call() throws Exception { 88 | LOGGER.debug("starting to fetch mercury for url {}", url); 89 | return getMercuryResult(url); 90 | } 91 | }; 92 | return t; 93 | } 94 | 95 | public static String formatContent(String imgUrl, String pageUrl, String content) { 96 | StringBuilder sb = new StringBuilder(); 97 | sb.append(" "); 98 | sb.append("
"); 101 | sb.append("
"); 102 | if (! pageUrl.startsWith("/")) { 103 | sb.append(""); 109 | } 110 | 111 | sb.append(content); 112 | sb.append("
"); 113 | return sb.toString(); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/mobilizer/MercuryResult.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.mobilizer; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAlias; 4 | 5 | public class MercuryResult { 6 | 7 | private String title; 8 | 9 | private String content; 10 | 11 | @JsonAlias({"date_published"}) 12 | private String datePublished; 13 | 14 | @JsonAlias({"lead_image_url"}) 15 | private String imageUrl; 16 | 17 | private String dek; 18 | 19 | private String url; 20 | 21 | private String domain; 22 | 23 | private String excerpt; 24 | 25 | @JsonAlias({"word_count"}) 26 | private int wordCount; 27 | 28 | private String direction; 29 | 30 | @JsonAlias({"total_pages"}) 31 | private int totalPages; 32 | 33 | @JsonAlias({"rendered_pages"}) 34 | private int renderedPages; 35 | 36 | @JsonAlias({"next_page_url"}) 37 | private String nextPageUrl; 38 | 39 | public String getTitle() { 40 | return title; 41 | } 42 | 43 | public void setTitle(String title) { 44 | this.title = title; 45 | } 46 | 47 | public String getContent() { 48 | return content; 49 | } 50 | 51 | public void setContent(String content) { 52 | this.content = content; 53 | } 54 | 55 | public String getDatePublished() { 56 | return datePublished; 57 | } 58 | 59 | public void setDatePublished(String datePublished) { 60 | this.datePublished = datePublished; 61 | } 62 | 63 | public String getImageUrl() { 64 | return imageUrl; 65 | } 66 | 67 | public void setImageUrl(String imageUrl) { 68 | this.imageUrl = imageUrl; 69 | } 70 | 71 | public String getDek() { 72 | return dek; 73 | } 74 | 75 | public void setDek(String dek) { 76 | this.dek = dek; 77 | } 78 | 79 | public String getUrl() { 80 | return url; 81 | } 82 | 83 | public void setUrl(String url) { 84 | this.url = url; 85 | } 86 | 87 | public String getDomain() { 88 | return domain; 89 | } 90 | 91 | public void setDomain(String domain) { 92 | this.domain = domain; 93 | } 94 | 95 | public String getExcerpt() { 96 | return excerpt; 97 | } 98 | 99 | public void setExcerpt(String excerpt) { 100 | this.excerpt = excerpt; 101 | } 102 | 103 | public int getWordCount() { 104 | return wordCount; 105 | } 106 | 107 | public void setWordCount(int wordCount) { 108 | this.wordCount = wordCount; 109 | } 110 | 111 | public String getDirection() { 112 | return direction; 113 | } 114 | 115 | public void setDirection(String direction) { 116 | this.direction = direction; 117 | } 118 | 119 | public int getTotalPages() { 120 | return totalPages; 121 | } 122 | 123 | public void setTotalPages(int totalPages) { 124 | this.totalPages = totalPages; 125 | } 126 | 127 | public int getRenderedPages() { 128 | return renderedPages; 129 | } 130 | 131 | public void setRenderedPages(int renderedPages) { 132 | this.renderedPages = renderedPages; 133 | } 134 | 135 | public String getNextPageUrl() { 136 | return nextPageUrl; 137 | } 138 | 139 | public void setNextPageUrl(String nextPageUrl) { 140 | this.nextPageUrl = nextPageUrl; 141 | } 142 | 143 | @Override 144 | public String toString() { 145 | StringBuilder builder = new StringBuilder(); 146 | builder.append("MercuryResult [title=").append(title) 147 | .append(", content=").append(content).append(", datePublished=") 148 | .append(datePublished).append(", imageUrl=").append(imageUrl) 149 | .append(", dek=").append(dek).append(", url=").append(url) 150 | .append(", domain=").append(domain).append(", excerpt=") 151 | .append(excerpt).append(", wordCount=").append(wordCount) 152 | .append(", direction=").append(direction) 153 | .append(", totalPages=").append(totalPages) 154 | .append(", renderedPages=").append(renderedPages) 155 | .append(", nextPageUrl=").append(nextPageUrl).append("]"); 156 | return builder.toString(); 157 | } 158 | 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/AddResult.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | public class AddResult { 4 | private String query; 5 | private int numResults; 6 | private String streamId; 7 | private String streamName; 8 | 9 | public AddResult() { 10 | } 11 | 12 | public AddResult(String query, int numResults, String streamId, String streamName) { 13 | this.query = query; 14 | this.numResults = numResults; 15 | this.streamId = streamId; 16 | this.streamName = streamName; 17 | } 18 | 19 | public String getQuery() { 20 | return query; 21 | } 22 | 23 | public void setQuery(String query) { 24 | this.query = query; 25 | } 26 | 27 | public int getNumResults() { 28 | return numResults; 29 | } 30 | 31 | public void setNumResults(int numResults) { 32 | this.numResults = numResults; 33 | } 34 | 35 | public String getStreamId() { 36 | return streamId; 37 | } 38 | 39 | public void setStreamId(String streamId) { 40 | this.streamId = streamId; 41 | } 42 | 43 | public String getStreamName() { 44 | return streamName; 45 | } 46 | 47 | public void setStreamName(String streamName) { 48 | this.streamName = streamName; 49 | } 50 | 51 | 52 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Alternate.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | public class Alternate{ 4 | private String href; 5 | private String type; 6 | 7 | public Alternate() { 8 | } 9 | 10 | public Alternate(String href, String type) { 11 | this.href = href; 12 | this.type = type; 13 | } 14 | 15 | public String getHref() { 16 | return href; 17 | } 18 | 19 | public String getType() { 20 | return type; 21 | } 22 | 23 | public void setHref(String href) { 24 | this.href = href; 25 | } 26 | 27 | public void setType(String type) { 28 | this.type = type; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | StringBuilder builder = new StringBuilder(); 34 | builder.append("Alternate [href=").append(href).append(", type=") 35 | .append(type).append("]"); 36 | return builder.toString(); 37 | } 38 | 39 | 40 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Canonical.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | /** 4 | * Created by zz on 2016/12/10. 5 | */ 6 | public class Canonical{ 7 | 8 | private String href; 9 | 10 | public Canonical() { 11 | } 12 | 13 | public Canonical(String href) { 14 | this.href = href; 15 | } 16 | 17 | public String getHref() { 18 | return href; 19 | } 20 | 21 | public void setHref(String href) { 22 | this.href = href; 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | StringBuilder builder = new StringBuilder(); 28 | builder.append("Canonical [href=").append(href).append("]"); 29 | return builder.toString(); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Categories.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | public class Categories implements Feed { 4 | private String id; 5 | private String label; 6 | 7 | public Categories() { 8 | } 9 | 10 | public Categories(String id, String label) { 11 | this.id = id; 12 | this.label = label; 13 | } 14 | 15 | @Override 16 | public String getId() { 17 | return id; 18 | } 19 | 20 | @Override 21 | public String getLabel() { 22 | return label; 23 | } 24 | 25 | @Override 26 | public String getSortid() { 27 | return null; 28 | } 29 | 30 | public void setId(String id) { 31 | this.id = id; 32 | } 33 | 34 | public void setLabel(String label) { 35 | this.label = label; 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | StringBuilder builder = new StringBuilder(); 41 | builder.append("Categories [id=").append(id).append(", label=") 42 | .append(label).append("]"); 43 | return builder.toString(); 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Feed.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | /** 4 | * An interface created to combine Tag, Categories and Subscription, used in the TreeView. 5 | */ 6 | public interface Feed { 7 | String getId(); 8 | String getSortid(); 9 | String getLabel(); 10 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/FoldersTagsList.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * A FolderTagsList represents a object get from Inoreader's Folders and tags list 7 | */ 8 | public class FoldersTagsList { 9 | 10 | private ArrayList tags; 11 | 12 | public FoldersTagsList() { 13 | } 14 | 15 | public FoldersTagsList(ArrayList tags) { 16 | this.tags = tags; 17 | } 18 | 19 | public ArrayList getTags() { 20 | return tags; 21 | } 22 | 23 | public void setTags(ArrayList tags) { 24 | this.tags = tags; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | StringBuilder builder = new StringBuilder(); 30 | builder.append("FoldersTagsList [tags=").append(tags).append("]"); 31 | return builder.toString(); 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Item.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | import java.util.ArrayList; 3 | 4 | import javafx.beans.property.BooleanProperty; 5 | import javafx.beans.property.SimpleBooleanProperty; 6 | 7 | public class Item{ 8 | 9 | private String crawlTimeMsec; 10 | private String timestampUsec; 11 | private String id; 12 | private ArrayList categories; 13 | private String title; 14 | private long published; 15 | private long updated; 16 | private ArrayList canonical; 17 | private ArrayList alternate; 18 | private Summary summary; 19 | private String author; 20 | private ArrayList likingUsers; 21 | private ArrayList comments; 22 | private int commentsNum; 23 | private ArrayList annotations; 24 | private Origin origin; 25 | private boolean isRead = false;//doesn't include in the stream, but add this boolean to determine the color in listView 26 | private BooleanProperty readPropperty = new SimpleBooleanProperty(isRead); 27 | 28 | public Item() { 29 | } 30 | 31 | public Item(String crawlTimeMsec, String timestampUsec, String id, ArrayList categories, String title, long published, long updated, ArrayList canonical, ArrayList alternate, Summary summary, String author, ArrayList likingUsers, ArrayList comments, int commentsNum, ArrayList annotations, Origin origin) { 32 | this.crawlTimeMsec = crawlTimeMsec; 33 | this.timestampUsec = timestampUsec; 34 | this.id = id; 35 | this.categories = categories; 36 | this.title = title; 37 | this.published = published; 38 | this.updated = updated; 39 | this.canonical = canonical; 40 | this.alternate = alternate; 41 | this.summary = summary; 42 | this.author = author; 43 | this.likingUsers = likingUsers; 44 | this.comments = comments; 45 | this.commentsNum = commentsNum; 46 | this.annotations = annotations; 47 | this.origin = origin; 48 | } 49 | 50 | public String getCrawlTimeMsec() { 51 | return crawlTimeMsec; 52 | } 53 | 54 | public String getTimestampUsec() { 55 | return timestampUsec; 56 | } 57 | 58 | public String getId() { 59 | return id; 60 | } 61 | 62 | public ArrayList getCategories() { 63 | return categories; 64 | } 65 | 66 | public String getTitle() { 67 | return title; 68 | } 69 | 70 | public long getPublished() { 71 | return published; 72 | } 73 | 74 | public long getUpdated() { 75 | return updated; 76 | } 77 | 78 | public ArrayList getCanonical() { 79 | return canonical; 80 | } 81 | 82 | public ArrayList getAlternate() { 83 | return alternate; 84 | } 85 | 86 | public Summary getSummary() { 87 | return summary; 88 | } 89 | 90 | public String getAuthor() { 91 | return author; 92 | } 93 | 94 | public ArrayList getLikingUsers() { 95 | return likingUsers; 96 | } 97 | 98 | public ArrayList getComments() { 99 | return comments; 100 | } 101 | 102 | public int getCommentsNum() { 103 | return commentsNum; 104 | } 105 | 106 | public ArrayList getAnnotations() { 107 | return annotations; 108 | } 109 | 110 | public Origin getOrigin() { 111 | return origin; 112 | } 113 | 114 | public boolean isRead() { 115 | return readProperty().get(); 116 | } 117 | 118 | public void setRead(boolean read) { 119 | readProperty().set(read); 120 | } 121 | 122 | public String getDecimalId() { 123 | return String.valueOf(Long.parseLong(id.substring(id.lastIndexOf("/") + 1), 16)); 124 | } 125 | 126 | public void setCrawlTimeMsec(String crawlTimeMsec) { 127 | this.crawlTimeMsec = crawlTimeMsec; 128 | } 129 | 130 | public void setTimestampUsec(String timestampUsec) { 131 | this.timestampUsec = timestampUsec; 132 | } 133 | 134 | public void setId(String id) { 135 | this.id = id; 136 | } 137 | 138 | public void setCategories(ArrayList categories) { 139 | this.categories = categories; 140 | } 141 | 142 | public void setTitle(String title) { 143 | this.title = title; 144 | } 145 | 146 | public void setPublished(long published) { 147 | this.published = published; 148 | } 149 | 150 | public void setUpdated(long updated) { 151 | this.updated = updated; 152 | } 153 | 154 | public void setCanonical(ArrayList canonical) { 155 | this.canonical = canonical; 156 | } 157 | 158 | public void setAlternate(ArrayList alternate) { 159 | this.alternate = alternate; 160 | } 161 | 162 | public void setSummary(Summary summary) { 163 | this.summary = summary; 164 | } 165 | 166 | public void setAuthor(String author) { 167 | this.author = author; 168 | } 169 | 170 | public void setLikingUsers(ArrayList likingUsers) { 171 | this.likingUsers = likingUsers; 172 | } 173 | 174 | public void setComments(ArrayList comments) { 175 | this.comments = comments; 176 | } 177 | 178 | public void setCommentsNum(int commentsNum) { 179 | this.commentsNum = commentsNum; 180 | } 181 | 182 | public void setAnnotations(ArrayList annotations) { 183 | this.annotations = annotations; 184 | } 185 | 186 | public void setOrigin(Origin origin) { 187 | this.origin = origin; 188 | } 189 | 190 | public BooleanProperty readProperty() { 191 | return readPropperty; 192 | } 193 | 194 | @Override 195 | public String toString() { 196 | StringBuilder builder = new StringBuilder(); 197 | builder.append("Item [crawlTimeMsec=").append(crawlTimeMsec) 198 | .append(", timestampUsec=").append(timestampUsec) 199 | .append(", id=").append(id).append(", categories=") 200 | .append(categories).append(", title=").append(title) 201 | .append(", published=").append(published).append(", updated=") 202 | .append(updated).append(", canonical=").append(canonical) 203 | .append(", alternate=").append(alternate).append(", summary=") 204 | .append(summary).append(", author=").append(author) 205 | .append(", likingUsers=").append(likingUsers) 206 | .append(", comments=").append(comments).append(", commentsNum=") 207 | .append(commentsNum).append(", annotations=") 208 | .append(annotations).append(", origin=").append(origin) 209 | .append(", isRead=").append(isRead).append("]"); 210 | return builder.toString(); 211 | } 212 | 213 | /** 214 | * Process the content to show in the RSS and Readability view, such as combine title and content, add background color 215 | * @param title 216 | * @param content 217 | * @return 218 | */ 219 | public static String processContent(String title, String content) { 220 | StringBuilder sb = new StringBuilder(); 221 | sb.append(" "); 222 | sb.append("

"); 223 | sb.append(title); 224 | sb.append("

"); 225 | sb.append(content); 226 | sb.append("
"); 227 | return sb.toString(); 228 | } 229 | 230 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/ItemId.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * A ItemId represents a object get from Inoreader's Item IDs 7 | */ 8 | public class ItemId { 9 | private ArrayList items; 10 | private ArrayList itemRefs; 11 | private String continuation; 12 | 13 | public ItemId() { 14 | } 15 | 16 | public ItemId(ArrayList items, ArrayList itemRefs, String continuation) { 17 | this.items = items; 18 | this.itemRefs = itemRefs; 19 | this.continuation = continuation; 20 | } 21 | 22 | public ArrayList getItems() { 23 | return items; 24 | } 25 | 26 | public ArrayList getItemRefs() { 27 | return itemRefs; 28 | } 29 | 30 | public String getContinuation() { 31 | return continuation; 32 | } 33 | 34 | public void setItems(ArrayList items) { 35 | this.items = items; 36 | } 37 | 38 | public void setItemRefs(ArrayList itemRefs) { 39 | this.itemRefs = itemRefs; 40 | } 41 | 42 | public void setContinuation(String continuation) { 43 | this.continuation = continuation; 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/ItemRef.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class ItemRef{ 6 | 7 | private String id; 8 | private ArrayList directStreamIds; 9 | private String timestampUsec; 10 | 11 | public ItemRef() { 12 | } 13 | 14 | public ItemRef(String id, ArrayList directStreamIds, String timestampUsec) { 15 | this.id = id; 16 | this.directStreamIds = directStreamIds; 17 | this.timestampUsec = timestampUsec; 18 | } 19 | 20 | public String getId() { 21 | return id; 22 | } 23 | 24 | public ArrayList getDirectStreamIds() { 25 | return directStreamIds; 26 | } 27 | 28 | public String getTimestampUsec() { 29 | return timestampUsec; 30 | } 31 | 32 | public void setId(String id) { 33 | this.id = id; 34 | } 35 | 36 | public void setDirectStreamIds(ArrayList directStreamIds) { 37 | this.directStreamIds = directStreamIds; 38 | } 39 | 40 | public void setTimestampUsec(String timestampUsec) { 41 | this.timestampUsec = timestampUsec; 42 | } 43 | 44 | 45 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Origin.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | public class Origin{ 4 | 5 | private String streamId; 6 | private String title; 7 | private String htmlUrl; 8 | 9 | public Origin() { 10 | } 11 | 12 | public Origin(String streamId, String title, String htmlUrl) { 13 | this.streamId = streamId; 14 | this.title = title; 15 | this.htmlUrl = htmlUrl; 16 | } 17 | 18 | public String getStreamId() { 19 | return streamId; 20 | } 21 | 22 | public String getTitle() { 23 | return title; 24 | } 25 | 26 | public String getHtmlUrl() { 27 | return htmlUrl; 28 | } 29 | 30 | public void setStreamId(String streamId) { 31 | this.streamId = streamId; 32 | } 33 | 34 | public void setTitle(String title) { 35 | this.title = title; 36 | } 37 | 38 | public void setHtmlUrl(String htmlUrl) { 39 | this.htmlUrl = htmlUrl; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | StringBuilder builder = new StringBuilder(); 45 | builder.append("Origin [streamId=").append(streamId).append(", title=") 46 | .append(title).append(", htmlUrl=").append(htmlUrl).append("]"); 47 | return builder.toString(); 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Self.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | public class Self { 4 | private String href; 5 | 6 | public Self() { 7 | } 8 | 9 | public Self(String href) { 10 | this.href = href; 11 | } 12 | 13 | public String getHref() { 14 | return href; 15 | } 16 | 17 | public void setHref(String href) { 18 | this.href = href; 19 | } 20 | 21 | 22 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/StreamContent.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * A StreamContent represents a object get from Inoreader's Stream Contents 7 | */ 8 | public class StreamContent { 9 | private String direction; 10 | private String id; 11 | private String title; 12 | private String description; 13 | private Self self; 14 | private long updated; 15 | private String updatedUsec; 16 | private ArrayList items; 17 | private String continuation; 18 | 19 | public StreamContent() { 20 | 21 | } 22 | 23 | public StreamContent(String direction, String id, String title, String description, Self self, long updated, String updatedUsec, ArrayList items, String continuation) { 24 | this.direction = direction; 25 | this.id = id; 26 | this.title = title; 27 | this.description = description; 28 | this.self = self; 29 | this.updated = updated; 30 | this.updatedUsec = updatedUsec; 31 | this.items = items; 32 | this.continuation = continuation; 33 | } 34 | 35 | public String getDirection() { 36 | return direction; 37 | } 38 | 39 | public String getId() { 40 | return id; 41 | } 42 | 43 | public String getTitle() { 44 | return title; 45 | } 46 | 47 | public String getDescription() { 48 | return description; 49 | } 50 | 51 | public Self getSelf() { 52 | return self; 53 | } 54 | 55 | public long getUpdated() { 56 | return updated; 57 | } 58 | 59 | public String getUpdatedUsec() { 60 | return updatedUsec; 61 | } 62 | 63 | public ArrayList getItems() { 64 | return items; 65 | } 66 | 67 | public String getContinuation() { 68 | return continuation; 69 | } 70 | 71 | public void setDirection(String direction) { 72 | this.direction = direction; 73 | } 74 | 75 | public void setId(String id) { 76 | this.id = id; 77 | } 78 | 79 | public void setTitle(String title) { 80 | this.title = title; 81 | } 82 | 83 | public void setDescription(String description) { 84 | this.description = description; 85 | } 86 | 87 | public void setSelf(Self self) { 88 | this.self = self; 89 | } 90 | 91 | public void setUpdated(long updated) { 92 | this.updated = updated; 93 | } 94 | 95 | public void setUpdatedUsec(String updatedUsec) { 96 | this.updatedUsec = updatedUsec; 97 | } 98 | 99 | public void setItems(ArrayList items) { 100 | this.items = items; 101 | } 102 | 103 | public void setContinuation(String continuation) { 104 | this.continuation = continuation; 105 | } 106 | 107 | 108 | 109 | } 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Subscription.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class Subscription implements Feed { 6 | 7 | private String id; 8 | private String title; 9 | private ArrayList categories; 10 | private String sortid; 11 | private long firstitemmsec; 12 | private String url; 13 | private String htmlUrl; 14 | private String iconUrl; 15 | 16 | public Subscription() { 17 | } 18 | 19 | public Subscription(String id, String title, ArrayList categories, String sortid, long firstitemmsec, String url, String htmlUrl, String iconUrl) { 20 | this.id = id; 21 | this.title = title; 22 | this.categories = categories; 23 | this.sortid = sortid; 24 | this.firstitemmsec = firstitemmsec; 25 | this.url = url; 26 | this.htmlUrl = htmlUrl; 27 | this.iconUrl = iconUrl; 28 | } 29 | 30 | public String getId() { 31 | return id; 32 | } 33 | 34 | public String getTitle() { 35 | return title; 36 | } 37 | 38 | public ArrayList getCategories() { 39 | return categories; 40 | } 41 | 42 | public String getSortid() { 43 | return sortid; 44 | } 45 | 46 | public long getFirstitemmsec() { 47 | return firstitemmsec; 48 | } 49 | 50 | public String getUrl() { 51 | return url; 52 | } 53 | 54 | public String getHtmlUrl() { 55 | return htmlUrl; 56 | } 57 | 58 | public String getIconUrl() { 59 | return iconUrl; 60 | } 61 | 62 | @Override 63 | public String getLabel() { 64 | return getTitle(); 65 | } 66 | 67 | public void setId(String id) { 68 | this.id = id; 69 | } 70 | 71 | public void setTitle(String title) { 72 | this.title = title; 73 | } 74 | 75 | public void setCategories(ArrayList categories) { 76 | this.categories = categories; 77 | } 78 | 79 | public void setSortid(String sortid) { 80 | this.sortid = sortid; 81 | } 82 | 83 | public void setFirstitemmsec(long firstitemmsec) { 84 | this.firstitemmsec = firstitemmsec; 85 | } 86 | 87 | public void setUrl(String url) { 88 | this.url = url; 89 | } 90 | 91 | public void setHtmlUrl(String htmlUrl) { 92 | this.htmlUrl = htmlUrl; 93 | } 94 | 95 | public void setIconUrl(String iconUrl) { 96 | this.iconUrl = iconUrl; 97 | } 98 | 99 | @Override 100 | public String toString() { 101 | StringBuilder builder = new StringBuilder(); 102 | builder.append("Subscription [id=").append(id).append(", title=") 103 | .append(title).append(", categories=").append(categories) 104 | .append(", sortid=").append(sortid).append(", firstitemmsec=") 105 | .append(firstitemmsec).append(", url=").append(url) 106 | .append(", htmlUrl=").append(htmlUrl).append(", iconUrl=") 107 | .append(iconUrl).append("]"); 108 | return builder.toString(); 109 | } 110 | 111 | 112 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/SubscriptionsList.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * A SubscriptionsList represents a object get from Inoreader's Subscriptions List 7 | */ 8 | public class SubscriptionsList { 9 | 10 | private ArrayList subscriptions; 11 | 12 | public SubscriptionsList() { 13 | super(); 14 | } 15 | 16 | public SubscriptionsList(ArrayList subscriptions) { 17 | this.subscriptions = subscriptions; 18 | } 19 | 20 | public ArrayList getSubscriptions() { 21 | return subscriptions; 22 | } 23 | 24 | public void setSubscriptions(ArrayList subscriptions) { 25 | this.subscriptions = subscriptions; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | StringBuilder builder = new StringBuilder(); 31 | builder.append("SubscriptionsList [subscriptions=") 32 | .append(subscriptions).append("]"); 33 | return builder.toString(); 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Summary.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | public class Summary{ 4 | 5 | private String direction; 6 | private String content; 7 | 8 | public Summary() { 9 | } 10 | 11 | public Summary(String direction, String content) { 12 | this.direction = direction; 13 | this.content = content; 14 | } 15 | 16 | public String getContent() { 17 | return content; 18 | } 19 | 20 | public String getDirection() { 21 | return direction; 22 | } 23 | 24 | public void setDirection(String direction) { 25 | this.direction = direction; 26 | } 27 | 28 | public void setContent(String content) { 29 | this.content = content; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | StringBuilder builder = new StringBuilder(); 35 | builder.append("Summary [direction=").append(direction) 36 | .append(", content=").append(content).append("]"); 37 | return builder.toString(); 38 | } 39 | 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/Tag.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | public class Tag implements Feed { 4 | 5 | private String id; 6 | private String sortid; 7 | 8 | public Tag() { 9 | } 10 | 11 | public Tag(String id, String sortid) { 12 | this.id = id; 13 | this.sortid = sortid; 14 | } 15 | 16 | public String getId() { 17 | return id; 18 | } 19 | 20 | public String getSortid() { 21 | return sortid; 22 | } 23 | 24 | @Override 25 | public String getLabel() { 26 | return getId(); 27 | } 28 | 29 | public void setId(String id) { 30 | this.id = id; 31 | } 32 | 33 | public void setSortid(String sortid) { 34 | this.sortid = sortid; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | StringBuilder builder = new StringBuilder(); 40 | builder.append("Tag [id=").append(id).append(", sortid=").append(sortid) 41 | .append("]"); 42 | return builder.toString(); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/UnreadCounter.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | import java.util.ArrayList; 3 | 4 | /** 5 | * This class is used to get the counts of the unread items. 6 | */ 7 | public class UnreadCounter { 8 | 9 | private String max; 10 | private ArrayList unreadcounts; 11 | 12 | public UnreadCounter() { 13 | } 14 | 15 | public UnreadCounter(String max, ArrayList unreadcounts) { 16 | this.max = max; 17 | this.unreadcounts = unreadcounts; 18 | } 19 | 20 | public String getMax() { 21 | return max; 22 | } 23 | 24 | public ArrayList getUnreadcounts() { 25 | return unreadcounts; 26 | } 27 | 28 | public void setMax(String max) { 29 | this.max = max; 30 | } 31 | 32 | public void setUnreadcounts(ArrayList unreadcounts) { 33 | this.unreadcounts = unreadcounts; 34 | } 35 | 36 | 37 | 38 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/UnreadCounts.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | public class UnreadCounts{ 4 | 5 | private String id; 6 | private int count; 7 | private String newestItemTimestampUsec; 8 | 9 | public UnreadCounts() { 10 | } 11 | 12 | public UnreadCounts(String id, int count, String newestItemTimestampUsec) { 13 | this.id = id; 14 | this.count = count; 15 | this.newestItemTimestampUsec = newestItemTimestampUsec; 16 | } 17 | 18 | public String getId() { 19 | return id; 20 | } 21 | 22 | public int getCount() { 23 | return count; 24 | } 25 | 26 | public String getNewestItemTimestampUsec() { 27 | return newestItemTimestampUsec; 28 | } 29 | 30 | public void setId(String id) { 31 | this.id = id; 32 | } 33 | 34 | public void setCount(int count) { 35 | this.count = count; 36 | } 37 | 38 | public void setNewestItemTimestampUsec(String newestItemTimestampUsec) { 39 | this.newestItemTimestampUsec = newestItemTimestampUsec; 40 | } 41 | 42 | 43 | } -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/rssmodels/UserInformation.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.rssmodels; 2 | 3 | /** 4 | * A UserInformation represents a object get from Inoreader's User information 5 | */ 6 | 7 | public class UserInformation { 8 | 9 | private String userId; 10 | private String userName; 11 | private String userProfileId; 12 | private String userEmail; 13 | private boolean isBloggerUser; 14 | private long signupTimeSec; 15 | private boolean isMultiLoginEnabled; 16 | 17 | public UserInformation() { 18 | super(); 19 | } 20 | 21 | public UserInformation(String userId, String userName, String userProfileId, String userEmail, boolean isBloggerUser, long signupTimeSec, boolean isMultiLoginEnabled) { 22 | this.userId = userId; 23 | this.userName = userName; 24 | this.userProfileId = userProfileId; 25 | this.userEmail = userEmail; 26 | this.isBloggerUser = isBloggerUser; 27 | this.signupTimeSec = signupTimeSec; 28 | this.isMultiLoginEnabled = isMultiLoginEnabled; 29 | } 30 | 31 | public String getUserId() { 32 | return userId; 33 | } 34 | 35 | public String getUserName() { 36 | return userName; 37 | } 38 | 39 | public String getUserProfileId() { 40 | return userProfileId; 41 | } 42 | 43 | public String getUserEmail() { 44 | return userEmail; 45 | } 46 | 47 | public boolean isBloggerUser() { 48 | return isBloggerUser; 49 | } 50 | 51 | public long getSignupTimeSec() { 52 | return signupTimeSec; 53 | } 54 | 55 | public boolean isMultiLoginEnabled() { 56 | return isMultiLoginEnabled; 57 | } 58 | 59 | public void setUserId(String userId) { 60 | this.userId = userId; 61 | } 62 | 63 | public void setUserName(String userName) { 64 | this.userName = userName; 65 | } 66 | 67 | public void setUserProfileId(String userProfileId) { 68 | this.userProfileId = userProfileId; 69 | } 70 | 71 | public void setUserEmail(String userEmail) { 72 | this.userEmail = userEmail; 73 | } 74 | 75 | public void setBloggerUser(boolean isBloggerUser) { 76 | this.isBloggerUser = isBloggerUser; 77 | } 78 | 79 | public void setSignupTimeSec(long signupTimeSec) { 80 | this.signupTimeSec = signupTimeSec; 81 | } 82 | 83 | public void setMultiLoginEnabled(boolean isMultiLoginEnabled) { 84 | this.isMultiLoginEnabled = isMultiLoginEnabled; 85 | } 86 | 87 | @Override 88 | public String toString() { 89 | StringBuilder builder = new StringBuilder(); 90 | builder.append("UserInformation [userId=").append(userId) 91 | .append(", userName=").append(userName) 92 | .append(", userProfileId=").append(userProfileId) 93 | .append(", userEmail=").append(userEmail) 94 | .append(", isBloggerUser=").append(isBloggerUser) 95 | .append(", signupTimeSec=").append(signupTimeSec) 96 | .append(", isMultiLoginEnabled=").append(isMultiLoginEnabled) 97 | .append("]"); 98 | return builder.toString(); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/Provider.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share; 2 | 3 | public enum Provider { 4 | POCKET, 5 | WALLABAG; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/pocket/PocketAccessTokenPayload.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share.pocket; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class PocketAccessTokenPayload { 6 | 7 | @JsonProperty("consumer_key") 8 | private String consumerKey; 9 | 10 | private String code; 11 | 12 | public String getConsumerKey() { 13 | return consumerKey; 14 | } 15 | 16 | public void setConsumerKey(String consumerKey) { 17 | this.consumerKey = consumerKey; 18 | } 19 | 20 | public String getCode() { 21 | return code; 22 | } 23 | 24 | public void setCode(String code) { 25 | this.code = code; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | StringBuilder builder = new StringBuilder(); 31 | builder.append("PocketAccessTokenPayload [consumerKey=") 32 | .append(consumerKey).append(", code=").append(code).append("]"); 33 | return builder.toString(); 34 | } 35 | 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/pocket/PocketAccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share.pocket; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAlias; 4 | 5 | public class PocketAccessTokenResponse { 6 | 7 | @JsonAlias({"access_token"}) 8 | private String accessToken; 9 | 10 | private String username; 11 | 12 | public String getAccessToken() { 13 | return accessToken; 14 | } 15 | 16 | public void setAccessToken(String accessToken) { 17 | this.accessToken = accessToken; 18 | } 19 | 20 | public String getUsername() { 21 | return username; 22 | } 23 | 24 | public void setUsername(String username) { 25 | this.username = username; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | StringBuilder builder = new StringBuilder(); 31 | builder.append("PocketAccessTokenResponse [accessToken=") 32 | .append(accessToken).append(", username=").append(username) 33 | .append("]"); 34 | return builder.toString(); 35 | } 36 | 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/pocket/PocketAddLinkPayload.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share.pocket; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class PocketAddLinkPayload { 6 | 7 | private String url; 8 | 9 | private String title; 10 | 11 | private String tags; 12 | 13 | @JsonProperty("consumer_key") 14 | private String consumerKey; 15 | 16 | @JsonProperty("access_token") 17 | private String accessToken; 18 | 19 | public String getUrl() { 20 | return url; 21 | } 22 | 23 | public void setUrl(String url) { 24 | this.url = url; 25 | } 26 | 27 | public String getTitle() { 28 | return title; 29 | } 30 | 31 | public void setTitle(String title) { 32 | this.title = title; 33 | } 34 | 35 | public String getTags() { 36 | return tags; 37 | } 38 | 39 | public void setTags(String tags) { 40 | this.tags = tags; 41 | } 42 | 43 | public String getConsumerKey() { 44 | return consumerKey; 45 | } 46 | 47 | public void setConsumerKey(String consumerKey) { 48 | this.consumerKey = consumerKey; 49 | } 50 | 51 | public String getAccessToken() { 52 | return accessToken; 53 | } 54 | 55 | public void setAccessToken(String accessToken) { 56 | this.accessToken = accessToken; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | StringBuilder builder = new StringBuilder(); 62 | builder.append("PocketAddLinkPayload [url=").append(url) 63 | .append(", title=").append(title).append(", tags=").append(tags) 64 | .append(", consumerKey=").append(consumerKey) 65 | .append(", accessToken=").append(accessToken).append("]"); 66 | return builder.toString(); 67 | } 68 | 69 | 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/pocket/PocketClient.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share.pocket; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.annotation.PostConstruct; 6 | import javax.annotation.Resource; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Service; 13 | 14 | import com.fasterxml.jackson.core.JsonProcessingException; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | 17 | import me.bayang.reader.FXMain; 18 | import me.bayang.reader.controllers.RssController; 19 | import me.bayang.reader.storage.IStorageService; 20 | import okhttp3.HttpUrl; 21 | import okhttp3.MediaType; 22 | import okhttp3.OkHttpClient; 23 | import okhttp3.Request; 24 | import okhttp3.RequestBody; 25 | import okhttp3.Response; 26 | 27 | @Service 28 | public class PocketClient { 29 | 30 | private static final Logger LOGGER = LoggerFactory.getLogger(PocketClient.class); 31 | 32 | @Value("${pocket.key}") 33 | private String key; 34 | 35 | @Resource(name="mapper") 36 | private ObjectMapper mapper; 37 | 38 | @Autowired 39 | private IStorageService storage; 40 | 41 | @Resource(name="baseOkHttpClient") 42 | private OkHttpClient baseOkHttpClient; 43 | 44 | private static final String requestTokenUrl = "https://getpocket.com/v3/oauth/request"; 45 | public static final HttpUrl requestTokenHTTPUrl = HttpUrl.parse(requestTokenUrl); 46 | 47 | private static final String redirectUrl = "http://localhost:8080/pocket/reader/redirect"; 48 | public static final String redirectUrlNoScheme = "localhost:8080/pocket/reader/redirect"; 49 | 50 | // https://getpocket.com/auth/authorize?request_token=YOUR_REQUEST_TOKEN&redirect_uri=YOUR_REDIRECT_URI 51 | private static final String authorizeUrl = "https://getpocket.com/auth/authorize"; 52 | public static final HttpUrl authorizeHTTPUrl = HttpUrl.parse(authorizeUrl); 53 | 54 | private static final HttpUrl AccessTokenUrl = HttpUrl.parse("https://getpocket.com/v3/oauth/authorize"); 55 | 56 | private static final HttpUrl addLinkUrl = HttpUrl.parse("https://getpocket.com/v3/add"); 57 | 58 | private OkHttpClient httpClient; 59 | 60 | private RequestBody requestTokenBody; 61 | 62 | private String code; 63 | 64 | private String accessToken; 65 | 66 | private String username; 67 | 68 | @PostConstruct 69 | public void initialize() throws JsonProcessingException { 70 | httpClient = baseOkHttpClient.newBuilder().build(); 71 | PocketTokenRequestPayload payload = new PocketTokenRequestPayload(); 72 | payload.setConsumerKey(key); 73 | payload.setRedirectUri(redirectUrl); 74 | requestTokenBody = RequestBody.create(MediaType.parse("application/json"), mapper.writeValueAsString(payload)); 75 | } 76 | 77 | public boolean isConfigured() { 78 | String accessToken = storage.loadPocketToken(); 79 | if (accessToken.isEmpty()) { 80 | RssController.snackbarNotifyBlocking(FXMain.bundle.getString("pocketLoginRequired")); 81 | return false; 82 | } 83 | else { 84 | this.setAccessToken(accessToken); 85 | return true; 86 | } 87 | } 88 | 89 | public Request addLink(String link, String tags) { 90 | if (storage.loadPocketToken().isEmpty()) { 91 | RssController.snackbarNotifyBlocking(FXMain.bundle.getString("pocketLoginRequired")); 92 | return null; 93 | } 94 | else { 95 | PocketAddLinkPayload p = new PocketAddLinkPayload(); 96 | p.setAccessToken(accessToken); 97 | p.setConsumerKey(key); 98 | p.setUrl(link); 99 | if (tags != null && ! tags.isEmpty()) { 100 | p.setTags(tags); 101 | } 102 | try { 103 | RequestBody r = RequestBody.create(MediaType.parse("application/json"), mapper.writeValueAsString(p)); 104 | Request request = new Request.Builder() 105 | .url(addLinkUrl) 106 | .addHeader("Content-Type", "application/json; charset=UTF-8") 107 | .addHeader("X-Accept", "application/json") 108 | .post(r) 109 | .build(); 110 | return request; 111 | } catch (JsonProcessingException e) { 112 | LOGGER.error("error sending link to pocket", e); 113 | } 114 | } 115 | return null; 116 | } 117 | 118 | public Response getRequestToken() throws IOException { 119 | Request request = new Request.Builder() 120 | .url(requestTokenHTTPUrl) 121 | .addHeader("Content-Type", "application/json; charset=UTF-8") 122 | .addHeader("X-Accept", "application/json") 123 | .post(requestTokenBody) 124 | .build(); 125 | return httpClient.newCall(request).execute(); 126 | } 127 | 128 | public String loginApprovalUrl(String requestToken) { 129 | HttpUrl authWithParams = authorizeHTTPUrl.newBuilder() 130 | .addQueryParameter("request_token", requestToken) 131 | .addQueryParameter("redirect_uri", redirectUrl) 132 | .addQueryParameter("mobile", "1") 133 | .build(); 134 | return authWithParams.toString(); 135 | } 136 | 137 | public PocketAccessTokenPayload getAccessTokenPayload() { 138 | if (key == null || getCode() == null || getCode().isEmpty()) { 139 | return null; 140 | } 141 | PocketAccessTokenPayload accessTokenPayload = new PocketAccessTokenPayload(); 142 | accessTokenPayload.setCode(getCode()); 143 | accessTokenPayload.setConsumerKey(key); 144 | return accessTokenPayload; 145 | } 146 | 147 | public Response getAccessToken(PocketAccessTokenPayload payload) { 148 | try { 149 | RequestBody r = RequestBody.create(MediaType.parse("application/json"), mapper.writeValueAsString(payload)); 150 | Request request = new Request.Builder() 151 | .url(AccessTokenUrl) 152 | .addHeader("Content-Type", "application/json; charset=UTF-8") 153 | .addHeader("X-Accept", "application/json") 154 | .post(r) 155 | .build(); 156 | return httpClient.newCall(request).execute(); 157 | 158 | } catch (IOException e) { 159 | LOGGER.error("", e); 160 | } 161 | return null; 162 | } 163 | 164 | public OkHttpClient getHttpClient() { 165 | return httpClient; 166 | } 167 | 168 | public void setHttpClient(OkHttpClient httpClient) { 169 | this.httpClient = httpClient; 170 | } 171 | 172 | public String getCode() { 173 | return code; 174 | } 175 | 176 | public void setCode(String code) { 177 | this.code = code; 178 | } 179 | 180 | public String getAccessToken() { 181 | return accessToken; 182 | } 183 | 184 | public void setAccessToken(String accessToken) { 185 | this.accessToken = accessToken; 186 | storage.savePocketToken(accessToken); 187 | } 188 | 189 | public String getUsername() { 190 | return username; 191 | } 192 | 193 | public void setUsername(String username) { 194 | this.username = username; 195 | storage.setPocketUser(username); 196 | } 197 | 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/pocket/PocketTokenRequestPayload.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share.pocket; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class PocketTokenRequestPayload { 6 | 7 | @JsonProperty("consumer_key") 8 | private String consumerKey; 9 | 10 | @JsonProperty("redirect_uri") 11 | private String redirectUri; 12 | 13 | public String getConsumerKey() { 14 | return consumerKey; 15 | } 16 | 17 | public void setConsumerKey(String consumerKey) { 18 | this.consumerKey = consumerKey; 19 | } 20 | 21 | public String getRedirectUri() { 22 | return redirectUri; 23 | } 24 | 25 | public void setRedirectUri(String redirectUri) { 26 | this.redirectUri = redirectUri; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | StringBuilder builder = new StringBuilder(); 32 | builder.append("PocketTokenRequestPayload [consumerKey=") 33 | .append(consumerKey).append(", redirectUri=") 34 | .append(redirectUri).append("]"); 35 | return builder.toString(); 36 | } 37 | 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/pocket/PocketTokenResponse.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share.pocket; 2 | 3 | public class PocketTokenResponse { 4 | 5 | private String code; 6 | 7 | public String getCode() { 8 | return code; 9 | } 10 | 11 | public void setCode(String code) { 12 | this.code = code; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | StringBuilder builder = new StringBuilder(); 18 | builder.append("PocketTokenResponse [code=").append(code).append("]"); 19 | return builder.toString(); 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/wallabag/WallabagClient.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share.wallabag; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | import javax.annotation.PostConstruct; 7 | import javax.annotation.Resource; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.scheduling.annotation.Async; 13 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 14 | import org.springframework.stereotype.Service; 15 | 16 | import com.di72nn.stuff.wallabag.apiwrapper.BasicParameterHandler; 17 | import com.di72nn.stuff.wallabag.apiwrapper.WallabagService; 18 | import com.di72nn.stuff.wallabag.apiwrapper.exceptions.UnsuccessfulResponseException; 19 | import com.di72nn.stuff.wallabag.apiwrapper.models.Article; 20 | import com.di72nn.stuff.wallabag.apiwrapper.models.Articles; 21 | import com.di72nn.stuff.wallabag.apiwrapper.models.TokenResponse; 22 | 23 | import me.bayang.reader.FXMain; 24 | import me.bayang.reader.controllers.RssController; 25 | import me.bayang.reader.storage.IStorageService; 26 | import okhttp3.OkHttpClient; 27 | 28 | @Service 29 | public class WallabagClient { 30 | 31 | private static Logger LOGGER = LoggerFactory.getLogger(WallabagClient.class); 32 | 33 | @Autowired 34 | private IStorageService storage; 35 | 36 | @Resource(name = "threadPoolTaskExecutor") 37 | private ThreadPoolTaskExecutor taskExecutor; 38 | 39 | private WallabagCredentials credentials; 40 | 41 | private OkHttpClient okHttpClient; 42 | 43 | private WallabagService wallabagService = null; 44 | 45 | @Resource(name="baseOkHttpClient") 46 | private OkHttpClient baseOkHttpClient; 47 | 48 | @PostConstruct 49 | public void initialize() { 50 | this.okHttpClient = baseOkHttpClient.newBuilder().build(); 51 | } 52 | 53 | public boolean isConfigured() { 54 | WallabagCredentials credentials = storage.loadWallabagCredentials(); 55 | if (! credentials.isValid()) { 56 | RssController.snackbarNotifyBlocking(FXMain.bundle.getString("wallabagLoginRequired")); 57 | return false; 58 | } 59 | else { 60 | this.setCredentials(credentials); 61 | initializeService(); 62 | return true; 63 | } 64 | } 65 | 66 | public boolean testCredentials(WallabagCredentials credentials) { 67 | this.setCredentials(credentials); 68 | initializeService(); 69 | try { 70 | Articles ar = this.getWallabagService().getArticlesBuilder().execute(); 71 | storage.saveWallabagCredentials(getCredentials()); 72 | return true; 73 | } catch (IOException | UnsuccessfulResponseException e) { 74 | LOGGER.error("error while testing wallabag credentials", e); 75 | // reset credentials and client 76 | this.setCredentials(null); 77 | this.setWallabagService(null); 78 | return false; 79 | } 80 | } 81 | 82 | @Async("threadPoolTaskExecutor") 83 | public void submitLink(String link, List tags) { 84 | try { 85 | Article article = this.getWallabagService().addArticleBuilder(link).tags(tags).execute(); 86 | LOGGER.debug("saved link '{}' to wallabag", article.url); 87 | } catch (IOException e) { 88 | LOGGER.error("error sending link to wallabag", e); 89 | RssController.snackbarNotify("error sending link to wallabag"); 90 | } catch (UnsuccessfulResponseException e) { 91 | LOGGER.error("error sending link to wallabag, body {}", e.getResponseBody(), e); 92 | RssController.snackbarNotify("error sending link to wallabag"); 93 | } 94 | } 95 | 96 | private void initializeService() { 97 | this.wallabagService = new WallabagService(this.getCredentials().getUrl(), 98 | new BasicParameterHandler( 99 | this.getCredentials().getUsername(), this.getCredentials().getPassword(), this.getCredentials().getClientId(), this.getCredentials().getClientSecret(), 100 | this.getCredentials().getRefreshToken(), this.getCredentials().getAccessToken()) { 101 | @Override 102 | public boolean tokensUpdated(TokenResponse token) { 103 | LOGGER.debug("wallabag token: " + token); 104 | storage.saveWallabagRefreshToken(token.refreshToken); 105 | getCredentials().setRefreshToken(token.refreshToken); 106 | getCredentials().setAccessToken(token.accessToken); 107 | return super.tokensUpdated(token); 108 | } 109 | }, this.okHttpClient); 110 | 111 | } 112 | 113 | public WallabagCredentials getCredentials() { 114 | return credentials; 115 | } 116 | 117 | public void setCredentials(WallabagCredentials credentials) { 118 | this.credentials = credentials; 119 | } 120 | 121 | public WallabagService getWallabagService() { 122 | return wallabagService; 123 | } 124 | 125 | public void setWallabagService(WallabagService wallabagService) { 126 | this.wallabagService = wallabagService; 127 | } 128 | 129 | 130 | 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/share/wallabag/WallabagCredentials.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.share.wallabag; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | public class WallabagCredentials { 6 | 7 | private String url; 8 | 9 | private String username; 10 | 11 | private String password; 12 | 13 | private String clientId; 14 | 15 | private String clientSecret; 16 | 17 | private String refreshToken; 18 | 19 | private String accessToken; 20 | 21 | public WallabagCredentials() { 22 | } 23 | 24 | public WallabagCredentials(String url, String username, String password, 25 | String clientId, String clientSecret, String refreshToken, 26 | String accessToken) { 27 | this.url = url; 28 | this.username = username; 29 | this.password = password; 30 | this.clientId = clientId; 31 | this.clientSecret = clientSecret; 32 | this.refreshToken = refreshToken; 33 | this.accessToken = accessToken; 34 | } 35 | 36 | public boolean isValid() { 37 | return (! StringUtils.isBlank(this.getRefreshToken()) 38 | && ! StringUtils.isBlank(this.getClientId()) 39 | && ! StringUtils.isBlank(this.getClientSecret()) 40 | && ! StringUtils.isBlank(this.getUrl())); 41 | } 42 | 43 | public String getUrl() { 44 | return url; 45 | } 46 | 47 | public void setUrl(String url) { 48 | this.url = url; 49 | } 50 | 51 | public String getUsername() { 52 | return username; 53 | } 54 | 55 | public void setUsername(String username) { 56 | this.username = username; 57 | } 58 | 59 | public String getPassword() { 60 | return password; 61 | } 62 | 63 | public void setPassword(String password) { 64 | this.password = password; 65 | } 66 | 67 | public String getClientId() { 68 | return clientId; 69 | } 70 | 71 | public void setClientId(String clientId) { 72 | this.clientId = clientId; 73 | } 74 | 75 | public String getClientSecret() { 76 | return clientSecret; 77 | } 78 | 79 | public void setClientSecret(String clientSecret) { 80 | this.clientSecret = clientSecret; 81 | } 82 | 83 | public String getRefreshToken() { 84 | return refreshToken; 85 | } 86 | 87 | public void setRefreshToken(String refreshToken) { 88 | this.refreshToken = refreshToken; 89 | } 90 | 91 | public String getAccessToken() { 92 | return accessToken; 93 | } 94 | 95 | public void setAccessToken(String accessToken) { 96 | this.accessToken = accessToken; 97 | } 98 | 99 | @Override 100 | public String toString() { 101 | StringBuilder builder = new StringBuilder(); 102 | builder.append("WallabagCredentials [url=").append(url) 103 | .append(", username=").append(username).append(", password=") 104 | .append("****").append(", clientId=").append(clientId) 105 | .append(", clientSecret=").append(clientSecret) 106 | .append(", refreshToken=").append(refreshToken) 107 | .append(", accessToken=").append(accessToken).append("]"); 108 | return builder.toString(); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/storage/IStorageService.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.storage; 2 | 3 | import org.dmfs.oauth2.client.OAuth2AccessToken; 4 | 5 | import javafx.beans.property.BooleanProperty; 6 | import javafx.beans.property.StringProperty; 7 | import me.bayang.reader.rssmodels.UserInformation; 8 | import me.bayang.reader.share.wallabag.WallabagCredentials; 9 | import me.bayang.reader.utils.Theme; 10 | 11 | public interface IStorageService { 12 | 13 | void saveToken(OAuth2AccessToken token) throws Exception; 14 | 15 | void savePocketToken(String token); 16 | 17 | String loadPocketToken(); 18 | 19 | WallabagCredentials loadWallabagCredentials(); 20 | 21 | void saveWallabagCredentials(WallabagCredentials wallabagCredentials); 22 | 23 | void saveWallabagRefreshToken(String token); 24 | 25 | void setPocketUser(String user); 26 | 27 | StringProperty pocketUserProperty(); 28 | 29 | OAuth2AccessToken loadToken(); 30 | 31 | void saveUser(UserInformation user); 32 | 33 | String loadUser(); 34 | 35 | boolean hasToken(); 36 | 37 | boolean hasUser(); 38 | 39 | boolean prefersGridLayout(); 40 | 41 | BooleanProperty pocketEnabledProperty(); 42 | 43 | BooleanProperty wallabagEnabledProperty(); 44 | 45 | BooleanProperty prefersGridLayoutProperty(); 46 | 47 | Theme getAppTheme(); 48 | 49 | void setAppTheme(Theme appTheme); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/storage/PropertiesStorageServiceImpl.java: -------------------------------------------------------------------------------- 1 | //package me.bayang.reader.storage; 2 | // 3 | //import java.io.File; 4 | //import java.io.FileInputStream; 5 | //import java.io.FileNotFoundException; 6 | //import java.io.FileOutputStream; 7 | //import java.io.IOException; 8 | //import java.util.Properties; 9 | // 10 | //import org.dmfs.httpessentials.exceptions.ProtocolException; 11 | //import org.dmfs.oauth2.client.OAuth2AccessToken; 12 | //import org.dmfs.oauth2.client.OAuth2Scope; 13 | //import org.dmfs.oauth2.client.scope.StringScope; 14 | //import org.dmfs.rfc5545.DateTime; 15 | //import org.slf4j.Logger; 16 | //import org.slf4j.LoggerFactory; 17 | //import org.springframework.stereotype.Service; 18 | // 19 | //import me.bayang.reader.rssmodels.UserInformation; 20 | // 21 | //@Service 22 | //public class PropertiesStorageServiceImpl implements IStorageService { 23 | // 24 | // private static final String userPropertiesFilename = "UserInfo.dat"; 25 | // private static final String tokenPropertiesFilename = "TokenInfo.dat"; 26 | // 27 | // private static Logger LOGGER = LoggerFactory.getLogger(PropertiesStorageServiceImpl.class); 28 | // 29 | // @Override 30 | // public void saveToken(OAuth2AccessToken token) throws FileNotFoundException, IOException, ProtocolException { 31 | // Properties properties = new Properties(); 32 | // String accessToken = String.valueOf(token.accessToken()); 33 | // String refreshToken = String.valueOf(token.refreshToken()); 34 | // String scope = token.scope().toString(); 35 | // LOGGER.debug("access {}, refresh {}, scope {}", accessToken, refreshToken, scope); 36 | // try { 37 | // String accessTokenExpiration = token.expirationDate().toString(); 38 | // properties.setProperty("expiration", accessTokenExpiration); 39 | // LOGGER.debug("expiration {}", accessTokenExpiration); 40 | // } catch (ProtocolException e) { 41 | // LOGGER.error("error reading token", e); 42 | // } 43 | // properties.setProperty("refresh", refreshToken); 44 | // properties.setProperty("access", accessToken); 45 | // properties.setProperty("scope", scope); 46 | // properties.store(new FileOutputStream(new File(tokenPropertiesFilename)), null); 47 | // } 48 | // 49 | // @Override 50 | // public OAuth2AccessToken loadToken() { 51 | // File file = new File(tokenPropertiesFilename); 52 | // Properties properties = new Properties(); 53 | // try { 54 | // properties.load(new FileInputStream(file)); 55 | // String refreshToken = properties.getProperty("refresh"); 56 | // String accessToken = properties.getProperty("access"); 57 | // final String scope = properties.getProperty("scope"); 58 | // OAuth2AccessToken token = new OAuth2AccessToken() { 59 | // public CharSequence accessToken() throws ProtocolException { 60 | // throw new UnsupportedOperationException("accessToken not present"); 61 | // } 62 | // 63 | // public CharSequence tokenType() throws ProtocolException { 64 | // throw new UnsupportedOperationException("tokenType not present"); 65 | // } 66 | // 67 | // public boolean hasRefreshToken() { 68 | // return true; 69 | // } 70 | // 71 | // public CharSequence refreshToken() throws ProtocolException { 72 | // return refreshToken; 73 | // } 74 | // 75 | // public DateTime expirationDate() throws ProtocolException { 76 | // throw new UnsupportedOperationException("expirationDate not present"); 77 | // } 78 | // 79 | // public OAuth2Scope scope() throws ProtocolException { 80 | // return new StringScope(scope); 81 | // } 82 | // }; 83 | // return token; 84 | // } catch (IOException ioe) { 85 | // LOGGER.error("error loading token", ioe); 86 | // return null; 87 | // } 88 | // } 89 | // 90 | // @Override 91 | // public String loadUser() { 92 | // File file = new File(userPropertiesFilename); 93 | // if (!file.exists()) { 94 | // return null; 95 | // } 96 | // else { 97 | // Properties properties = new Properties(); 98 | // try { 99 | // properties.load(new FileInputStream(file)); 100 | // String userId = properties.getProperty("userId"); 101 | // return userId; 102 | // } catch (IOException ioe) { 103 | // LOGGER.error("error loading user", ioe); 104 | // } 105 | // } 106 | // return null; 107 | // } 108 | // 109 | // @Override 110 | // public void saveUser(UserInformation user) { 111 | // Properties properties = new Properties(); 112 | // properties.setProperty("userId", user.getUserId()); 113 | // try { 114 | // properties.store(new FileOutputStream(userPropertiesFilename), null); 115 | // } catch (IOException e) { 116 | // LOGGER.error("error saving user", e); 117 | // } 118 | // } 119 | // 120 | // @Override 121 | // public void savePocketToken(String token) { 122 | // } 123 | // 124 | // @Override 125 | // public void savePocketUser(String user) { 126 | // } 127 | // 128 | // @Override 129 | // public boolean hasToken() { 130 | // File f = new File(tokenPropertiesFilename); 131 | // return f.exists(); 132 | // } 133 | // 134 | // @Override 135 | // public boolean hasUser() { 136 | // File f = new File(userPropertiesFilename); 137 | // return f.exists(); 138 | // } 139 | // 140 | // @Override 141 | // public boolean prefersGridLayout() { 142 | // return false; 143 | // } 144 | // 145 | //} 146 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/utils/Filters.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.utils; 2 | 3 | import java.util.function.Predicate; 4 | 5 | import javafx.beans.Observable; 6 | import javafx.util.Callback; 7 | import me.bayang.reader.rssmodels.Item; 8 | 9 | public class Filters { 10 | 11 | public static final Predicate NO_FILTER_PREDICATE = item -> { 12 | // display all 13 | return true; 14 | }; 15 | 16 | public static final Predicate FILTER_READ_PREDICATE = item -> { 17 | if (item == null) { 18 | return false; 19 | } 20 | if (! item.isRead()) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | 26 | public static final Predicate FILTER_UNREAD_PREDICATE = item -> { 27 | if (item == null) { 28 | return false; 29 | } 30 | if (item.isRead()) { 31 | return true; 32 | } 33 | return false; 34 | }; 35 | 36 | public static Callback itemExtractor() { 37 | Callback itemExtractor = (Item i) -> { 38 | return new Observable[]{i.readProperty()}; 39 | }; 40 | return itemExtractor; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.utils; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | import org.apache.commons.text.StringEscapeUtils; 7 | 8 | import me.bayang.reader.FXMain; 9 | import net.htmlparser.jericho.Renderer; 10 | import net.htmlparser.jericho.Source; 11 | 12 | public class StringUtils { 13 | 14 | public static final Pattern PATTERN = Pattern.compile("
[.*<>\\/\\a-zA-Z0-9]*Ads from Inoreader[.*<>\\/\\a-zA-Z0-9]*<\\/center>"); 15 | public static final Pattern IMG_PATTERN = Pattern.compile("^\\s*(\\[[%-_a-zA-Z0-9].*(\\.png|\\.jp|\\.pn|\\.jpg|\\.jpeg|\\.JPEG|\\.JPG|\\.PNG|\\.bmp)\\])\\s*.+"); 16 | 17 | public static String processContent(String content) { 18 | String s = stripHeadImages(StringEscapeUtils.unescapeHtml4(stripAds(content))); 19 | if (s.length() > 160) { 20 | return s.substring(0, 160); 21 | } 22 | return s; 23 | } 24 | 25 | public static String stripAds(String text) { 26 | Matcher m = PATTERN.matcher(text); 27 | if (m.find()) { 28 | String stripped = text.replaceAll(PATTERN.toString(), ""); 29 | Source source = new Source(stripped); 30 | source.setLogger(null); 31 | Renderer renderer = source.getRenderer(); 32 | renderer.setDecorateFontStyles(true); 33 | return renderer.toString(); 34 | } 35 | Source source = new Source(text); 36 | source.setLogger(null); 37 | Renderer renderer = source.getRenderer(); 38 | renderer.setDecorateFontStyles(true); 39 | return renderer.toString(); 40 | } 41 | 42 | public static String stripHeadImages(String text) { 43 | Matcher m = IMG_PATTERN.matcher(text); 44 | if (m.find()) { 45 | return (text.substring(m.end(1)).trim()); 46 | } 47 | return text; 48 | } 49 | 50 | public static void openHyperlink(String url) { 51 | FXMain.getAppHostServices().showDocument(url); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/utils/Theme.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.utils; 2 | 3 | public enum Theme { 4 | 5 | LIGHT("/css/application.css", "Light"), 6 | DARK_ORANGE("/css/application-dark-orange.css", "Dark orange"); 7 | 8 | private String path; 9 | 10 | private String displayName; 11 | 12 | private Theme(final String path, final String displayName) { 13 | this.path = path; 14 | this.displayName = displayName; 15 | } 16 | 17 | public String getPath() { 18 | return this.path; 19 | } 20 | 21 | public String getDisplayName() { 22 | return this.displayName; 23 | } 24 | 25 | public static Theme forDisplayName(String value) { 26 | for (Theme t : values()) { 27 | if (t.getDisplayName().equals(value)) { 28 | return t; 29 | } 30 | } 31 | return null; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/AboutPopupView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import de.felixroske.jfxsupport.AbstractFxmlView; 6 | import de.felixroske.jfxsupport.FXMLView; 7 | import javafx.scene.Parent; 8 | import me.bayang.reader.storage.IStorageService; 9 | 10 | @FXMLView(bundle="i18n.translations",value="/fxml/AboutPopup.fxml", encoding = "UTF-8") 11 | public class AboutPopupView extends AbstractFxmlView { 12 | 13 | 14 | @Autowired 15 | IStorageService config; 16 | 17 | @Override 18 | public Parent getView() { 19 | Parent p = super.getView(); 20 | p.getStylesheets().add(getClass().getResource(config.getAppTheme().getPath()).toExternalForm()); 21 | 22 | return p; 23 | 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/AddSubscriptionView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import de.felixroske.jfxsupport.AbstractFxmlView; 6 | import de.felixroske.jfxsupport.FXMLView; 7 | import javafx.scene.Parent; 8 | import me.bayang.reader.storage.IStorageService; 9 | 10 | @FXMLView(bundle="i18n.translations",value="/AddSubscriptionPanel.fxml", encoding = "UTF-8") 11 | public class AddSubscriptionView extends AbstractFxmlView { 12 | 13 | @Autowired 14 | IStorageService config; 15 | 16 | @Override 17 | public Parent getView() { 18 | Parent p = super.getView(); 19 | p.getStylesheets().add(getClass().getResource(config.getAppTheme().getPath()).toExternalForm()); 20 | 21 | return p; 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/EditSubscriptionView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import de.felixroske.jfxsupport.AbstractFxmlView; 6 | import de.felixroske.jfxsupport.FXMLView; 7 | import javafx.scene.Parent; 8 | import me.bayang.reader.storage.IStorageService; 9 | 10 | @FXMLView(bundle="i18n.translations",value="/fxml/EditSubscriptionDialog.fxml", encoding = "UTF-8") 11 | public class EditSubscriptionView extends AbstractFxmlView { 12 | 13 | @Autowired 14 | IStorageService config; 15 | 16 | @Override 17 | public Parent getView() { 18 | Parent p = super.getView(); 19 | p.getStylesheets().add(getClass().getResource(config.getAppTheme().getPath()).toExternalForm()); 20 | 21 | return p; 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/NoOpSplashScreen.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import de.felixroske.jfxsupport.SplashScreen; 4 | import javafx.scene.Parent; 5 | 6 | public class NoOpSplashScreen extends SplashScreen { 7 | 8 | @Override 9 | public Parent getParent() { 10 | return super.getParent(); 11 | } 12 | 13 | @Override 14 | public boolean visible() { 15 | return false; 16 | } 17 | 18 | @Override 19 | public String getImagePath() { 20 | return super.getImagePath(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/OauthView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import de.felixroske.jfxsupport.AbstractFxmlView; 6 | import de.felixroske.jfxsupport.FXMLView; 7 | import javafx.scene.Parent; 8 | import me.bayang.reader.storage.IStorageService; 9 | 10 | @FXMLView(bundle="i18n.translations",value="/fxml/OauthPopUp.fxml", encoding = "UTF-8") 11 | public class OauthView extends AbstractFxmlView { 12 | 13 | @Autowired 14 | IStorageService config; 15 | 16 | @Override 17 | public Parent getView() { 18 | Parent p = super.getView(); 19 | p.getStylesheets().add(getClass().getResource(config.getAppTheme().getPath()).toExternalForm()); 20 | 21 | return p; 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/PocketOauthView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import de.felixroske.jfxsupport.AbstractFxmlView; 6 | import de.felixroske.jfxsupport.FXMLView; 7 | import javafx.scene.Parent; 8 | import me.bayang.reader.storage.IStorageService; 9 | 10 | @FXMLView(bundle="i18n.translations",value="/fxml/PocketOauthPopUp.fxml", encoding = "UTF-8") 11 | public class PocketOauthView extends AbstractFxmlView { 12 | 13 | @Autowired 14 | IStorageService config; 15 | 16 | @Override 17 | public Parent getView() { 18 | Parent p = super.getView(); 19 | p.getStylesheets().add(getClass().getResource(config.getAppTheme().getPath()).toExternalForm()); 20 | 21 | return p; 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/PopupWebView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import de.felixroske.jfxsupport.AbstractFxmlView; 6 | import de.felixroske.jfxsupport.FXMLView; 7 | import javafx.scene.Parent; 8 | import me.bayang.reader.storage.IStorageService; 9 | 10 | @FXMLView(bundle="i18n.translations",value="/fxml/PopupWebView.fxml", encoding = "UTF-8") 11 | public class PopupWebView extends AbstractFxmlView { 12 | 13 | @Autowired 14 | IStorageService config; 15 | 16 | @Override 17 | public Parent getView() { 18 | Parent p = super.getView(); 19 | p.getStylesheets().add(getClass().getResource(config.getAppTheme().getPath()).toExternalForm()); 20 | 21 | return p; 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/RssView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import de.felixroske.jfxsupport.AbstractFxmlView; 4 | import de.felixroske.jfxsupport.FXMLView; 5 | 6 | @FXMLView(bundle="i18n.translations",value="/UI.fxml", encoding = "UTF-8") 7 | public class RssView extends AbstractFxmlView { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/SettingsView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import de.felixroske.jfxsupport.AbstractFxmlView; 4 | import de.felixroske.jfxsupport.FXMLView; 5 | 6 | @FXMLView(bundle="i18n.translations",value="/fxml/SettingsView.fxml", encoding = "UTF-8") 7 | public class SettingsView extends AbstractFxmlView { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/ShareLinkView.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import de.felixroske.jfxsupport.AbstractFxmlView; 6 | import de.felixroske.jfxsupport.FXMLView; 7 | import javafx.scene.Parent; 8 | import me.bayang.reader.storage.IStorageService; 9 | 10 | @FXMLView(bundle="i18n.translations",value="/fxml/ShareLinkPopup.fxml", encoding = "UTF-8") 11 | public class ShareLinkView extends AbstractFxmlView { 12 | 13 | @Autowired 14 | private IStorageService config; 15 | 16 | @Override 17 | public Parent getView() { 18 | Parent p = super.getView(); 19 | p.getStylesheets().add(getClass().getResource(config.getAppTheme().getPath()).toExternalForm()); 20 | 21 | return p; 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/bayang/reader/view/SpinnerSplashScreen.java: -------------------------------------------------------------------------------- 1 | package me.bayang.reader.view; 2 | 3 | import com.jfoenix.controls.JFXSpinner; 4 | 5 | import de.felixroske.jfxsupport.SplashScreen; 6 | import javafx.geometry.Insets; 7 | import javafx.geometry.Pos; 8 | import javafx.scene.Parent; 9 | import javafx.scene.control.Label; 10 | import javafx.scene.layout.VBox; 11 | import javafx.scene.text.Font; 12 | 13 | public class SpinnerSplashScreen extends SplashScreen { 14 | 15 | @Override 16 | public Parent getParent() { 17 | JFXSpinner spinner = new JFXSpinner(); 18 | Label appTitle = new Label("lecter"); 19 | appTitle.setFont(Font.font(30)); 20 | appTitle.setStyle("-fx-text-fill : #90a4ae;"); 21 | appTitle.setPadding(new Insets(0, 0, 10, 0)); 22 | Label label = new Label("Please wait..."); 23 | label.setStyle("-fx-text-fill : white;"); 24 | label.setFont(Font.font(20)); 25 | final VBox vbox = new VBox(); 26 | vbox.setPrefSize(400, 300); 27 | spinner.setPrefWidth(200); 28 | spinner.setPrefHeight(200); 29 | spinner.setMinWidth(200); 30 | spinner.setMinHeight(200); 31 | spinner.setRadius(200); 32 | spinner.setPadding(new Insets(0, 0, 10, 0)); 33 | vbox.getChildren().addAll(appTitle, spinner, label); 34 | vbox.setStyle("-fx-background-color: #263238 ;"); 35 | vbox.setAlignment(Pos.CENTER); 36 | 37 | return vbox; 38 | } 39 | 40 | @Override 41 | public boolean visible() { 42 | return super.visible(); 43 | } 44 | 45 | @Override 46 | public String getImagePath() { 47 | return ""; 48 | } 49 | 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/AddSubscriptionPanel.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |