diff --git a/.gitignore b/.gitignore index 89443587..252008e0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,7 @@ captures/ .externalNativeBuild ### Android Patch ### -gen-external-apklibs \ No newline at end of file +gen-external-apklibs + +# Google Services (release build) +app/src/release/google-services.json \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..9a7ce1df --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,31 @@ +image: openjdk:8-jdk + +variables: + ANDROID_TARGET_SDK: "25" + ANDROID_BUILD_TOOLS: "25.0.1" + ANDROID_SDK_TOOLS: "25.2.4" + +before_script: + - apt-get --quiet update --yes + - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 + - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/tools_r${ANDROID_SDK_TOOLS}-linux.zip + - unzip android-sdk.zip -d android-sdk-linux + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter android-${ANDROID_TARGET_SDK} + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter platform-tools + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter build-tools-${ANDROID_BUILD_TOOLS} + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-android-m2repository + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-google_play_services + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-m2repository + - export ANDROID_HOME=$PWD/android-sdk-linux + - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ + - chmod +x ./gradlew + +build_develop: + script: + - ./gradlew assembleDebug + except: + - master + artifacts: + name: "${CI_BUILD_NAME}" + paths: + - app/build/outputs/apk diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md new file mode 100644 index 00000000..d13fb1d2 --- /dev/null +++ b/.gitlab/issue_templates/Bug.md @@ -0,0 +1,24 @@ +### Summary + +(Summarize the bug encountered concisely) + +### Steps to reproduce + +(How one can reproduce the issue) + +### Expected behavior + +(What should normally happen) + +### Actual behavior + +(What actually happens instead) + +### Relevant logs and/or screenshots + +(Paste any relevant logs using code blocks (```) to format console output, +logs, and code) + +### Possible fixes + +(If you can, link to the line of code that might be responsible for the problem) diff --git a/.gitlab/issue_templates/Improvement.md b/.gitlab/issue_templates/Improvement.md new file mode 100644 index 00000000..bce0589e --- /dev/null +++ b/.gitlab/issue_templates/Improvement.md @@ -0,0 +1,5 @@ +### Description + +### Proposal + +### Links / references diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f1102015 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contribute to mTHMMY + +Thank you for your interest in contributing to mTHMMY! This guide details how +to contribute to mTHMMY in a way that is efficient for everyone. + +## Security vulnerability disclosure + +**Important!** Instead of creating publicly viewable issues for suspected security +vulnerabilities, please report them in private to +`thmmynolife@gmail.com`. + +## I want to contribute! + +There are many ways of contributing to mTHMMY: + +- Simply using the latest release version +- Joining our [Discord server][discord-server] +- Creating issues & joining discussions on our [Issue Tracker][issue-tracker] +- Getting code access to fork mTHMMY and submit [merge requests](#merge-requests) +- Joining our core team +- Contacting us by email at `thmmynolife@gmail.com` + +## Issue tracker + +The [mTHMMY issue tracker on GitLab.com][issue-tracker] is for [bugs](#bugs) concerning the latest mTHMMY release and [improvements](#improvements). + +Before submitting an issue please **[search the issue tracker][issue-tracker]** for similar entries and if you don't find any create your own conforming to the simple issue submission guidelines listed below. + +### Bugs + +Please submit bugs using the ['Bug' issue template](.gitlab/issue_templates/Bug.md) provided on the issue tracker and the [`Bug`](https://gitlab.com/thmmy.gr/mTHMMY/issues?label_name=Bug) label. + +### Improvements + +Please submit improvements/ feature proposals using the ['Improvement' issue template](.gitlab/issue_templates/Improvement.md) provided on the issue tracker and the [`Improvement`](https://gitlab.com/thmmy.gr/mTHMMY/issues?label_name=Improvement) label. + +### Issue weight + +Issue weight represents the amount of work required for an issue, as an abstract measurement of its complexity. You are encouraged to discuss and set the weight of any issue to what makes most sense. +For example, something that has a weight of 1 (or no weight) is really small and simple. +Something that is 9 is a very complex issue requiring to (re)write big parts of code. + +## Merge requests + +Merge requests with fixes and improvements to mTHMMY are most welcome. The simple workflow to make a merge request is as follows: + +1. Fork the project into your personal space on GitLab.com +1. Create a feature branch, branch away from `master` +1. Push the commit(s) to your fork +1. Create a merge request (MR) targeting `master` [at mTHMMY](https://gitlab.com/thmmy.gr/mTHMMY/tree/master) +1. Fill the MR title describing the change you want to make +1. Fill the MR description with a brief motive for your change and the method you used to achieve it. +1. Submit the MR. + + + +*This guide was inspired by [Gitlab CE's Contributing Guide][gitlab-contributing-guide].* + +[issue-tracker]: https://gitlab.com/thmmy.gr/mTHMMY/issues +[discord-server]: https://discord.gg/CVt3yrn +[gitlab-contributing-guide]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..69c60e28 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# mTHMMY + +[![build status](https://gitlab.com/ThmmyNoLife/mTHMMY/badges/develop/build.svg)](https://gitlab.com/ThmmyNoLife/mTHMMY/commits/develop) +[![API](https://img.shields.io/badge/API-19%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=19) +[![Discord Channel](https://img.shields.io/badge/discord-public@mTHMMY-738bd7.svg?style=flat)](https://discord.gg/CVt3yrn) + +mTHMMY is a mobile app for thmmy.gr + +## Requirements + +mTHMMY can be installed on any smartphone with Android 4.4 KitKat or newer. + +## Contributing + +Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details. + +## Contact + +Do not hesitate to contact us for any matter at `thmmynolife@gmail.com`. diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..3eefcb9d --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/app/build.gradle b/app/build.gradle index 764cd232..ac5a28c4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,28 +2,51 @@ apply plugin: 'com.android.application' android { compileSdkVersion 25 - buildToolsVersion "25.0.0" + buildToolsVersion "25.0.1" + defaultConfig { + vectorDrawables.useSupportLibrary = true applicationId "gr.thmmy.mthmmy" - minSdkVersion 15 + minSdkVersion 19 targetSdkVersion 25 versionCode 1 - versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + versionName "1.0.0" + archivesBaseName = "mTHMMY-v$versionName" } + buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + debug { + def date = new Date().format('ddMMyy_HHmm'); + archivesBaseName = archivesBaseName + "-$date" + } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - compile 'com.android.support:appcompat-v7:25.0.0' - testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support:design:25.1.0' + compile 'com.android.support:support-v4:25.1.0' + compile 'com.android.support:cardview-v7:25.1.0' + compile 'com.android.support:recyclerview-v7:25.1.0' + compile 'com.google.firebase:firebase-crash:10.0.1' + compile 'com.squareup.okhttp3:okhttp:3.5.0' + compile 'com.squareup.picasso:picasso:2.5.2' + compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0' + compile 'org.jsoup:jsoup:1.10.1' + compile 'com.github.franmontiel:PersistentCookieJar:v1.0.0' + compile 'com.github.PhilJay:MPAndroidChart:v3.0.1' + compile('com.mikepenz:materialdrawer:5.8.1@aar') { + transitive = true + } + compile 'com.mikepenz:fontawesome-typeface:4.7.0.0@aar' + compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.3' + compile 'com.bignerdranch.android:expandablerecyclerview:3.0.0-RC1' + compile 'me.zhanghai.android.materialprogressbar:library:1.3.0' } + +apply plugin: 'com.google.gms.google-services' diff --git a/app/src/androidTest/java/gr/thmmy/mthmmy/ExampleInstrumentedTest.java b/app/src/androidTest/java/gr/thmmy/mthmmy/ExampleInstrumentedTest.java deleted file mode 100644 index 7c6fd486..00000000 --- a/app/src/androidTest/java/gr/thmmy/mthmmy/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package gr.thmmy.mthmmy; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumentation test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("gr.thmmy.mthmmy", appContext.getPackageName()); - } -} diff --git a/app/src/debug/google-services.json b/app/src/debug/google-services.json new file mode 100644 index 00000000..c6718eaa --- /dev/null +++ b/app/src/debug/google-services.json @@ -0,0 +1,42 @@ +{ + "project_info": { + "project_number": "934432863001", + "firebase_url": "https://mthmmy-debug.firebaseio.com", + "project_id": "mthmmy-debug", + "storage_bucket": "mthmmy-debug.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:934432863001:android:088be0537ff6b292", + "android_client_info": { + "package_name": "gr.thmmy.mthmmy" + } + }, + "oauth_client": [ + { + "client_id": "934432863001-d5oocs1vdi0pcepesi55a41p7enphfcv.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyD4-gwVcb2Rc8zeT8l1v2Lg1DU0QgfGtk8" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 2 + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/debug/java/mthmmy/utils/Report.java b/app/src/debug/java/mthmmy/utils/Report.java new file mode 100644 index 00000000..68ab3c3b --- /dev/null +++ b/app/src/debug/java/mthmmy/utils/Report.java @@ -0,0 +1,67 @@ +package mthmmy.utils; + +import android.util.Log; + +public class Report +{ + + public static void v (String TAG, String message) + { + Log.v(TAG,message); + } + + public static void v (String TAG, String message, Throwable tr) + { + Log.v(TAG,message + ": " + tr.getMessage(),tr); + } + + public static void d (String TAG, String message) + { + Log.d(TAG,message); + } + + public static void d (String TAG, String message, Throwable tr) + { + Log.d(TAG,message + ": " + tr.getMessage(),tr); + } + + public static void i (String TAG, String message) + { + Log.i(TAG,message); + } + + public static void i (String TAG, String message, Throwable tr) + { + Log.i(TAG,message + ": " + tr.getMessage(),tr); + } + + public static void w (String TAG, String message) + { + Log.w(TAG,message); + } + + public static void w (String TAG, String message, Throwable tr) + { + Log.w(TAG,message + ": " + tr.getMessage(),tr); + } + + public static void e (String TAG, String message) + { + Log.e(TAG,message); + } + + public static void e (String TAG, String message, Throwable tr) + { + Log.e(TAG,message + ": " + tr.getMessage(),tr); + } + + public static void wtf (String TAG, String message) + { + Log.wtf(TAG,message); + } + + public static void wtf (String TAG, String message, Throwable tr) + { + Log.wtf(TAG,message + ": " + tr.getMessage(),tr); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7517b415..f8bed348 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,13 +1,68 @@ + + package="gr.thmmy.mthmmy"> + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/app/src/main/assets/apache_libraries.html b/app/src/main/assets/apache_libraries.html new file mode 100644 index 00000000..10867db3 --- /dev/null +++ b/app/src/main/assets/apache_libraries.html @@ -0,0 +1,139 @@ + + + + + + + + + + + +
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+

Apache License v2.0

+
+Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+    You must give any other recipients of the Work or Derivative Works a copy of this License; and
+    You must cause any modified files to carry prominent notices stating that You changed the files; and
+    You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+    If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+    You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+ + + diff --git a/app/src/main/assets/fonts/fontawesome-webfont.ttf b/app/src/main/assets/fonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..35acda2f Binary files /dev/null and b/app/src/main/assets/fonts/fontawesome-webfont.ttf differ diff --git a/app/src/main/assets/mit_libraries.html b/app/src/main/assets/mit_libraries.html new file mode 100644 index 00000000..f51bcca0 --- /dev/null +++ b/app/src/main/assets/mit_libraries.html @@ -0,0 +1,78 @@ + + + + + + + + + + +
+

The MIT License

+
+  Copyright <YEAR> <COPYRIGHT HOLDER>
+
+  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:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  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.
+
+ + + diff --git a/app/src/main/assets/style.css b/app/src/main/assets/style.css new file mode 100644 index 00000000..d870a911 --- /dev/null +++ b/app/src/main/assets/style.css @@ -0,0 +1,523 @@ +/* TP specific classes */ +.sitemap{ + margin: 0; + padding: 0; + list-style: none; +} +.sitemap_topheader{ + background: #ECEDF3; + border-bottom: solid 1px #ffffff; + padding: 4px; +} + +.sitemap_header{ + background: #ECEDF3; + border-bottom: solid 1px #ffffff; + padding: 4px; + display: block; + font-weight: bold; + } + +.sitemap_header_active{ + background: #C8D6E1; + border-bottom: solid 1px #ffffff; + padding: 4px; + display: block; + font-weight: bold; +} + +.sitemap_header:hover , .sitemap_header_active:hover{ + background: #DBE4ED; + border-bottom: solid 1px #ffffff; + padding: 4px; + display: block; + text-decoration: none; +} + +/* TP other styles */ +ul#articlelist +{ + margin: 0; + padding: 0.5ex 0; + list-style: none; +} +ul#catlist +{ + margin: 0; + padding: 0; + list-style: none; + border-top: solid 1px #d0d0d0; +} + +ul#articlelist li +{ + margin: 0; + display: block; + padding: 0 0 0 3ex; + background: url(images/divider.gif) no-repeat 5px 3px; +} +ul#catlist li +{ + display: block; + padding: 0 0 0 3ex; + margin: 0; +} + +/* Normal, standard links. */ +a:link, a:visited +{ + color: #26A69A; + text-decoration: none; +} +a:hover +{ + text-decoration: underline; +} + +/* Navigation links - for the link tree. */ +.nav, .nav:link, .nav:visited +{ + color: #000000; + text-decoration: none; +} +a.nav:hover +{ + color: #cc3333; +} + +/* Tables should show empty cells. */ +table +{ + empty-cells: show; +} +/* The main body of the entire forum. */ +body +{ + background: #3C3F41; + margin: 0; + padding: 0; +} +/* By default (td, body..) use verdana in black. */ +body, td, th , tr +{ + color: #FFFFFF; + font-size: small; + font-family: Trebuchet, sans-serif; +} + + + +/* Input boxes - just a bit smaller than normal so they align well. */ +input, textarea, button +{ + color: #FFFFFF; + font-family: Trebuchet, sans-serif; + border: 1px solid #aaa; +} +input, button +{ + font-size: 90%; +} + +textarea +{ + font-size: 100%; + color: #FFFFFF; + font-family: Trebuchet, sans-serif; +} + +/* All input elements that are checkboxes or radio buttons. */ +input.check +{ +} + +/* Selects are a bit smaller, because it makes them look even better 8). */ +select +{ + font-size: 90%; + font-weight: normal; + color: #FFFFFF; + font-family: Trebuchet, sans-serif; +} + +/* Standard horizontal rule.. ([hr], etc.) */ +hr, .hrcolor +{ + height: 1px; + border: 0; + color: #666666; + background-color: #666666; +} + +/* No image should have a border when linked */ +a img +{ + border: 0; +} +/* A quote, perhaps from another post. */ +.quote +{ + font-family: tahoma, sans-serif; + color: #FFFFFF; + background-color: #404D50; + border: 1px solid #E7E7E7; + margin: 1px; + padding: 1px; + font-size: x-small; + line-height: 1.4em; +} + +/* A code block - maybe even PHP ;). */ +.code +{ + color: #FFFFFF; + background-color: #626566; + font-family: "Comic Sans MS", "times new roman", monospace; + font-size: x-small; + line-height: 1.3em; + /* Put a nice border around it. */ + border: 1px solid #FFFFFF; + margin: 1px auto 1px auto; + padding: 1px; + width: 99%; + /* Don't wrap its contents, and show scrollbars. */ + white-space: nowrap; + overflow: auto; + /* Stop after about 24 lines, and just show a scrollbar. */ + max-height: 24em; +} + +/* The "Quote:" and "Code:" header parts... */ +.quoteheader, .codeheader +{ + font-family: tahoma, sans-serif; + color: #26A69A; + text-decoration: none; + font-style: normal; + font-weight: bold; + font-size: x-small; + line-height: 1.2em; +} + +/* Generally, those [?] icons. This makes your cursor a help icon. */ +.help +{ + cursor: help; +} + +/* /me uses this a lot. (emote, try typing /me in a post.) */ +.meaction +{ + color: red; +} + +/* The main post box - this makes it as wide as possible. */ +.editor +{ + width: 96%; +} + +/* Highlighted text - such as search results. */ +.highlight +{ + background-color: yellow; + font-weight: bold; + color: black; +} + +/* Alternating backgrounds for posts, and several other sections of the forum. */ +.windowbg +{ + color: #FFFFFF; + background-color: #E3E6E1; +} +.windowbg2 +{ + color: #FFFFFF; + background-color: #F2F5F0; +} +.windowbg3 +{ + color: #FFFFFF; + background-color: #E1E8E0; +} +/* the today container in calendar */ +.calendar_today +{ + background-color: #FFFFFF; +} + +/* These are used primarily for titles, but also for headers (the row that says what everything in the table is.) */ +.titlebg, tr.titlebg th, tr.titlebg td, .titlebg2, tr.titlebg2 th, tr.titlebg2 td +{ + background-color: #A3A392; + padding-top: 10px; +} +.titlebg, tr.titlebg th, tr.titlebg td, .titlebg a:link, .titlebg a:visited, .titlebg2, tr.titlebg2 th, tr.titlebg2 td, .titlebg2 a:link, .titlebg2 a:visited +{ + color: white; + font-style: normal; +} +.titlebg a:hover +{ + color: #dfdfdf; +} + +.catbg, .catbg2, .catbg3 +{ + font-weight: bold; + background-color: #e4e2e0; + color: #FFFFFF; +} +/* This is used for tables that have a grid/border background color (such as the topic listing.) */ +.bordercolor +{ + background-color: white; +} + +/* This is used on tables that should just have a border around them. */ +.tborder +{ + background-color: #FFFFFF; +} + +/* Default font sizes: small (8pt), normal (10pt), and large (14pt). */ +.smalltext +{ + font-size: x-small; + font-family: tahoma, sans-serif; +} +.middletext +{ + font-size: 90%; +} +.normaltext +{ + font-size: small; +} +.largetext +{ + font-size: large; +} + + +/* Posts and personal messages displayed throughout the forum. */ +.post, .personalmessage +{ + width: 100%; + overflow: auto; + line-height: 1.3em; + color: white; + background: #3C3F41 !important; +} + +/* All the signatures used in the forum. If your forum users use Mozilla, Opera, or Safari, you might add max-height here ;). */ +.signature +{ + width: 100%; + overflow: auto; + padding-bottom: 3px; + line-height: 1.3em; +} + +#left +{ + background: url(images/img2/leftbg.jpg) repeat-y white; + margin: auto; +} +#right +{ + background: url(images/img2/rightbg.gif) repeat-y top right; +} +#top +{ + background: url(images/img2/top.jpg) repeat-x; +} +#topleft +{ + background: url(images/img2/lefttop.jpg) no-repeat; +} +#topright +{ + background: url(images/img2/logo.jpg) no-repeat top right; +} +#main +{ + padding: 100px 81px 20px 81px; +} +/* #################### */ + +ul#menubox +{ + padding: 0 0 44px 0; + margin: 0; + list-style: none; + position: absolute; + left: 0; + top: 87px; + background: url(images/img2/leftbot.gif) no-repeat bottom left; +} + +ul#menubox li +{ + padding: 0 0 0 8px; + width: 65px; + height: 44px; + margin: 0; + background: url(images/img2/left.gif) repeat-y; +} +ul#menubox li a +{ + font-family: "Comic Sans MS", serif; + display: block; + color: black; + width: 45px; + height: 42px; + padding: 0 0 0 6px; +} +ul#menubox li a span +{ + display: none; +} + +ul#menubox li.m1 +{ + padding-left: 2px; +} +ul#menubox li.m2 +{ + padding-left: 6px; +} +ul#menubox li.m3 +{ + padding-left: 10px; +} +ul#menubox li.m4 +{ + padding-left: 14px; +} +ul#menubox li.m5 +{ + padding-left: 18px; +} + + +#myuser +{ + font-size: small; + padding-bottom: 1em; +} +#ava +{ + float: right; + margin-right: 10px; + text-align: right; + font-family: "Comic Sans MS", sans-serif; +} +#bodyarea +{ + border-bottom: solid 1px #ddd; + margin-bottom: 1em; + padding-bottom: 1em; +} +.clearfix:after +{ + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +.clearfix +{ + display: inline-block; +} + +/* Hides from IE-mac \*/ +* html .clearfix , * html .catbg, * html .catbg2, * html .catbg3 +{ + height: 1%; +} +/* End hide from IE-mac */ + +ul#topmenu +{ + position: absolute; + top: 45px; + margin: 0 195px 0 40px; + padding: 0 5px 4px 5px; + list-style: none; + font-weight: bold; + font-size: 11px; + border-bottom: groove 2px #EDF4ED; +} +ul#topmenu li +{ + float: left; +} +ul#topmenu li a +{ + display: block; + padding: 2px 5px 2px 5px; + border-style: solid solid; + border-width: 0px 1px; + border-color: #E3E6E1; + font-size: 11px; + color: #004080; +} +ul#topmenu li a:hover +{ + background: #E3E6E1; + text-decoration: none; + color: #E78E13; +} +#pages +{ + padding-top: 1em; +} +#uppersection +{ + padding: 1em; + background: url(images/img/upper.jpg) repeat-x; +} +.errorbar +{ + color: white; + font-size: xx-small; + text-align: center; + padding: 3px; + border-bottom: solid 1px black; +} +#errorpanel +{ + position: absolute; + top: 0; + left: 0; + z-index: 90; + width: 100%; +} + +/* Additions */ +img +{ + max-width:100% !important; + height:auto !important; +} + +.yt { + position: relative; +} + +.embedded-video-play { + position: absolute; + top: 22%; + left: 10%; + width: 20%; + opacity: 0.7; + z-index: 2; +} + +.customSignature{ + background: #323232; +} \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java new file mode 100644 index 00000000..0b33c3e9 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java @@ -0,0 +1,114 @@ +package gr.thmmy.mthmmy.activities; + +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CoordinatorLayout; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.WebView; +import android.widget.FrameLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import gr.thmmy.mthmmy.BuildConfig; +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseActivity; + +public class AboutActivity extends BaseActivity { + private static final int TIME_INTERVAL = 1000; + private static final int TIMES_TO_PRESS = 4; + private long mVersionLastPressedTime; + private int mVersionPressedCounter; + + private AppBarLayout appBar; + private CoordinatorLayout coordinatorLayout; + AlertDialog alertDialog; + private FrameLayout trollGif; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + String versionName = BuildConfig.VERSION_NAME; + + //Initialize appbar + appBar = (AppBarLayout) findViewById(R.id.appbar); + coordinatorLayout = (CoordinatorLayout) findViewById(R.id.main_content); + //Initialize toolbar + toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar.setTitle(R.string.about); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + + createDrawer(); + drawer.setSelection(ABOUT_ID); + + final ScrollView mainContent = (ScrollView) findViewById(R.id.scrollview); + trollGif = (FrameLayout) findViewById(R.id.trollPicFrame); + + TextView tv = (TextView) findViewById(R.id.version); + if (tv != null) + tv.setText(getString(R.string.version, versionName)); + + tv.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mVersionLastPressedTime + TIME_INTERVAL > System.currentTimeMillis()) { + if (mVersionPressedCounter == TIMES_TO_PRESS) { + appBar.setVisibility(View.INVISIBLE); + mainContent.setVisibility(View.INVISIBLE); + trollGif.setVisibility(View.VISIBLE); + drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + mVersionLastPressedTime = System.currentTimeMillis(); + ++mVersionPressedCounter; + } else { + mVersionLastPressedTime = System.currentTimeMillis(); + mVersionPressedCounter = 0; + } + } + }); + + } + + @Override + protected void onResume() { + drawer.setSelection(ABOUT_ID); + super.onResume(); + } + + public void displayApacheLibraries(View v) { + LayoutInflater inflater =LayoutInflater.from(this); + WebView webView = (WebView) inflater.inflate(R.layout.dialog_licenses, coordinatorLayout, false); + webView.loadUrl("file:///android_asset/apache_libraries.html"); + int width = (int)(getResources().getDisplayMetrics().widthPixels*0.95); + int height = (int)(getResources().getDisplayMetrics().heightPixels*0.95); + alertDialog = new AlertDialog.Builder(this, R.style.AppTheme_Dark_Dialog) + .setTitle(getString(R.string.apache_v2_0_libraries)) + .setView(webView) + .setPositiveButton(android.R.string.ok, null) + .show(); + alertDialog.getWindow().setLayout(width, height); + } + + public void displayMITLibraries(View v) { + LayoutInflater inflater =LayoutInflater.from(this); + WebView webView = (WebView) inflater.inflate(R.layout.dialog_licenses, coordinatorLayout, false); + webView.loadUrl("file:///android_asset/mit_libraries.html"); + int width = (int)(getResources().getDisplayMetrics().widthPixels*0.95); + int height = (int)(getResources().getDisplayMetrics().heightPixels*0.95); + alertDialog = new AlertDialog.Builder(this, R.style.AppTheme_Dark_Dialog) + .setTitle(getString(R.string.the_mit_libraries)) + .setView(webView) + .setPositiveButton(android.R.string.ok, null) + .show(); + alertDialog.getWindow().setLayout(width, height); + } + +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/LoginActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/LoginActivity.java new file mode 100644 index 00000000..2035653a --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/LoginActivity.java @@ -0,0 +1,213 @@ +package gr.thmmy.mthmmy.activities; + +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v7.widget.AppCompatButton; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.Toast; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import gr.thmmy.mthmmy.activities.main.MainActivity; +import mthmmy.utils.Report; + +import static gr.thmmy.mthmmy.session.SessionManager.CONNECTION_ERROR; +import static gr.thmmy.mthmmy.session.SessionManager.EXCEPTION; +import static gr.thmmy.mthmmy.session.SessionManager.FAILURE; +import static gr.thmmy.mthmmy.session.SessionManager.SUCCESS; +import static gr.thmmy.mthmmy.session.SessionManager.WRONG_PASSWORD; +import static gr.thmmy.mthmmy.session.SessionManager.WRONG_USER; + +public class LoginActivity extends BaseActivity { + + //-----------------------------------------CLASS VARIABLES------------------------------------------ + /* --Graphics-- */ + private AppCompatButton btnLogin; + private EditText inputUsername; + private EditText inputPassword; + private String username; + private String password; + /* --Graphics End-- */ + + //Other variables + private static final String TAG = "LoginActivity"; + + private LoginTask loginTask; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + //Variables initialization + inputUsername = (EditText) findViewById(R.id.username); + inputPassword = (EditText) findViewById(R.id.password); + btnLogin = (AppCompatButton) findViewById(R.id.btnLogin); + AppCompatButton btnGuest = (AppCompatButton) findViewById(R.id.btnContinueAsGuest); + + //Login button Click Event + btnLogin.setOnClickListener(new View.OnClickListener() { + + public void onClick(View view) { + Report.d(TAG, "Login"); + + //Get username and password strings + username = inputUsername.getText().toString().trim(); + password = inputPassword.getText().toString().trim(); + + //Check for empty data in the form + if (!validate()) { + onLoginFailed(); + return; + } + + //Login user + loginTask = new LoginTask(); + loginTask.execute(username, password); + } + }); + + //Guest Button Action + btnGuest.setOnClickListener(new View.OnClickListener() { + + public void onClick(View view) { + //Session data update + sessionManager.guestLogin(); + + //Go to main + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.push_left_in, R.anim.push_left_out); + } + }); + } + + @Override + public void onBackPressed() { + // Disable going back to the MainActivity + moveTaskToBack(true); + if (loginTask != null && loginTask.getStatus() == AsyncTask.Status.RUNNING) { + loginTask.cancel(true); + } + } + + private void onLoginFailed() { + Toast.makeText(getBaseContext(), "Login failed", Toast.LENGTH_LONG).show(); + btnLogin.setEnabled(true); + } + + private boolean validate() { + //Handle empty text fields + boolean valid = true; + + if (username.isEmpty()) { + inputUsername.setError("Enter a valid username"); + inputUsername.requestFocus(); + valid = false; + } else { + inputUsername.setError(null); + } + + if (password.isEmpty()) { + inputPassword.setError("Enter a valid password", null); + if (valid) + inputPassword.requestFocus(); + valid = false; + } else { + inputPassword.setError(null); + } + + return valid; + } + + //--------------------------------------------LOGIN------------------------------------------------- + private class LoginTask extends AsyncTask { + //Class variables + private LinearLayout spinner; + private ScrollView loginContent; + + @Override + protected Integer doInBackground(String... params) { + return sessionManager.login(params[0], params[1]); + } + + @Override + protected void onPreExecute() { //Show a progress dialog until done + btnLogin.setEnabled(false); //Login button shouldn't be pressed during this + + spinner = (LinearLayout) findViewById(R.id.login_progress_bar); + loginContent = (ScrollView) findViewById(R.id.inner_scroll_view); + + View view = getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + loginContent.setVisibility(View.INVISIBLE); + spinner.setVisibility(View.VISIBLE); + } + + + @Override + protected void onPostExecute(Integer result) { //Handle attempt result + switch (result) { + case SUCCESS: //Successful login + Toast.makeText(getApplicationContext(), + "Login successful!", Toast.LENGTH_LONG) + .show(); + //Go to main + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.push_left_in, R.anim.push_left_out); + break; + case WRONG_USER: + Toast.makeText(getApplicationContext(), + "Wrong username!", Toast.LENGTH_LONG).show(); + inputUsername.requestFocus(); + break; + case WRONG_PASSWORD: + Toast.makeText(getApplicationContext(), + "Wrong password!", Toast.LENGTH_LONG).show(); + inputPassword.requestFocus(); + break; + case FAILURE: + Toast.makeText(getApplicationContext(), + "Login failed...", Toast.LENGTH_LONG).show(); + break; + case CONNECTION_ERROR: + Toast.makeText(getApplicationContext(), + "Connection Error", Toast.LENGTH_LONG).show(); + break; + case EXCEPTION: + Toast.makeText(getApplicationContext(), + "Error", Toast.LENGTH_LONG).show(); + break; + + } + //Login failed + btnLogin.setEnabled(true); //Re-enable login button + + loginContent.setVisibility(View.VISIBLE); + spinner.setVisibility(View.INVISIBLE); + } + + @Override + protected void onCancelled() { + super.onCancelled(); + btnLogin.setEnabled(true); //Re-enable login button + loginContent.setVisibility(View.VISIBLE); + spinner.setVisibility(View.INVISIBLE); + } + + } +//---------------------------------------LOGIN ENDS------------------------------------------------- +} \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/base/BaseActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/base/BaseActivity.java new file mode 100644 index 00000000..4794cf9e --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/base/BaseActivity.java @@ -0,0 +1,373 @@ +package gr.thmmy.mthmmy.activities.base; + +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.ImageView; +import android.widget.Toast; + +import com.franmontiel.persistentcookiejar.PersistentCookieJar; +import com.franmontiel.persistentcookiejar.cache.SetCookieCache; +import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor; +import com.jakewharton.picasso.OkHttp3Downloader; +import com.mikepenz.fontawesome_typeface_library.FontAwesome; +import com.mikepenz.iconics.IconicsDrawable; +import com.mikepenz.materialdrawer.AccountHeader; +import com.mikepenz.materialdrawer.AccountHeaderBuilder; +import com.mikepenz.materialdrawer.Drawer; +import com.mikepenz.materialdrawer.DrawerBuilder; +import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; +import com.mikepenz.materialdrawer.model.ProfileDrawerItem; +import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; +import com.mikepenz.materialdrawer.model.interfaces.IProfile; +import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader; +import com.mikepenz.materialdrawer.util.DrawerImageLoader; +import com.squareup.picasso.Picasso; + +import java.util.concurrent.TimeUnit; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.AboutActivity; +import gr.thmmy.mthmmy.activities.LoginActivity; +import gr.thmmy.mthmmy.activities.main.MainActivity; +import gr.thmmy.mthmmy.activities.profile.ProfileActivity; +import gr.thmmy.mthmmy.session.SessionManager; +import okhttp3.OkHttpClient; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_URL; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_THUMBNAIL_URL; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_USERNAME; + +public abstract class BaseActivity extends AppCompatActivity +{ + // Client & Cookies + protected static OkHttpClient client; + private static final long connectTimeout = 30; //TimeUnit.SECONDS for all three + private static final long writeTimeout = 30; + private static final long readTimeout = 30; + protected static Picasso picasso; + private static PersistentCookieJar cookieJar; + private static SharedPrefsCookiePersistor sharedPrefsCookiePersistor; + + //Shared Preferences + protected static final String SHARED_PREFS_NAME = "ThmmySharedPrefs"; + protected static SharedPreferences sharedPrefs; + + //SessionManager + protected static SessionManager sessionManager; + + //Other variables + private static boolean init = false; //To initialize stuff only once per app start + + //Common UI elements + protected Toolbar toolbar; + protected Drawer drawer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (!init) { + sharedPrefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE); + sharedPrefsCookiePersistor = new SharedPrefsCookiePersistor(BaseActivity.this); + cookieJar = new PersistentCookieJar(new SetCookieCache(), sharedPrefsCookiePersistor); + client = new OkHttpClient.Builder() + .cookieJar(cookieJar) + .connectTimeout(connectTimeout, TimeUnit.SECONDS) + .writeTimeout(writeTimeout, TimeUnit.SECONDS) + .readTimeout(readTimeout, TimeUnit.SECONDS) + .build(); + sessionManager = new SessionManager(client, cookieJar, sharedPrefsCookiePersistor, sharedPrefs); + picasso = new Picasso.Builder(BaseActivity.this) + .downloader(new OkHttp3Downloader(client)) + .build(); + Picasso.setSingletonInstance(picasso); // all following Picasso (with Picasso.with(Context context) requests will use this Picasso object + //initialize and create the image loader logic TODO move this to a singleton BaseApplication obj + DrawerImageLoader.init(new AbstractDrawerImageLoader() { + @Override + public void set(ImageView imageView, Uri uri, Drawable placeholder) { + Picasso.with(imageView.getContext()).load(uri).placeholder(placeholder).into(imageView); + } + @Override + public void cancel(ImageView imageView) { + Picasso.with(imageView.getContext()).cancelRequest(imageView); + } + + @Override + public Drawable placeholder(Context ctx, String tag) { + if (DrawerImageLoader.Tags.PROFILE.name().equals(tag)) { + return new IconicsDrawable(ctx).icon(FontAwesome.Icon.faw_user) + .paddingDp(10) + .color(ContextCompat.getColor(ctx, R.color.primary_light)) + .backgroundColor(ContextCompat.getColor(ctx, R.color.primary)); + } + + return super.placeholder(ctx, tag); + } + }); + init = true; + } + } + + @Override + protected void onResume() { + super.onResume(); + updateDrawer(); + } + + @Override + protected void onPause() { + super.onPause(); + if(drawer!=null) //close drawer animation after returning to activity + drawer.closeDrawer(); + } + + + public static OkHttpClient getClient() + { + return client; + } + + public static SessionManager getSessionManager() + { + return sessionManager; + } + + //TODO: move stuff below + //------------------------------------------DRAWER STUFF---------------------------------------- + protected static final int HOME_ID=0; + protected static final int LOG_ID =1; + protected static final int ABOUT_ID=2; + + private AccountHeader accountHeader; + private ProfileDrawerItem profileDrawerItem; + private PrimaryDrawerItem homeItem, loginLogoutItem, aboutItem; + private IconicsDrawable homeIcon, homeIconSelected, loginIcon, logoutIcon, + aboutIcon, aboutIconSelected; + + /** + * Call only after initializing Toolbar + */ + protected void createDrawer() + { + final int primaryColor = ContextCompat.getColor(this, R.color.iron); + final int selectedPrimaryColor = ContextCompat.getColor(this, R.color.primary_dark); + final int selectedSecondaryColor = ContextCompat.getColor(this, R.color.accent); + + //Drawer Icons + homeIcon =new IconicsDrawable(this) + .icon(FontAwesome.Icon.faw_home) + .color(primaryColor); + + homeIconSelected =new IconicsDrawable(this) + .icon(FontAwesome.Icon.faw_home) + .color(selectedSecondaryColor); + + loginIcon =new IconicsDrawable(this) + .icon(FontAwesome.Icon.faw_sign_in) + .color(primaryColor); + + logoutIcon =new IconicsDrawable(this) + .icon(FontAwesome.Icon.faw_sign_out) + .color(primaryColor); + + aboutIcon =new IconicsDrawable(this) + .icon(FontAwesome.Icon.faw_info_circle) + .color(primaryColor); + + aboutIconSelected =new IconicsDrawable(this) + .icon(FontAwesome.Icon.faw_info_circle) + .color(selectedSecondaryColor); + + //Drawer Items + homeItem = new PrimaryDrawerItem() + .withTextColor(primaryColor) + .withSelectedColor(selectedPrimaryColor) + .withSelectedTextColor(selectedSecondaryColor) + .withIdentifier(HOME_ID) + .withName(R.string.home) + .withIcon(homeIcon) + .withSelectedIcon(homeIconSelected); + + if (!sessionManager.isLoggedIn()) //When logged out + loginLogoutItem = new PrimaryDrawerItem() + .withTextColor(primaryColor) + .withSelectedColor(selectedSecondaryColor) + .withIdentifier(LOG_ID).withName(R.string.login) + .withIcon(loginIcon) + .withSelectable(false); + else + loginLogoutItem = new PrimaryDrawerItem() + .withTextColor(primaryColor) + .withSelectedColor(selectedSecondaryColor) + .withIdentifier(LOG_ID) + .withName(R.string.logout) + .withIcon(logoutIcon) + .withSelectable(false); + aboutItem = new PrimaryDrawerItem() + .withTextColor(primaryColor) + .withSelectedColor(selectedPrimaryColor) + .withSelectedTextColor(selectedSecondaryColor) + .withIdentifier(ABOUT_ID) + .withName(R.string.about) + .withIcon(aboutIcon) + .withSelectedIcon(aboutIconSelected); + + //Profile + profileDrawerItem = new ProfileDrawerItem().withName(sessionManager.getUsername()); + + //AccountHeader + accountHeader = new AccountHeaderBuilder() + .withActivity(this) + .withCompactStyle(true) + .withSelectionListEnabledForSingleProfile(false) + .withHeaderBackground(R.color.primary) + .addProfiles(profileDrawerItem) + .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { + @Override + public boolean onProfileChanged(View view, IProfile profile, boolean currentProfile) { + if(sessionManager.isLoggedIn()) + { + Intent intent = new Intent(BaseActivity.this, ProfileActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_PROFILE_URL, "https://www.thmmy.gr/smf/index.php?action=profile"); + if(!sessionManager.hasAvatar()) + extras.putString(BUNDLE_THUMBNAIL_URL, ""); + else + extras.putString(BUNDLE_THUMBNAIL_URL, sessionManager.getAvatarLink()); + extras.putString(BUNDLE_USERNAME, sessionManager.getUsername()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return false; + } + return true; + + } + }) + .build(); + + //Drawer + drawer = new DrawerBuilder() + .withActivity(this) + .withToolbar(toolbar) + .withSliderBackgroundColor(ContextCompat.getColor(this, R.color.primary_light)) + .withAccountHeader(accountHeader) + .addDrawerItems(homeItem,loginLogoutItem,aboutItem) + .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { + @Override + public boolean onItemClick(View view, int position, IDrawerItem drawerItem) { + if(drawerItem.equals(HOME_ID)) + { + if(!(BaseActivity.this instanceof MainActivity)) + { + Intent i = new Intent(BaseActivity.this, MainActivity.class); + startActivity(i); + } + } + else if(drawerItem.equals(LOG_ID)) + { + if (!sessionManager.isLoggedIn()) //When logged out or if user is guest + { + Intent intent = new Intent(BaseActivity.this, LoginActivity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.push_right_in, R.anim.push_right_out); + } + else + new LogoutTask().execute(); + } + else if(drawerItem.equals(ABOUT_ID)) + { + if(!(BaseActivity.this instanceof AboutActivity)) + { + Intent i = new Intent(BaseActivity.this, AboutActivity.class); + startActivity(i); + } + + } + + drawer.closeDrawer(); + return true; + } + }) + .build(); + + drawer.getActionBarDrawerToggle().setDrawerIndicatorEnabled(false); + drawer.setOnDrawerNavigationListener(new Drawer.OnDrawerNavigationListener() { + @Override + public boolean onNavigationClickListener(View clickedView) { + onBackPressed(); + return true; + } + }); + } + + protected void updateDrawer() + { + if(drawer!=null) + { + if (!sessionManager.isLoggedIn()) //When logged out or if user is guest + { + loginLogoutItem.withName(R.string.login).withIcon(loginIcon); //Swap logout with login + profileDrawerItem.withName(sessionManager.getUsername()).withIcon(new IconicsDrawable(this) + .icon(FontAwesome.Icon.faw_user) + .paddingDp(10) + .color(ContextCompat.getColor(this, R.color.primary_light)) + .backgroundColor(ContextCompat.getColor(this, R.color.primary))); + } + else + { + loginLogoutItem.withName(R.string.logout).withIcon(logoutIcon); //Swap login with logout + profileDrawerItem.withName(sessionManager.getUsername()).withIcon(sessionManager.getAvatarLink()); + } + accountHeader.updateProfile(profileDrawerItem); + drawer.updateItem(loginLogoutItem); + + } + } + + + //-------------------------------------------LOGOUT------------------------------------------------- + /** + * Result toast will always display a success, because when user chooses logout all data are + * cleared regardless of the actual outcome + */ + protected class LogoutTask extends AsyncTask { //Attempt logout + ProgressDialog progressDialog; + + protected Integer doInBackground(Void... voids) { + return sessionManager.logout(); + } + + protected void onPreExecute() + { //Show a progress dialog until done + progressDialog = new ProgressDialog(BaseActivity.this, + R.style.AppTheme_Dark_Dialog); + progressDialog.setCancelable(false); + progressDialog.setIndeterminate(true); + progressDialog.setMessage("Logging out..."); + progressDialog.show(); + } + + protected void onPostExecute(Integer result) + { + Toast.makeText(getBaseContext(), "Logged out successfully!", Toast.LENGTH_LONG).show(); + updateDrawer(); + progressDialog.dismiss(); + } + } +//-----------------------------------------LOGOUT END----------------------------------------------- + + +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/base/BaseFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/base/BaseFragment.java new file mode 100644 index 00000000..6b0b93c9 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/base/BaseFragment.java @@ -0,0 +1,78 @@ +package gr.thmmy.mthmmy.activities.base; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +import mthmmy.utils.Report; +import okhttp3.OkHttpClient; + +public abstract class BaseFragment extends Fragment { + protected static final String ARG_SECTION_NUMBER = "SectionNumber"; + protected static final String ARG_TAG = "FragmentTAG"; + + protected FragmentInteractionListener fragmentInteractionListener; + + private String TAG; + protected int sectionNumber; + protected OkHttpClient client; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + TAG = getArguments().getString(ARG_TAG); + sectionNumber = getArguments().getInt(ARG_SECTION_NUMBER); + client = BaseActivity.getClient(); + Report.d(TAG, "onCreate"); + } + + @Override + public void onStart() { + super.onStart(); + Report.d(TAG, "onStart"); + } + + @Override + public void onResume() { + super.onResume(); + Report.d(TAG, "onResume"); + } + + @Override + public void onPause() { + super.onPause(); + Report.d(TAG, "onPause"); + } + + @Override + public void onStop() { + super.onStop(); + Report.d(TAG, "onStop"); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof FragmentInteractionListener) { + fragmentInteractionListener = (FragmentInteractionListener) context; + + } else { + throw new RuntimeException(context.toString() + + " must implement OnFragmentInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + fragmentInteractionListener = null; + } + + /** + * This interface MUST be extended by the fragment subclass AND implemented by + * the activity that contains it, to allow communication upon interaction, + * between the fragment and the activity/ other fragments + */ + public interface FragmentInteractionListener {} +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java new file mode 100644 index 00000000..bedc7ed1 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java @@ -0,0 +1,322 @@ +package gr.thmmy.mthmmy.activities.board; + +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; +import java.util.Objects; + +import javax.net.ssl.SSLHandshakeException; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.LoginActivity; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import gr.thmmy.mthmmy.model.Board; +import gr.thmmy.mthmmy.model.Topic; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.Request; +import okhttp3.Response; + +public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMoreListener { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "BoardActivity"; + /** + * The key to use when putting board's url String to {@link BoardActivity}'s Bundle. + */ + public static final String BUNDLE_BOARD_URL = "BOARD_URL"; + /** + * The key to use when putting board's title String to {@link BoardActivity}'s Bundle. + */ + public static final String BUNDLE_BOARD_TITLE = "BOARD_TITLE"; + + private MaterialProgressBar progressBar; + private FloatingActionButton newTopicFAB; + private BoardTask boardTask; + + private BoardAdapter boardAdapter; + private final ArrayList parsedSubBoards = new ArrayList<>(); + private final ArrayList parsedTopics = new ArrayList<>(); + + private String boardUrl; + private String boardTitle; + + private int numberOfPages = -1; + private int pagesLoaded = 0; + private boolean isLoadingMore; + private static final int visibleThreshold = 5; + private int lastVisibleItem, totalItemCount; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_board); + + Bundle extras = getIntent().getExtras(); + boardTitle = extras.getString(BUNDLE_BOARD_TITLE); + boardUrl = extras.getString(BUNDLE_BOARD_URL); + + //Initializes graphics + toolbar = (Toolbar) findViewById(R.id.toolbar); + if (boardTitle != null && !Objects.equals(boardTitle, "")) toolbar.setTitle(boardTitle); + else toolbar.setTitle("Board"); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + createDrawer(); + progressBar = (MaterialProgressBar) findViewById(R.id.progressBar); + newTopicFAB = (FloatingActionButton) findViewById(R.id.board_fab); + newTopicFAB.setEnabled(false); + if (!sessionManager.isLoggedIn()) newTopicFAB.hide(); + else { + newTopicFAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (sessionManager.isLoggedIn()) { + //TODO PM + } else { + new AlertDialog.Builder(BoardActivity.this) + .setMessage("You need to be logged in to create a new topic!") + .setPositiveButton("Login", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + Intent intent = new Intent(BoardActivity.this, LoginActivity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.push_right_in, R.anim.push_right_out); + } + }) + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + } + }) + .show(); + } + } + }); + } + + boardAdapter = new BoardAdapter(getApplicationContext(), parsedSubBoards, parsedTopics); + RecyclerView mainContent = (RecyclerView) findViewById(R.id.board_recycler_view); + mainContent.setAdapter(boardAdapter); + final LinearLayoutManager layoutManager = new LinearLayoutManager(this); + mainContent.setLayoutManager(layoutManager); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mainContent.getContext(), + layoutManager.getOrientation()); + mainContent.addItemDecoration(dividerItemDecoration); + + mainContent.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + totalItemCount = layoutManager.getItemCount(); + lastVisibleItem = layoutManager.findLastVisibleItemPosition(); + + if (!isLoadingMore && totalItemCount <= (lastVisibleItem + visibleThreshold)) { + isLoadingMore = true; + onLoadMore(); + } + } + }); + + boardTask = new BoardTask(); + boardTask.execute(boardUrl); + } + + @Override + public void onLoadMore() { + if (pagesLoaded < numberOfPages) { + parsedTopics.add(null); + boardAdapter.notifyItemInserted(parsedSubBoards.size() + parsedTopics.size()); + + //Load data + boardTask = new BoardTask(); + boardTask.execute(boardUrl.substring(0, boardUrl.lastIndexOf(".")) + "." + pagesLoaded * 20); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (boardTask != null && boardTask.getStatus() != AsyncTask.Status.RUNNING) + boardTask.cancel(true); + } + + /** + * An {@link AsyncTask} that handles asynchronous fetching of a board page and parsing it's content. + *

BoardTask's {@link AsyncTask#execute execute} method needs a boards's url as String + * parameter!

+ */ + public class BoardTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "BoardTask"; //Separate tag for AsyncTask + + protected void onPreExecute() { + if (!isLoadingMore) progressBar.setVisibility(ProgressBar.VISIBLE); + if (newTopicFAB.getVisibility() != View.GONE) newTopicFAB.setEnabled(false); + } + + @Override + protected Boolean doInBackground(String... boardUrl) { + Request request = new Request.Builder() + .url(boardUrl[0]) + .build(); + try { + Response response = BaseActivity.getClient().newCall(request).execute(); + return parseBoard(Jsoup.parse(response.body().string())); + } catch (SSLHandshakeException e) { + Report.w(TAG, "Certificate problem (please switch to unsafe connection)."); + } catch (Exception e) { + Report.e("TAG", "ERROR", e); + } + return false; + } + + protected void onPostExecute(Boolean result) { + if (!result) { //Parse failed! + Report.d(TAG, "Parse failed!"); + Toast.makeText(getApplicationContext() + , "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show(); + finish(); + } + //Parse was successful + ++pagesLoaded; + if (newTopicFAB.getVisibility() != View.GONE) newTopicFAB.setEnabled(true); + progressBar.setVisibility(ProgressBar.INVISIBLE); + boardAdapter.notifyDataSetChanged(); + isLoadingMore = false; + } + + private boolean parseBoard(Document boardPage) { + if (boardTitle == null || Objects.equals(boardTitle, "")) + boardTitle = boardPage.select("div.nav a.nav").last().text(); + + //Removes loading item + if (isLoadingMore) { + if (parsedTopics.size() > 0) parsedTopics.remove(parsedTopics.size() - 1); + } + //Finds number of pages + if (numberOfPages == -1) { + numberOfPages = 1; + try { + Elements pages = boardPage.select("table.tborder td.catbg[height=30]").first() + .select("a.navPages"); + if (pages != null && !pages.isEmpty()) { + for (Element page : pages) { + if (Integer.parseInt(page.text()) > numberOfPages) + numberOfPages = Integer.parseInt(page.text()); + } + } + } catch (NullPointerException nullP) { + //It just means this board has only one page of topics. + } + } + { //Finds sub boards + Elements subBoardRows = boardPage.select("div.tborder>table>tbody>tr"); + if (subBoardRows != null && !subBoardRows.isEmpty()) { + for (Element subBoardRow : subBoardRows) { + if (!Objects.equals(subBoardRow.className(), "titlebg")) { + String pUrl = "", pTitle = "", pMods = "", pStats = "", + pLastPost = "No posts yet", pLastPostUrl = ""; + Elements subBoardColumns = subBoardRow.select(">td"); + for (Element subBoardCol : subBoardColumns) { + if (Objects.equals(subBoardCol.className(), "windowbg")) + pStats = subBoardCol.text(); + else if (Objects.equals(subBoardCol.className(), "smalltext")) { + pLastPost = subBoardCol.text(); + if (pLastPost.contains(" in ")) { + pLastPost = pLastPost.substring(0, pLastPost.indexOf(" in ")) + + "\n" + + pLastPost.substring(pLastPost.indexOf(" in ") + 1, pLastPost.indexOf(" by ")) + + "\n" + + pLastPost.substring(pLastPost.lastIndexOf(" by ") + 1); + pLastPostUrl = subBoardCol.select("a").first().attr("href"); + } else if (pLastPost.contains(" σε ")) { + pLastPost = pLastPost.substring(0, pLastPost.indexOf(" σε ")) + + "\n" + + pLastPost.substring(pLastPost.indexOf(" σε ") + 1, pLastPost.indexOf(" από ")) + + "\n" + + pLastPost.substring(pLastPost.lastIndexOf(" από ") + 1); + pLastPostUrl = subBoardCol.select("a").first().attr("href"); + } else { + pLastPost = "No posts yet."; + pLastPostUrl = ""; + } + } else { + pUrl = subBoardCol.select("a").first().attr("href"); + pTitle = subBoardCol.select("a").first().text(); + pMods = subBoardCol.select("div.smalltext").first().text(); + } + } + parsedSubBoards.add(new Board(pUrl, pTitle, pMods, pStats, pLastPost, pLastPostUrl)); + } + } + } + } + { //Finds topics + Elements topicRows = boardPage.select("table.bordercolor>tbody>tr"); + if (topicRows != null && !topicRows.isEmpty()) { + for (Element topicRow : topicRows) { + if (!Objects.equals(topicRow.className(), "titlebg")) { + String pTopicUrl, pSubject, pStartedBy, pLastPost, pLastPostUrl, pStats; + boolean pLocked = false, pSticky = false; + Elements topicColumns = topicRow.select(">td"); + { + Element column = topicColumns.get(2); + Element tmp = column.select("span[id^=msg_] a").first(); + pTopicUrl = tmp.attr("href"); + pSubject = tmp.text(); + if (column.select("img[id^=stickyicon]").first() != null) + pSticky = true; + if (column.select("img[id^=lockicon]").first() != null) + pLocked = true; + } + pStartedBy = topicColumns.get(3).text(); + pStats = "Replies " + topicColumns.get(4).text() + ", Views " + topicColumns.get(5).text(); + + pLastPost = topicColumns.last().text(); + if (pLastPost.contains("by")) { + pLastPost = pLastPost.substring(0, pLastPost.indexOf("by")) + + "\n" + pLastPost.substring(pLastPost.indexOf("by")); + } else { + pLastPost = pLastPost.substring(0, pLastPost.indexOf("από")) + + "\n" + pLastPost.substring(pLastPost.indexOf("από")); + } + pLastPostUrl = topicColumns.last().select("a:has(img)").first().attr("href"); + parsedTopics.add(new Topic(pTopicUrl, pSubject, pStartedBy, pLastPost, pLastPostUrl, + pStats, pLocked, pSticky)); + } + } + } + } + return true; + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java new file mode 100644 index 00000000..306302fe --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java @@ -0,0 +1,320 @@ +package gr.thmmy.mthmmy.activities.board; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Objects; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.topic.TopicActivity; +import gr.thmmy.mthmmy.model.Board; +import gr.thmmy.mthmmy.model.Topic; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_TITLE; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_URL; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL; + +/** + * {@link RecyclerView.Adapter} that can display a {@link gr.thmmy.mthmmy.model.Board}. + */ +class BoardAdapter extends RecyclerView.Adapter { + private static final String TAG = "BoardAdapter"; + private final int VIEW_TYPE_SUB_BOARD_TITLE = 0; + private final int VIEW_TYPE_SUB_BOARD = 1; + private final int VIEW_TYPE_TOPIC_TITLE = 2; + private final int VIEW_TYPE_TOPIC = 3; + private final int VIEW_TYPE_LOADING = 4; + + private final Context context; + private ArrayList parsedSubBoards = new ArrayList<>(); + private ArrayList parsedTopics = new ArrayList<>(); + private final ArrayList boardExpandableVisibility = new ArrayList<>(); + private final ArrayList topicExpandableVisibility = new ArrayList<>(); + + BoardAdapter(Context context, ArrayList parsedSubBoards, ArrayList parsedTopics) { + this.context = context; + this.parsedSubBoards = parsedSubBoards; + this.parsedTopics = parsedTopics; + } + + interface OnLoadMoreListener { + void onLoadMore(); + } + + @Override + public int getItemViewType(int position) { + if (position <= parsedSubBoards.size()) { + if (position == 0) return VIEW_TYPE_SUB_BOARD_TITLE; + return VIEW_TYPE_SUB_BOARD; + } else if (position <= parsedSubBoards.size() + parsedTopics.size() + 1) { + if (position == parsedSubBoards.size() + 1) return VIEW_TYPE_TOPIC_TITLE; + if (parsedTopics.get(position - parsedSubBoards.size() - 1 - 1) != null) + return VIEW_TYPE_TOPIC; + } + return VIEW_TYPE_LOADING; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_SUB_BOARD_TITLE) { + TextView subBoardTitle = new TextView(context); + subBoardTitle.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT + , LinearLayout.LayoutParams.WRAP_CONTENT)); + subBoardTitle.setText(context.getString(R.string.child_board_title)); + subBoardTitle.setTypeface(subBoardTitle.getTypeface(), Typeface.BOLD); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + subBoardTitle.setBackgroundColor(context.getColor(R.color.card_background)); + subBoardTitle.setTextColor(context.getColor(R.color.accent)); + } else { + //noinspection deprecation + subBoardTitle.setBackgroundColor(context.getResources().getColor(R.color.card_background)); + //noinspection deprecation + subBoardTitle.setTextColor(context.getResources().getColor(R.color.accent)); + } + subBoardTitle.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + subBoardTitle.setTextSize(20f); + + return new TitlesViewHolder(subBoardTitle); + } else if (viewType == VIEW_TYPE_SUB_BOARD) { + View subBoard = LayoutInflater.from(parent.getContext()). + inflate(R.layout.activity_board_sub_board, parent, false); + return new SubBoardViewHolder(subBoard); + } else if (viewType == VIEW_TYPE_TOPIC_TITLE) { + TextView topicTitle = new TextView(context); + topicTitle.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT + , LinearLayout.LayoutParams.WRAP_CONTENT)); + topicTitle.setText(context.getString(R.string.topic_title)); + topicTitle.setTypeface(topicTitle.getTypeface(), Typeface.BOLD); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + topicTitle.setTextColor(context.getColor(R.color.primary_text)); + } else { + //noinspection deprecation + topicTitle.setTextColor(context.getResources().getColor(R.color.primary_text)); + } + topicTitle.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + topicTitle.setTextSize(20f); + + return new TitlesViewHolder(topicTitle); + } else if (viewType == VIEW_TYPE_TOPIC) { + View topic = LayoutInflater.from(parent.getContext()). + inflate(R.layout.activity_board_topic, parent, false); + return new TopicViewHolder(topic); + } else if (viewType == VIEW_TYPE_LOADING) { + View loading = LayoutInflater.from(parent.getContext()). + inflate(R.layout.recycler_loading_item, parent, false); + return new LoadingViewHolder(loading); + } + return null; + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) { + if (holder instanceof SubBoardViewHolder) { + final Board subBoard = parsedSubBoards.get(position - 1); + final SubBoardViewHolder subBoardViewHolder = (SubBoardViewHolder) holder; + + if (boardExpandableVisibility.size() != parsedSubBoards.size()) { + for (int i = boardExpandableVisibility.size(); i < parsedSubBoards.size(); ++i) + boardExpandableVisibility.add(false); + } + + subBoardViewHolder.boardRow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(context, BoardActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_BOARD_URL, subBoard.getUrl()); + extras.putString(BUNDLE_BOARD_TITLE, subBoard.getTitle()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + if (boardExpandableVisibility.get(subBoardViewHolder.getAdapterPosition() - 1)) { + subBoardViewHolder.boardExpandable.setVisibility(View.VISIBLE); + subBoardViewHolder.showHideExpandable.setImageResource(R.drawable.ic_arrow_drop_up); + } else { + subBoardViewHolder.boardExpandable.setVisibility(View.GONE); + subBoardViewHolder.showHideExpandable.setImageResource(R.drawable.ic_arrow_drop_down); + } + subBoardViewHolder.showHideExpandable.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final boolean visible = boardExpandableVisibility.get(subBoardViewHolder.getAdapterPosition() - 1); + if (visible) { + subBoardViewHolder.boardExpandable.setVisibility(View.GONE); + subBoardViewHolder.showHideExpandable.setImageResource(R.drawable.ic_arrow_drop_down); + } else { + subBoardViewHolder.boardExpandable.setVisibility(View.VISIBLE); + subBoardViewHolder.showHideExpandable.setImageResource(R.drawable.ic_arrow_drop_up); + } + boardExpandableVisibility.set(subBoardViewHolder.getAdapterPosition() - 1, !visible); + } + }); + subBoardViewHolder.boardTitle.setText(subBoard.getTitle()); + subBoardViewHolder.boardMods.setText(subBoard.getMods()); + subBoardViewHolder.boardStats.setText(subBoard.getStats()); + subBoardViewHolder.boardLastPost.setText(subBoard.getLastPost()); + if (!Objects.equals(subBoard.getLastPostUrl(), "")) { + subBoardViewHolder.boardLastPost.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(context, TopicActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_TOPIC_URL, subBoard.getLastPostUrl()); + //Doesn't put an already ellipsized topic title in Bundle + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + } + } else if (holder instanceof TopicViewHolder) { + final Topic topic = parsedTopics.get(position - parsedSubBoards.size() - 1 - 1); + final TopicViewHolder topicViewHolder = (TopicViewHolder) holder; + + if (topicExpandableVisibility.size() != parsedTopics.size()) { + for (int i = topicExpandableVisibility.size(); i < parsedTopics.size(); ++i) + topicExpandableVisibility.add(false); + } + + topicViewHolder.topicRow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(context, TopicActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_TOPIC_URL, topic.getUrl()); + extras.putString(BUNDLE_TOPIC_TITLE, topic.getSubject()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + if (topicExpandableVisibility.get(topicViewHolder.getAdapterPosition() - parsedSubBoards + .size() - 2)) { + topicViewHolder.topicExpandable.setVisibility(View.VISIBLE); + topicViewHolder.showHideExpandable.setImageResource(R.drawable.ic_arrow_drop_up); + } else { + topicViewHolder.topicExpandable.setVisibility(View.GONE); + topicViewHolder.showHideExpandable.setImageResource(R.drawable.ic_arrow_drop_down); + } + topicViewHolder.showHideExpandable.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final boolean visible = topicExpandableVisibility.get(topicViewHolder. + getAdapterPosition() - parsedSubBoards.size() - 2); + if (visible) { + topicViewHolder.topicExpandable.setVisibility(View.GONE); + topicViewHolder.showHideExpandable.setImageResource(R.drawable.ic_arrow_drop_down); + } else { + topicViewHolder.topicExpandable.setVisibility(View.VISIBLE); + topicViewHolder.showHideExpandable.setImageResource(R.drawable.ic_arrow_drop_up); + } + topicExpandableVisibility.set(topicViewHolder.getAdapterPosition() - + parsedSubBoards.size() - 2, !visible); + } + }); + topicViewHolder.topicSubject.setTypeface(Typeface.createFromAsset(context.getAssets() + , "fonts/fontawesome-webfont.ttf")); + String lockedSticky = topic.getSubject(); + if (topic.isLocked()) + lockedSticky += " " + context.getResources().getString(R.string.fa_lock); + if (topic.isSticky()) { + //topicViewHolder.topicSubject.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0); + lockedSticky += " " + context.getResources().getString(R.string.fa_sticky); + } + topicViewHolder.topicSubject.setText(lockedSticky); + topicViewHolder.topicStartedBy.setText(context.getString(R.string.topic_started_by, topic.getStarter())); + topicViewHolder.topicStats.setText(topic.getStats()); + topicViewHolder.topicLastPost.setText(context.getString(R.string.topic_last_post, topic.getLastPostDateAndTime())); + topicViewHolder.topicLastPost.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(context, TopicActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_TOPIC_URL, topic.getLastPostUrl()); + //Doesn't put an already ellipsized topic title in Bundle + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + } else if (holder instanceof LoadingViewHolder) { + LoadingViewHolder loadingViewHolder = (LoadingViewHolder) holder; + loadingViewHolder.progressBar.setIndeterminate(true); + } + } + + @Override + public int getItemCount() { + int items = 0; + if (parsedSubBoards != null) items += parsedSubBoards.size() + 1; + if (parsedTopics != null) items += parsedTopics.size() + 1; + return items; + } + + private static class SubBoardViewHolder extends RecyclerView.ViewHolder { + final LinearLayout boardRow, boardExpandable; + final TextView boardTitle, boardMods, boardStats, boardLastPost; + final ImageButton showHideExpandable; + + SubBoardViewHolder(View board) { + super(board); + boardRow = (LinearLayout) board.findViewById(R.id.child_board_row); + boardExpandable = (LinearLayout) board.findViewById(R.id.child_board_expandable); + showHideExpandable = (ImageButton) board.findViewById(R.id.child_board_expand_collapse_button); + boardTitle = (TextView) board.findViewById(R.id.child_board_title); + boardMods = (TextView) board.findViewById(R.id.child_board_mods); + boardStats = (TextView) board.findViewById(R.id.child_board_stats); + boardLastPost = (TextView) board.findViewById(R.id.child_board_last_post); + } + } + + private static class TopicViewHolder extends RecyclerView.ViewHolder { + final LinearLayout topicRow, topicExpandable; + final TextView topicSubject, topicStartedBy, topicStats, topicLastPost; + final ImageButton showHideExpandable; + + TopicViewHolder(View topic) { + super(topic); + topicRow = (LinearLayout) topic.findViewById(R.id.topic_row_linear); + topicExpandable = (LinearLayout) topic.findViewById(R.id.topic_expandable); + showHideExpandable = (ImageButton) topic.findViewById(R.id.topic_expand_collapse_button); + topicSubject = (TextView) topic.findViewById(R.id.topic_subject); + topicStartedBy = (TextView) topic.findViewById(R.id.topic_started_by); + topicStats = (TextView) topic.findViewById(R.id.topic_stats); + topicLastPost = (TextView) topic.findViewById(R.id.topic_last_post); + } + } + + private static class TitlesViewHolder extends RecyclerView.ViewHolder { + TitlesViewHolder(View title) { + super(title); + } + } + + private static class LoadingViewHolder extends RecyclerView.ViewHolder { + final MaterialProgressBar progressBar; + + LoadingViewHolder(View itemView) { + super(itemView); + progressBar = (MaterialProgressBar) itemView.findViewById(R.id.recycler_progress_bar); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java new file mode 100644 index 00000000..cb9981b1 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java @@ -0,0 +1,147 @@ +package gr.thmmy.mthmmy.activities.main; + +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.widget.Toolbar; +import android.widget.Toast; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.LoginActivity; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import gr.thmmy.mthmmy.activities.board.BoardActivity; +import gr.thmmy.mthmmy.activities.main.forum.ForumFragment; +import gr.thmmy.mthmmy.activities.main.recent.RecentFragment; +import gr.thmmy.mthmmy.activities.topic.TopicActivity; +import gr.thmmy.mthmmy.model.Board; +import gr.thmmy.mthmmy.model.TopicSummary; + +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_TITLE; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_URL; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL; + +public class MainActivity extends BaseActivity implements RecentFragment.RecentFragmentInteractionListener, ForumFragment.ForumFragmentInteractionListener { + + //----------------------------------------CLASS VARIABLES----------------------------------------- + private static final String TAG = "MainActivity"; + private static final int TIME_INTERVAL = 2000; + private long mBackPressed; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (sessionManager.isLoginScreenDefault()) { + //Go to login + Intent intent = new Intent(MainActivity.this, LoginActivity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.push_right_in, R.anim.push_right_out); + } + + //Initialize toolbar + toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + //Initialize drawer + createDrawer(); + + //Create the adapter that will return a fragment for each section of the activity + SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); + + //Set up the ViewPager with the sections adapter. + ViewPager mViewPager = (ViewPager) findViewById(R.id.container); + mViewPager.setAdapter(mSectionsPagerAdapter); + + TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); + tabLayout.setupWithViewPager(mViewPager); + } + + @Override + protected void onResume() { + drawer.setSelection(HOME_ID); + super.onResume(); + } + + @Override + public void onBackPressed() { + if(drawer.isDrawerOpen()){ + drawer.closeDrawer(); + return; + } + else if (mBackPressed + TIME_INTERVAL > System.currentTimeMillis()) { + super.onBackPressed(); + return; + } else { + Toast.makeText(getBaseContext(), "Press back again to exit!" + , Toast.LENGTH_SHORT).show(); + } + mBackPressed = System.currentTimeMillis(); + } + + @Override + public void onRecentFragmentInteraction(TopicSummary topicSummary) { + Intent i = new Intent(MainActivity.this, TopicActivity.class); + i.putExtra(BUNDLE_TOPIC_URL, topicSummary.getTopicUrl()); + i.putExtra(BUNDLE_TOPIC_TITLE, topicSummary.getSubject()); + startActivity(i); + } + + @Override + public void onForumFragmentInteraction(Board board) { + Intent i = new Intent(MainActivity.this, BoardActivity.class); + i.putExtra(BUNDLE_BOARD_URL, board.getUrl()); + i.putExtra(BUNDLE_BOARD_TITLE, board.getTitle()); + startActivity(i); + } + +//---------------------------------FragmentPagerAdapter--------------------------------------------- + + /** + * A {@link FragmentPagerAdapter} that returns a fragment corresponding to + * one of the sections/tabs/pages. If it becomes too memory intensive, + * it may be best to switch to a + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + */ + public class SectionsPagerAdapter extends FragmentPagerAdapter { + + SectionsPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + switch(position) { + case 0: return RecentFragment.newInstance(position +1); + case 1: return ForumFragment.newInstance(position +1); + default: return RecentFragment.newInstance(position +1); //temp (?) + } + } + + @Override + public int getCount() { + // Show 2 total pages. + return 2; + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case 0: + return "RECENT POSTS"; + case 1: + return "FORUM"; + } + + return null; + } + } +//-------------------------------FragmentPagerAdapter END------------------------------------------- + +} \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumAdapter.java new file mode 100644 index 00000000..896078e8 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumAdapter.java @@ -0,0 +1,113 @@ +package gr.thmmy.mthmmy.activities.main.forum; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.bignerdranch.expandablerecyclerview.ChildViewHolder; +import com.bignerdranch.expandablerecyclerview.ExpandableRecyclerAdapter; +import com.bignerdranch.expandablerecyclerview.ParentViewHolder; + +import java.util.List; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseFragment; +import gr.thmmy.mthmmy.model.Board; +import gr.thmmy.mthmmy.model.Category; +import gr.thmmy.mthmmy.model.TopicSummary; + + +/** + * {@link RecyclerView.Adapter} that can display a {@link TopicSummary} and makes a call to the + * specified {@link ForumFragment.ForumFragmentInteractionListener}. + */ +class ForumAdapter extends ExpandableRecyclerAdapter { + private final Context context; + private final LayoutInflater layoutInflater; + + private final List categories; + private final ForumFragment.ForumFragmentInteractionListener mListener; + + ForumAdapter(Context context, @NonNull List categories, BaseFragment.FragmentInteractionListener listener) { + super(categories); + this.context = context; + this.categories = categories; + mListener = (ForumFragment.ForumFragmentInteractionListener)listener; + layoutInflater = LayoutInflater.from(context); + } + + @NonNull + @Override + public CategoryViewHolder onCreateParentViewHolder(@NonNull ViewGroup parentViewGroup, int viewType) { + View categoryView = layoutInflater.inflate(R.layout.fragment_forum_category_row, parentViewGroup, false); + return new CategoryViewHolder(categoryView); + } + + @NonNull + @Override + public BoardViewHolder onCreateChildViewHolder(@NonNull ViewGroup childViewGroup, int viewType) { + View boardView = layoutInflater.inflate(R.layout.fragment_forum_board_row, childViewGroup, false); + return new BoardViewHolder(boardView); + } + + @Override + public void onBindParentViewHolder(@NonNull CategoryViewHolder parentViewHolder, int parentPosition, @NonNull Category parent) { + parentViewHolder.bind(parent); + } + + @Override + public void onBindChildViewHolder(@NonNull BoardViewHolder childViewHolder, int parentPosition, int childPosition, @NonNull Board child) { + childViewHolder.board = categories.get(parentPosition).getBoards().get(childPosition); + childViewHolder.bind(child); + } + + + class CategoryViewHolder extends ParentViewHolder { + + private TextView categoryTextview; + + CategoryViewHolder(View itemView) { + super(itemView); + categoryTextview = (TextView) itemView.findViewById(R.id.category); + } + + void bind(Category category) { + categoryTextview.setText(category.getTitle()); + } + + } + + class BoardViewHolder extends ChildViewHolder { + + private TextView boardTextView; + public Board board; + + BoardViewHolder(View itemView) { + super(itemView); + boardTextView = (TextView) itemView.findViewById(R.id.board); + } + + void bind(final Board board) { + boardTextView.setText(board.getTitle()); + + + boardTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + if (null != mListener) { + // Notify the active callbacks interface (the activity, if the + // fragment is attached to one) that an item has been selected. + mListener.onForumFragmentInteraction(board); //? + + } + + } + }); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java new file mode 100644 index 00000000..9b51c3af --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java @@ -0,0 +1,236 @@ +package gr.thmmy.mthmmy.activities.main.forum; + +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.Toast; + +import com.bignerdranch.expandablerecyclerview.ExpandableRecyclerAdapter; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import gr.thmmy.mthmmy.activities.base.BaseFragment; +import gr.thmmy.mthmmy.model.Board; +import gr.thmmy.mthmmy.model.Category; +import gr.thmmy.mthmmy.session.SessionManager; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; + +/** + * A {@link BaseFragment} subclass. + * Activities that contain this fragment must implement the + * {@link ForumFragment.ForumFragmentInteractionListener} interface + * to handle interaction events. + * Use the {@link ForumFragment#newInstance} factory method to + * create an instance of this fragment. + */ +public class ForumFragment extends BaseFragment +{ + private static final String TAG = "ForumFragment"; + // Fragment initialization parameters, e.g. ARG_SECTION_NUMBER + + private MaterialProgressBar progressBar; + private ForumAdapter forumAdapter; + + private List categories; + + private ForumTask forumTask; + + // Required empty public constructor + public ForumFragment() {} + + /** + * Use ONLY this factory method to create a new instance of + * this fragment using the provided parameters. + * @return A new instance of fragment Forum. + */ + public static ForumFragment newInstance(int sectionNumber) { + ForumFragment fragment = new ForumFragment(); + Bundle args = new Bundle(); + args.putString(ARG_TAG, TAG); + args.putInt(ARG_SECTION_NUMBER, sectionNumber); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + categories = new ArrayList<>(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (categories.isEmpty()) + { + forumTask =new ForumTask(); + forumTask.execute(); + + } + Report.d(TAG, "onActivityCreated"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + final View rootView = inflater.inflate(R.layout.fragment_forum, container, false); + + // Set the adapter + if (rootView instanceof RelativeLayout) { + progressBar = (MaterialProgressBar) rootView.findViewById(R.id.progressBar); + forumAdapter = new ForumAdapter(getContext(), categories, fragmentInteractionListener); + forumAdapter.setExpandCollapseListener(new ExpandableRecyclerAdapter.ExpandCollapseListener() { + @Override + public void onParentExpanded(int parentPosition) { + if(BaseActivity.getSessionManager().isLoggedIn()) + { + if(forumTask.getStatus()== AsyncTask.Status.RUNNING) + forumTask.cancel(true); + forumTask =new ForumTask(); + forumTask.setUrl(categories.get(parentPosition).getCategoryURL()); + forumTask.execute(); + } + } + + @Override + public void onParentCollapsed(int parentPosition) { + if(BaseActivity.getSessionManager().isLoggedIn()) + { + if(forumTask.getStatus()== AsyncTask.Status.RUNNING) + forumTask.cancel(true); + forumTask =new ForumTask(); + forumTask.setUrl(categories.get(parentPosition).getCategoryURL()); + forumTask.execute(); + } + } + }); + + RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.list); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(rootView.findViewById(R.id.list).getContext()); + recyclerView.setLayoutManager(linearLayoutManager); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(), + linearLayoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + recyclerView.setAdapter(forumAdapter); + + } + return rootView; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if(forumTask!=null&&forumTask.getStatus()!= AsyncTask.Status.RUNNING) + forumTask.cancel(true); + } + + public interface ForumFragmentInteractionListener extends FragmentInteractionListener{ + void onForumFragmentInteraction(Board board); + } + + //---------------------------------------ASYNC TASK----------------------------------- + + public class ForumTask extends AsyncTask { + private static final String TAG = "ForumTask"; + private HttpUrl forumUrl = SessionManager.forumUrl; //may change upon collapse/expand + private Document document; + + private final List fetchedCategories; + + ForumTask() { + fetchedCategories = new ArrayList<>(); + } + + protected void onPreExecute() { + progressBar.setVisibility(ProgressBar.VISIBLE); + } + + protected Integer doInBackground(Void... voids) { + Request request = new Request.Builder() + .url(forumUrl) + .build(); + try { + Response response = client.newCall(request).execute(); + document = Jsoup.parse(response.body().string()); + parse(document); + categories.clear(); + categories.addAll(fetchedCategories); + fetchedCategories.clear(); + return 0; + } catch (IOException e) { + Report.d(TAG, "Network Error", e); + return 1; + } catch (Exception e) { + Report.d(TAG, "Exception", e); + return 2; + } + + } + + + protected void onPostExecute(Integer result) { + + if (result == 0) + forumAdapter.notifyParentDataSetChanged(false); + else if (result == 1) + Toast.makeText(getActivity(), "Network error", Toast.LENGTH_SHORT).show(); + + progressBar.setVisibility(ProgressBar.INVISIBLE); + } + + private void parse(Document document) + { + Elements categoryBlocks = document.select(".tborder:not([style])>table[cellpadding=5]"); + if (categoryBlocks.size() != 0) { + for(Element categoryBlock: categoryBlocks) + { + Element categoryElement = categoryBlock.select("td[colspan=2]>[name]").first(); + String categoryUrl = categoryElement.attr("href"); + Category category = new Category(categoryElement.text(), categoryUrl); + + if(categoryUrl.contains("sa=collapse")|| !BaseActivity.getSessionManager().isLoggedIn()) + { + category.setExpanded(true); + Elements boardsElements = categoryBlock.select("b [name]"); + for(Element boardElement: boardsElements) { + Board board = new Board(boardElement.attr("href"), boardElement.text(), null, null, null, null); + category.getBoards().add(board); + } + } + else + category.setExpanded(false); + + fetchedCategories.add(category); + } + } + else + Report.e(TAG, "Parsing failed!"); + } + + public void setUrl(String string) + { + forumUrl = HttpUrl.parse(string); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java new file mode 100644 index 00000000..beb22697 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java @@ -0,0 +1,85 @@ +package gr.thmmy.mthmmy.activities.main.recent; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.List; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseFragment; +import gr.thmmy.mthmmy.model.TopicSummary; + + +/** + * {@link RecyclerView.Adapter} that can display a {@link TopicSummary} and makes a call to the + * specified {@link RecentFragment.RecentFragmentInteractionListener}. + */ +class RecentAdapter extends RecyclerView.Adapter { + private final Context context; + private final List recentList; + private final RecentFragment.RecentFragmentInteractionListener mListener; + + RecentAdapter(Context context, @NonNull List topicSummaryList, BaseFragment.FragmentInteractionListener listener) { + this.context = context; + this.recentList = topicSummaryList; + mListener = (RecentFragment.RecentFragmentInteractionListener) listener; + } + + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.fragment_recent_row, parent, false); + return new ViewHolder(view); + } + + + @Override + public void onBindViewHolder(final ViewHolder holder, final int position) { + holder.mTitleView.setText(recentList.get(position).getSubject()); + holder.mDateTimeView.setText(recentList.get(position).getDateTimeModified()); + holder.mUserView.setText(context.getString(R.string.byUser, recentList.get(position).getLastUser())); + + holder.topic = recentList.get(position); + + holder.mView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + if (null != mListener) { + // Notify the active callbacks interface (the activity, if the + // fragment is attached to one) that an item has been selected. + mListener.onRecentFragmentInteraction(holder.topic); //? + + } + + } + }); + } + + @Override + public int getItemCount() { + return recentList.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + final View mView; + final TextView mTitleView; + final TextView mUserView; + final TextView mDateTimeView; + public TopicSummary topic; + + ViewHolder(View view) { + super(view); + mView = view; + mTitleView = (TextView) view.findViewById(R.id.title); + mUserView = (TextView) view.findViewById(R.id.lastUser); + mDateTimeView = (TextView) view.findViewById(R.id.dateTime); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java new file mode 100644 index 00000000..8b5d14b1 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java @@ -0,0 +1,223 @@ +package gr.thmmy.mthmmy.activities.main.recent; + +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.Toast; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseFragment; +import gr.thmmy.mthmmy.model.TopicSummary; +import gr.thmmy.mthmmy.session.SessionManager; +import gr.thmmy.mthmmy.utils.CustomRecyclerView; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; + +/** + * A {@link BaseFragment} subclass. + * Activities that contain this fragment must implement the + * {@link RecentFragment.RecentFragmentInteractionListener} interface + * to handle interaction events. + * Use the {@link RecentFragment#newInstance} factory method to + * create an instance of this fragment. + */ +public class RecentFragment extends BaseFragment { + private static final String TAG = "RecentFragment"; + // Fragment initialization parameters, e.g. ARG_SECTION_NUMBER + + private MaterialProgressBar progressBar; + private SwipeRefreshLayout swipeRefreshLayout; + private RecentAdapter recentAdapter; + + private List topicSummaries; + + private RecentTask recentTask; + + // Required empty public constructor + public RecentFragment() {} + + /** + * Use ONLY this factory method to create a new instance of + * this fragment using the provided parameters. + * @return A new instance of fragment Recent. + */ + public static RecentFragment newInstance(int sectionNumber) { + RecentFragment fragment = new RecentFragment(); + Bundle args = new Bundle(); + args.putString(ARG_TAG, TAG); + args.putInt(ARG_SECTION_NUMBER, sectionNumber); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + topicSummaries = new ArrayList<>(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (topicSummaries.isEmpty()) + { + recentTask =new RecentTask(); + recentTask.execute(); + + } + Report.d(TAG, "onActivityCreated"); + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + final View rootView = inflater.inflate(R.layout.fragment_recent, container, false); + + // Set the adapter + if (rootView instanceof RelativeLayout) { + progressBar = (MaterialProgressBar) rootView.findViewById(R.id.progressBar); + recentAdapter = new RecentAdapter(getActivity(), topicSummaries, fragmentInteractionListener); + + CustomRecyclerView recyclerView = (CustomRecyclerView) rootView.findViewById(R.id.list); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(rootView.findViewById(R.id.list).getContext()); + recyclerView.setLayoutManager(linearLayoutManager); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(), + linearLayoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + recyclerView.setAdapter(recentAdapter); + + swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swiperefresh); + swipeRefreshLayout.setOnRefreshListener( + new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + if (recentTask != null && recentTask.getStatus() != AsyncTask.Status.RUNNING) { + recentTask = new RecentTask(); + recentTask.execute(); + } + } + + } + ); + } + + return rootView; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if(recentTask!=null&&recentTask.getStatus()!= AsyncTask.Status.RUNNING) + recentTask.cancel(true); + } + + + public interface RecentFragmentInteractionListener extends FragmentInteractionListener { + void onRecentFragmentInteraction(TopicSummary topicSummary); + } + + //---------------------------------------ASYNC TASK----------------------------------- + + public class RecentTask extends AsyncTask { + private static final String TAG = "RecentTask"; + private final HttpUrl thmmyUrl = SessionManager.indexUrl; + private Document document; + + protected void onPreExecute() { + + progressBar.setVisibility(ProgressBar.VISIBLE); + } + + protected Integer doInBackground(Void... voids) { + Request request = new Request.Builder() + .url(thmmyUrl) + .build(); + try { + Response response = client.newCall(request).execute(); + document = Jsoup.parse(response.body().string()); + parse(document); + return 0; + } catch (IOException e) { + Report.d(TAG, "Network Error", e); + return 1; + } catch (Exception e) { + Report.d(TAG, "Exception", e); + return 2; + } + + } + + + protected void onPostExecute(Integer result) { + + if (result == 0) + recentAdapter.notifyDataSetChanged(); + else if (result == 1) + Toast.makeText(getActivity(), "Network error", Toast.LENGTH_SHORT).show(); + + progressBar.setVisibility(ProgressBar.INVISIBLE); + swipeRefreshLayout.setRefreshing(false); + } + + private void parse(Document document) { + Elements recent = document.select("#block8 :first-child div"); + if (recent.size() == 30) { + topicSummaries.clear(); + + for (int i = 0; i < recent.size(); i += 3) { + String link = recent.get(i).child(0).attr("href"); + String title = recent.get(i).child(0).attr("title"); + + String lastUser = recent.get(i + 1).text(); + Pattern pattern = Pattern.compile("\\b (.*)"); + Matcher matcher = pattern.matcher(lastUser); + if (matcher.find()) + lastUser = matcher.group(1); + else { + Report.e(TAG, "Parsing failed (lastUser)!"); + return; + } + + String dateTime = recent.get(i + 2).text(); + pattern = Pattern.compile("\\[(.*)\\]"); + matcher = pattern.matcher(dateTime); + if (matcher.find()) + dateTime = matcher.group(1); + else { + Report.e(TAG, "Parsing failed (dateTime)!"); + return; + } + + + topicSummaries.add(new TopicSummary(link, title, lastUser, dateTime)); + } + + return; + } + Report.e(TAG, "Parsing failed!"); + } + } + +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java new file mode 100644 index 00000000..5899efae --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java @@ -0,0 +1,306 @@ +package gr.thmmy.mthmmy.activities.profile; + +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.squareup.picasso.Picasso; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.net.ssl.SSLHandshakeException; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.LoginActivity; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import gr.thmmy.mthmmy.activities.profile.latestPosts.LatestPostsFragment; +import gr.thmmy.mthmmy.activities.profile.stats.StatsFragment; +import gr.thmmy.mthmmy.activities.profile.summary.SummaryFragment; +import gr.thmmy.mthmmy.activities.topic.TopicActivity; +import gr.thmmy.mthmmy.model.PostSummary; +import gr.thmmy.mthmmy.utils.CircleTransform; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.Request; +import okhttp3.Response; + +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL; + +/** + * Activity for user profile. When creating an Intent of this activity you need to bundle a String + * containing this user's profile url using the key {@link #BUNDLE_PROFILE_URL}, a String containing + * this user's avatar url using the key {@link #BUNDLE_THUMBNAIL_URL} and a String containing + * the username using the key {@link #BUNDLE_USERNAME}. + */ +public class ProfileActivity extends BaseActivity implements LatestPostsFragment.LatestPostsFragmentInteractionListener { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "ProfileActivity"; + /** + * The key to use when putting profile's url String to {@link ProfileActivity}'s Bundle. + */ + public static final String BUNDLE_PROFILE_URL = "PROFILE_URL"; + /** + * The key to use when putting user's thumbnail url String to {@link ProfileActivity}'s Bundle. + * If user doesn't have a thumbnail put an empty string or leave it null. + */ + public static final String BUNDLE_THUMBNAIL_URL = "THUMBNAIL_URL"; + /** + * The key to use when putting username String to {@link ProfileActivity}'s Bundle. + * If username is not available put an empty string or leave it null. + */ + public static final String BUNDLE_USERNAME = "USERNAME"; + private static final int THUMBNAIL_SIZE = 200; + + private TextView usernameView; + private TextView personalTextView; + private MaterialProgressBar progressBar; + private FloatingActionButton pmFAB; + private ViewPager viewPager; + + private ProfileTask profileTask; + private String personalText; + private String profileUrl; + private String username; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_profile); + + Bundle extras = getIntent().getExtras(); + String thumbnailUrl = extras.getString(BUNDLE_THUMBNAIL_URL); + if (thumbnailUrl == null) thumbnailUrl = ""; + username = extras.getString(BUNDLE_USERNAME); + profileUrl = extras.getString(BUNDLE_PROFILE_URL); + + //Initializes graphic elements + toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar.setTitle(null); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowTitleEnabled(false); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + createDrawer(); + + progressBar = (MaterialProgressBar) findViewById(R.id.progressBar); + + ImageView thumbnailView = (ImageView) findViewById(R.id.user_thumbnail); + if (!Objects.equals(thumbnailUrl, "")) + //noinspection ConstantConditions + Picasso.with(this) + .load(thumbnailUrl) + .resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE) + .centerCrop() + .error(ResourcesCompat.getDrawable(this.getResources() + , R.drawable.ic_default_user_thumbnail, null)) + .placeholder(ResourcesCompat.getDrawable(this.getResources() + , R.drawable.ic_default_user_thumbnail, null)) + .transform(new CircleTransform()) + .into(thumbnailView); + usernameView = (TextView) findViewById(R.id.profile_activity_username); + if (username != null && !Objects.equals(username, "")) usernameView.setText(username); + personalTextView = (TextView) findViewById(R.id.profile_activity_personal_text); + + viewPager = (ViewPager) findViewById(R.id.profile_tab_container); + + pmFAB = (FloatingActionButton) findViewById(R.id.profile_fab); + pmFAB.setEnabled(false); + if (!sessionManager.isLoggedIn()) pmFAB.hide(); + else { + pmFAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (sessionManager.isLoggedIn()) { + //TODO PM + } else { + new AlertDialog.Builder(ProfileActivity.this) + .setMessage("You need to be logged in to sent a personal message!") + .setPositiveButton("Login", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + Intent intent = new Intent(ProfileActivity.this, LoginActivity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.push_right_in, R.anim.push_right_out); + } + }) + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + } + }) + .show(); + } + } + }); + } + + profileTask = new ProfileTask(); + profileTask.execute(profileUrl); //Attempts data parsing + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (profileTask != null && profileTask.getStatus() != AsyncTask.Status.RUNNING) + profileTask.cancel(true); + } + + @Override + public void onLatestPostsFragmentInteraction(PostSummary postSummary) { + Intent i = new Intent(ProfileActivity.this, TopicActivity.class); + i.putExtra(BUNDLE_TOPIC_URL, postSummary.getPostUrl()); + i.putExtra(BUNDLE_TOPIC_TITLE, postSummary.getSubject().substring(postSummary.getSubject(). + lastIndexOf("/ ") + 2)); + startActivity(i); + } + + /** + * An {@link AsyncTask} that handles asynchronous fetching of a profile page and parsing this + * user's personal text. The {@link Document} resulting from the parse is stored for use in + * the {@link SummaryFragment}. + *

ProfileTask's {@link AsyncTask#execute execute} method needs a profile's url as String + * parameter!

+ * + * @see Jsoup + */ + public class ProfileTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "ProfileTask"; //Separate tag for AsyncTask + Document profilePage; + + protected void onPreExecute() { + progressBar.setVisibility(ProgressBar.VISIBLE); + if (pmFAB.getVisibility() != View.GONE) pmFAB.setEnabled(false); + } + + protected Boolean doInBackground(String... profileUrl) { + String pageUrl = profileUrl[0] + ";wap"; //Profile's page wap url + + Request request = new Request.Builder() + .url(pageUrl) + .build(); + try { + Response response = client.newCall(request).execute(); + profilePage = Jsoup.parse(response.body().string()); + //Finds username if missing + if (username == null || Objects.equals(username, "")) { + username = profilePage. + select(".bordercolor > tbody:nth-child(1) > tr:nth-child(2) tr"). + first().text(); + } + + { //Finds personal text + Element tmpEl = profilePage.select("td.windowbg:nth-child(2)").first(); + if (tmpEl != null) { + personalText = tmpEl.text().trim(); + } else { + //Should never get here! + //Something is wrong. + Report.e(TAG, "An error occurred while trying to find profile's personal text."); + personalText = null; + } + } + return true; + } catch (SSLHandshakeException e) { + Report.w(TAG, "Certificate problem (please switch to unsafe connection)."); + } catch (Exception e) { + Report.e("TAG", "ERROR", e); + } + return false; + } + + protected void onPostExecute(Boolean result) { + if (!result) { //Parse failed! + Report.d(TAG, "Parse failed!"); + Toast.makeText(getBaseContext() + , "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show(); + finish(); + } + //Parse was successful + if (pmFAB.getVisibility() != View.GONE) pmFAB.setEnabled(true); + progressBar.setVisibility(ProgressBar.INVISIBLE); + + if (usernameView.getText() != username) usernameView.setText(username); + if (personalText != null) personalTextView.setText(personalText); + + setupViewPager(viewPager, profilePage); + TabLayout tabLayout = (TabLayout) findViewById(R.id.profile_tabs); + tabLayout.setupWithViewPager(viewPager); + } + } + + /** + * Simple method that sets up the {@link ViewPager} of a {@link ProfileActivity} + * + * @param viewPager the ViewPager to be setup + * @param profilePage this profile's parsed page + */ + private void setupViewPager(ViewPager viewPager, Document profilePage) { + ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager()); + adapter.addFrag(SummaryFragment.newInstance(profilePage), "SUMMARY"); + adapter.addFrag(LatestPostsFragment.newInstance(profileUrl), "LATEST POSTS"); + adapter.addFrag(StatsFragment.newInstance(profileUrl), "STATS"); + viewPager.setAdapter(adapter); + } + + class ViewPagerAdapter extends FragmentPagerAdapter { + private final List mFragmentList = new ArrayList<>(); + private final List mFragmentTitleList = new ArrayList<>(); + + ViewPagerAdapter(FragmentManager manager) { + super(manager); + } + + @Override + public Fragment getItem(int position) { + return mFragmentList.get(position); + } + + @Override + public int getCount() { + return mFragmentList.size(); + } + + void addFrag(Fragment fragment, String title) { + mFragmentList.add(fragment); + mFragmentTitleList.add(title); + } + + @Override + public CharSequence getPageTitle(int position) { + return mFragmentTitleList.get(position); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java new file mode 100644 index 00000000..7976dd13 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java @@ -0,0 +1,120 @@ +package gr.thmmy.mthmmy.activities.profile.latestPosts; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import java.util.ArrayList; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseFragment; +import gr.thmmy.mthmmy.model.PostSummary; +import gr.thmmy.mthmmy.model.TopicSummary; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; + +/** + * {@link RecyclerView.Adapter} that can display a {@link TopicSummary} and makes a call to the + * specified {@link LatestPostsFragment.LatestPostsFragmentInteractionListener}. + */ +class LatestPostsAdapter extends RecyclerView.Adapter { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "LatestPostsAdapter"; + private final int VIEW_TYPE_ITEM = 0; + private final int VIEW_TYPE_LOADING = 1; + final private LatestPostsFragment.LatestPostsFragmentInteractionListener interactionListener; + private final ArrayList parsedTopicSummaries; + + LatestPostsAdapter(BaseFragment.FragmentInteractionListener interactionListener, + ArrayList parsedTopicSummaries) { + this.interactionListener = (LatestPostsFragment.LatestPostsFragmentInteractionListener) interactionListener; + this.parsedTopicSummaries = parsedTopicSummaries; + } + + interface OnLoadMoreListener { + void onLoadMore(); + } + + @Override + public int getItemViewType(int position) { + return parsedTopicSummaries.get(position) == null ? VIEW_TYPE_LOADING : VIEW_TYPE_ITEM; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_ITEM) { + View view = LayoutInflater.from(parent.getContext()). + inflate(R.layout.fragment_latest_posts_row, parent, false); + return new LatestPostViewHolder(view); + } else if (viewType == VIEW_TYPE_LOADING) { + View view = LayoutInflater.from(parent.getContext()). + inflate(R.layout.recycler_loading_item, parent, false); + return new LoadingViewHolder(view); + } + return null; + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { + if (holder instanceof LatestPostViewHolder) { + PostSummary topic = parsedTopicSummaries.get(position); + final LatestPostViewHolder latestPostViewHolder = (LatestPostViewHolder) holder; + + latestPostViewHolder.postTitle.setText(topic.getSubject()); + latestPostViewHolder.postDate.setText(topic.getDateTime()); + latestPostViewHolder.post.loadDataWithBaseURL("file:///android_asset/" + , topic.getPost(), "text/html", "UTF-8", null); + + latestPostViewHolder.latestPostsRow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (interactionListener != null) { + // Notify the active callbacks interface (the activity, if the + // fragment is attached to one) that a post has been selected. + interactionListener.onLatestPostsFragmentInteraction( + parsedTopicSummaries.get(holder.getAdapterPosition())); + } + + } + }); + } else if (holder instanceof LoadingViewHolder) { + LoadingViewHolder loadingViewHolder = (LoadingViewHolder) holder; + loadingViewHolder.progressBar.setIndeterminate(true); + } + } + + @Override + public int getItemCount() { + return parsedTopicSummaries == null ? 0 : parsedTopicSummaries.size(); + } + + private static class LatestPostViewHolder extends RecyclerView.ViewHolder { + final RelativeLayout latestPostsRow; + final TextView postTitle; + final TextView postDate; + final WebView post; + + LatestPostViewHolder(View itemView) { + super(itemView); + latestPostsRow = (RelativeLayout) itemView.findViewById(R.id.latest_posts_row); + postTitle = (TextView) itemView.findViewById(R.id.title); + postDate = (TextView) itemView.findViewById(R.id.date); + post = (WebView) itemView.findViewById(R.id.post); + } + } + + private static class LoadingViewHolder extends RecyclerView.ViewHolder { + final MaterialProgressBar progressBar; + + LoadingViewHolder(View itemView) { + super(itemView); + progressBar = (MaterialProgressBar) itemView.findViewById(R.id.recycler_progress_bar); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java new file mode 100644 index 00000000..01786f3b --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java @@ -0,0 +1,242 @@ +package gr.thmmy.mthmmy.activities.profile.latestPosts; + +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.Toast; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; + +import javax.net.ssl.SSLHandshakeException; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import gr.thmmy.mthmmy.activities.base.BaseFragment; +import gr.thmmy.mthmmy.model.PostSummary; +import gr.thmmy.mthmmy.utils.ParseHelpers; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Use the {@link LatestPostsFragment#newInstance} factory method to create an instance of this fragment. + */ +public class LatestPostsFragment extends BaseFragment implements LatestPostsAdapter.OnLoadMoreListener{ + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "LatestPostsFragment"; + /** + * The key to use when putting profile's url String to {@link LatestPostsFragment}'s Bundle. + */ + private static final String PROFILE_URL = "PROFILE_URL"; + /** + * {@link ArrayList} of {@link PostSummary} objects used to hold profile's latest posts. Data + * are added in {@link LatestPostsTask}. + */ + private ArrayList parsedTopicSummaries; + private LatestPostsAdapter latestPostsAdapter; + private int numberOfPages = -1; + private int pagesLoaded = 0; + private String profileUrl; + private LatestPostsTask profileLatestPostsTask; + private MaterialProgressBar progressBar; + private boolean isLoadingMore; + private static final int visibleThreshold = 5; + private int lastVisibleItem, totalItemCount; + + public LatestPostsFragment() { + // Required empty public constructor + } + + /** + * Use ONLY this factory method to create a new instance of this fragment using the provided + * parameters. + * + * @param profileUrl String containing this profile's url + * @return A new instance of fragment Summary. + */ + public static LatestPostsFragment newInstance(String profileUrl) { + LatestPostsFragment fragment = new LatestPostsFragment(); + Bundle args = new Bundle(); + args.putString(PROFILE_URL, profileUrl); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + profileUrl = getArguments().getString(PROFILE_URL); + parsedTopicSummaries = new ArrayList<>(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View rootView = inflater.inflate(R.layout.fragment_latest_posts, container, false); + latestPostsAdapter = new LatestPostsAdapter(fragmentInteractionListener, parsedTopicSummaries); + RecyclerView mainContent = (RecyclerView) rootView.findViewById(R.id.profile_latest_posts_recycler); + mainContent.setAdapter(latestPostsAdapter); + final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + mainContent.setLayoutManager(layoutManager); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mainContent.getContext(), + layoutManager.getOrientation()); + mainContent.addItemDecoration(dividerItemDecoration); + + //latestPostsAdapter.setOnLoadMoreListener(); + mainContent.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + totalItemCount = layoutManager.getItemCount(); + lastVisibleItem = layoutManager.findLastVisibleItemPosition(); + + if (!isLoadingMore && totalItemCount <= (lastVisibleItem + visibleThreshold)) { + isLoadingMore = true; + onLoadMore(); + } + } + }); + progressBar = (MaterialProgressBar) rootView.findViewById(R.id.progressBar); + return rootView; + } + + @Override + public void onLoadMore() { + if (pagesLoaded < numberOfPages) { + parsedTopicSummaries.add(null); + latestPostsAdapter.notifyItemInserted(parsedTopicSummaries.size() - 1); + + //Load data + profileLatestPostsTask = new LatestPostsTask(); + profileLatestPostsTask.execute(profileUrl + ";sa=showPosts;start=" + pagesLoaded * 15); + ++pagesLoaded; + } + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (parsedTopicSummaries.isEmpty()) { + profileLatestPostsTask = new LatestPostsTask(); + profileLatestPostsTask.execute(profileUrl + ";sa=showPosts"); + pagesLoaded = 1; + } + Report.d(TAG, "onActivityCreated"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (profileLatestPostsTask != null && profileLatestPostsTask.getStatus() != AsyncTask.Status.RUNNING) + profileLatestPostsTask.cancel(true); + } + + public interface LatestPostsFragmentInteractionListener extends FragmentInteractionListener { + void onLatestPostsFragmentInteraction(PostSummary postSummary); + } + + /** + * An {@link AsyncTask} that handles asynchronous fetching of a profile page and parsing this + * user's latest posts. + *

LatestPostsTask's {@link AsyncTask#execute execute} method needs a profile's url as String + * parameter!

+ */ + public class LatestPostsTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "LatestPostsTask"; //Separate tag for AsyncTask + + protected void onPreExecute() { + if (!isLoadingMore) progressBar.setVisibility(ProgressBar.VISIBLE); + } + + protected Boolean doInBackground(String... profileUrl) { + Request request = new Request.Builder() + .url(profileUrl[0]) + .build(); + try { + Response response = BaseActivity.getClient().newCall(request).execute(); + return parseLatestPosts(Jsoup.parse(response.body().string())); + } catch (SSLHandshakeException e) { + Report.w(TAG, "Certificate problem (please switch to unsafe connection)."); + } catch (Exception e) { + Report.e("TAG", "ERROR", e); + } + return false; + } + + protected void onPostExecute(Boolean result) { + if (!result) { //Parse failed! + Report.d(TAG, "Parse failed!"); + Toast.makeText(getContext() + , "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show(); + getActivity().finish(); + } + //Parse was successful + progressBar.setVisibility(ProgressBar.INVISIBLE); + latestPostsAdapter.notifyDataSetChanged(); + isLoadingMore = false; + } + + private boolean parseLatestPosts(Document latestPostsPage) { + Elements latestPostsRows = latestPostsPage. + select("td:has(table:Contains(Show Posts)):not([style]) > table"); + if (latestPostsRows.isEmpty()) { + latestPostsRows = latestPostsPage. + select("td:has(table:Contains(Εμφάνιση μηνυμάτων)):not([style]) > table"); + } + //Removes loading item + if (isLoadingMore) { + parsedTopicSummaries.remove(parsedTopicSummaries.size() - 1); + } + + for (Element row : latestPostsRows) { + String pTopicUrl, pTopicTitle, pDateTime, pPost; + if (Integer.parseInt(row.attr("cellpadding")) == 4) { + if (numberOfPages == -1) { + Elements pages = row.select("tr.catbg3 a"); + for (Element page : pages) { + if (Integer.parseInt(page.text()) > numberOfPages) + numberOfPages = Integer.parseInt(page.text()); + } + } + } else { + Elements rowHeader = row.select("td.middletext"); + if (rowHeader.size() != 2) { + return false; + } else { + pTopicTitle = rowHeader.first().text().trim(); + pTopicUrl = rowHeader.first().select("a").last().attr("href"); + pDateTime = rowHeader.last().text(); + } + pPost = ParseHelpers.youtubeEmbeddedFix(row.select("div.post").first()); + + //Add stuff to make it work in WebView + //style.css + pPost = ("" + pPost); + + parsedTopicSummaries.add(new PostSummary(pTopicUrl, pTopicTitle, pDateTime, pPost)); + } + } + return true; + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java new file mode 100644 index 00000000..c75d16bb --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java @@ -0,0 +1,354 @@ +package gr.thmmy.mthmmy.activities.profile.stats; + +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.github.mikephil.charting.charts.HorizontalBarChart; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.formatter.PercentFormatter; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.SSLHandshakeException; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.Request; +import okhttp3.Response; + +public class StatsFragment extends Fragment { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "StatsFragment"; + /** + * The key to use when putting profile's url String to {@link StatsFragment}'s Bundle. + */ + private static final String PROFILE_URL = "PROFILE_DOCUMENT"; + private String profileUrl; + private ProfileStatsTask profileStatsTask; + private LinearLayout mainContent; + private MaterialProgressBar progressBar; + private boolean haveParsed = false; + + private String generalStatisticsTitle = "", generalStatistics = "", postingActivityByTimeTitle = "", mostPopularBoardsByPostsTitle = "", mostPopularBoardsByActivityTitle = ""; + final private List postingActivityByTime = new ArrayList<>(); + final private List mostPopularBoardsByPosts = new ArrayList<>(), mostPopularBoardsByActivity = new ArrayList<>(); + final private ArrayList mostPopularBoardsByPostsLabels = new ArrayList<>(), mostPopularBoardsByActivityLabels = new ArrayList<>(); + + public StatsFragment() { + // Required empty public constructor + } + + /** + * Use ONLY this factory method to create a new instance of this fragment using the provided + * parameters. + * + * @param profileUrl String containing this profile's url + * @return A new instance of fragment Stats. + */ + public static StatsFragment newInstance(String profileUrl) { + StatsFragment fragment = new StatsFragment(); + Bundle args = new Bundle(); + args.putString(PROFILE_URL, profileUrl); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + profileUrl = getArguments().getString(PROFILE_URL); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View rootView = inflater.inflate(R.layout.fragment_stats, container, false); + mainContent = (LinearLayout) rootView.findViewById(R.id.main_content); + progressBar = (MaterialProgressBar) rootView.findViewById(R.id.progressBar); + if (haveParsed) + populateLayout(); + return rootView; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (!haveParsed) { + profileStatsTask = new ProfileStatsTask(); + profileStatsTask.execute(profileUrl + ";sa=statPanel"); + } + Report.d(TAG, "onActivityCreated"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (profileStatsTask != null && profileStatsTask.getStatus() != AsyncTask.Status.RUNNING) + profileStatsTask.cancel(true); + } + + /** + * An {@link AsyncTask} that handles asynchronous parsing of a profile page's data. + * {@link AsyncTask#onPostExecute(Object) OnPostExecute} method calls {@link #()} + * to build graphics. + *

+ *

Calling SummaryTask's {@link AsyncTask#execute execute} method needs to have profile's url + * as String parameter!

+ */ + public class ProfileStatsTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "ProfileStatsTask"; //Separate tag for AsyncTask + + @Override + protected void onPreExecute() { + progressBar.setVisibility(ProgressBar.VISIBLE); + haveParsed = true; + } + + @Override + protected Boolean doInBackground(String... profileUrl) { + Request request = new Request.Builder() + .url(profileUrl[0]) + .build(); + try { + Response response = BaseActivity.getClient().newCall(request).execute(); + return parseStats(Jsoup.parse(response.body().string())); + } catch (SSLHandshakeException e) { + Report.w(TAG, "Certificate problem (please switch to unsafe connection)."); + } catch (Exception e) { + Report.e("TAG", "ERROR", e); + } + return false; + } + + @Override + protected void onPostExecute(Boolean result) { + if (!result) { //Parse failed! + Report.d(TAG, "Parse failed!"); + Toast.makeText(getContext() + , "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show(); + getActivity().finish(); + } + //Parse was successful + progressBar.setVisibility(ProgressBar.INVISIBLE); + populateLayout(); + } + + private boolean parseStats(Document statsPage) { + if (statsPage.select("table.bordercolor[align]>tbody>tr").size() != 6) + return false; + { + Elements titleRows = statsPage.select("table.bordercolor[align]>tbody>tr.titlebg"); + generalStatisticsTitle = titleRows.first().text(); + postingActivityByTimeTitle = titleRows.get(1).text(); + mostPopularBoardsByPostsTitle = titleRows.last().select("td").first().text(); + mostPopularBoardsByActivityTitle = titleRows.last().select("td").last().text(); + } + { + Elements statsRows = statsPage.select("table.bordercolor[align]>tbody>tr:not(.titlebg)"); + { + Elements generalStatisticsRows = statsRows.first().select("tbody>tr"); + for (Element generalStatisticsRow : generalStatisticsRows) + generalStatistics += generalStatisticsRow.text() + "\n"; + generalStatistics = generalStatistics.trim(); + } + { + Elements postingActivityByTimeCols = statsRows.get(1).select(">td").last() + .select("tr").first().select("td[width=4%]"); + int i = -1; + for (Element postingActivityByTimeColumn : postingActivityByTimeCols) { + postingActivityByTime.add(new Entry(++i, Float.parseFloat(postingActivityByTimeColumn + .select("img").first().attr("height")))); + } + } + { + Elements mostPopularBoardsByPostsRows = statsRows.last().select(">td").get(1) + .select(">table>tbody>tr"); + int i = mostPopularBoardsByPostsRows.size(); + for (Element mostPopularBoardsByPostsRow : mostPopularBoardsByPostsRows) { + Elements dataCols = mostPopularBoardsByPostsRow.select("td"); + mostPopularBoardsByPosts.add(new BarEntry(--i, + Integer.parseInt(dataCols.last().text()))); + mostPopularBoardsByPostsLabels.add(dataCols.first().text()); + } + } + { + Elements mostPopularBoardsByActivityRows = statsRows.last().select(">td").last() + .select(">table>tbody>tr"); + int i = mostPopularBoardsByActivityRows.size(); + for (Element mostPopularBoardsByActivityRow : mostPopularBoardsByActivityRows) { + Elements dataCols = mostPopularBoardsByActivityRow.select("td"); + String tmp = dataCols.last().text(); + mostPopularBoardsByActivity.add(new BarEntry(--i, + Float.parseFloat(tmp.substring(0, tmp.indexOf("%"))))); + mostPopularBoardsByActivityLabels.add(dataCols.first().text()); + } + } + } + return true; + } + } + + private void populateLayout() { + ((TextView) mainContent.findViewById(R.id.general_statistics_title)) + .setText(generalStatisticsTitle); + ((TextView) mainContent.findViewById(R.id.general_statistics)) + .setText(generalStatistics); + ((TextView) mainContent.findViewById(R.id.posting_activity_by_time_title)) + .setText(postingActivityByTimeTitle); + + LineChart postingActivityByTimeChart = (LineChart) mainContent + .findViewById(R.id.posting_activity_by_time_chart); + postingActivityByTimeChart.setDescription(null); + postingActivityByTimeChart.getLegend().setEnabled(false); + postingActivityByTimeChart.setScaleYEnabled(false); + postingActivityByTimeChart.setDrawBorders(true); + postingActivityByTimeChart.getAxisLeft().setEnabled(false); + postingActivityByTimeChart.getAxisRight().setEnabled(false); + XAxis postingActivityByTimeChartXAxis = postingActivityByTimeChart.getXAxis(); + postingActivityByTimeChartXAxis.setTextColor(Color.WHITE); + postingActivityByTimeChartXAxis.setPosition(XAxis.XAxisPosition.BOTTOM); + postingActivityByTimeChartXAxis.setDrawLabels(true); + postingActivityByTimeChartXAxis.setLabelCount(24, false); + postingActivityByTimeChartXAxis.setGranularity(1f); + + LineDataSet postingActivityByTimeDataSet = new LineDataSet(postingActivityByTime, null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + postingActivityByTimeDataSet.setFillDrawable(getResources().getDrawable(R.drawable.line_chart_gradient, null)); + } else + //noinspection deprecation + postingActivityByTimeDataSet.setFillDrawable(getResources().getDrawable(R.drawable.line_chart_gradient)); + postingActivityByTimeDataSet.setDrawFilled(true); + postingActivityByTimeDataSet.setDrawCircles(false); + postingActivityByTimeDataSet.setDrawValues(false); + LineData postingActivityByTimeData = new LineData(postingActivityByTimeDataSet); + postingActivityByTimeChart.setData(postingActivityByTimeData); + postingActivityByTimeChart.invalidate(); + + ((TextView) mainContent.findViewById(R.id.most_popular_boards_by_posts_title)) + .setText(mostPopularBoardsByPostsTitle); + + HorizontalBarChart mostPopularBoardsByPostsChart = (HorizontalBarChart) mainContent. + findViewById(R.id.most_popular_boards_by_posts_chart); + mostPopularBoardsByPostsChart.setDescription(null); + mostPopularBoardsByPostsChart.getLegend().setEnabled(false); + mostPopularBoardsByPostsChart.setScaleEnabled(false); + mostPopularBoardsByPostsChart.setDrawBorders(true); + mostPopularBoardsByPostsChart.getAxisLeft().setEnabled(false); + + XAxis mostPopularBoardsByPostsChartXAxis = mostPopularBoardsByPostsChart.getXAxis(); + mostPopularBoardsByPostsChartXAxis.setPosition(XAxis.XAxisPosition.TOP_INSIDE); + mostPopularBoardsByPostsChartXAxis.setTextColor(Color.WHITE); + mostPopularBoardsByPostsChartXAxis.setLabelCount(mostPopularBoardsByPostsLabels.size()); + mostPopularBoardsByPostsChartXAxis.setValueFormatter(new MyXAxisValueFormatter(mostPopularBoardsByPostsLabels)); + + YAxis mostPopularBoardsByPostsChartYAxis = mostPopularBoardsByPostsChart.getAxisRight(); + mostPopularBoardsByPostsChartYAxis.setTextColor(Color.WHITE); + mostPopularBoardsByPostsChartYAxis.setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART); + mostPopularBoardsByPostsChartYAxis.setDrawLabels(true); + mostPopularBoardsByPostsChartYAxis.setLabelCount(10, false); + mostPopularBoardsByPostsChartYAxis.setGranularity(1f); + + BarDataSet mostPopularBoardsByPostsDataSet = new BarDataSet(mostPopularBoardsByPosts, null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mostPopularBoardsByPostsDataSet.setColors(getResources().getColor(R.color.accent, null)); + } else + //noinspection deprecation + mostPopularBoardsByPostsDataSet.setColors(getResources().getColor(R.color.accent)); + mostPopularBoardsByPostsDataSet.setDrawValues(false); + mostPopularBoardsByPostsDataSet.setValueTextColor(Color.WHITE); + + BarData mostPopularBoardsByPostsData = new BarData(mostPopularBoardsByPostsDataSet); + mostPopularBoardsByPostsData.setDrawValues(false); + mostPopularBoardsByPostsData.setValueTextColor(Color.WHITE); + mostPopularBoardsByPostsChart.setData(mostPopularBoardsByPostsData); + mostPopularBoardsByPostsChart.invalidate(); + + ((TextView) mainContent.findViewById(R.id.most_popular_boards_by_activity_title)) + .setText(mostPopularBoardsByActivityTitle); + + HorizontalBarChart mostPopularBoardsByActivityChart = (HorizontalBarChart) mainContent. + findViewById(R.id.most_popular_boards_by_activity_chart); + mostPopularBoardsByActivityChart.setDescription(null); + mostPopularBoardsByActivityChart.getLegend().setEnabled(false); + mostPopularBoardsByActivityChart.setScaleEnabled(false); + mostPopularBoardsByActivityChart.setDrawBorders(true); + mostPopularBoardsByActivityChart.getAxisLeft().setEnabled(false); + + XAxis mostPopularBoardsByActivityChartXAxis = mostPopularBoardsByActivityChart.getXAxis(); + mostPopularBoardsByActivityChartXAxis.setPosition(XAxis.XAxisPosition.TOP_INSIDE); + mostPopularBoardsByActivityChartXAxis.setTextColor(Color.WHITE); + mostPopularBoardsByActivityChartXAxis.setLabelCount(mostPopularBoardsByActivity.size()); + mostPopularBoardsByActivityChartXAxis.setValueFormatter(new MyXAxisValueFormatter(mostPopularBoardsByActivityLabels)); + + YAxis mostPopularBoardsByActivityChartYAxis = mostPopularBoardsByActivityChart.getAxisRight(); + mostPopularBoardsByActivityChartYAxis.setValueFormatter(new PercentFormatter()); + mostPopularBoardsByActivityChartYAxis.setTextColor(Color.WHITE); + mostPopularBoardsByActivityChartYAxis.setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART); + mostPopularBoardsByActivityChartYAxis.setDrawLabels(true); + mostPopularBoardsByActivityChartYAxis.setLabelCount(10, false); + + BarDataSet mostPopularBoardsByActivityDataSet = new BarDataSet(mostPopularBoardsByActivity, null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mostPopularBoardsByActivityDataSet.setColors(getResources().getColor(R.color.accent, null)); + } else + //noinspection deprecation + mostPopularBoardsByActivityDataSet.setColors(getResources().getColor(R.color.accent)); + mostPopularBoardsByActivityDataSet.setDrawValues(false); + mostPopularBoardsByActivityDataSet.setValueTextColor(Color.WHITE); + + BarData mostPopularBoardsByActivityData = new BarData(mostPopularBoardsByActivityDataSet); + mostPopularBoardsByActivityData.setDrawValues(false); + mostPopularBoardsByActivityData.setValueTextColor(Color.WHITE); + mostPopularBoardsByActivityChart.setData(mostPopularBoardsByActivityData); + mostPopularBoardsByActivityChart.invalidate(); + } + + class MyXAxisValueFormatter implements IAxisValueFormatter { + private final ArrayList mValues; + + MyXAxisValueFormatter(ArrayList values) { + this.mValues = values; + } + + @Override + public String getFormattedValue(float value, AxisBase axis) { + return mValues.get((int) value); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java new file mode 100644 index 00000000..c621f295 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java @@ -0,0 +1,206 @@ +package gr.thmmy.mthmmy.activities.profile.summary; + +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; +import java.util.Objects; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.utils.ParseHelpers; +import mthmmy.utils.Report; + + +/** + * Use the {@link SummaryFragment#newInstance} factory method to create an instance of this fragment. + */ +public class SummaryFragment extends Fragment { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "SummaryFragment"; + /** + * The key to use when putting profile's source code String to {@link SummaryFragment}'s Bundle. + */ + private static final String PROFILE_DOCUMENT = "PROFILE_DOCUMENT"; + /** + * {@link ArrayList} of Strings used to hold profile's information. Data are added in + * {@link SummaryTask}. + */ + private ArrayList parsedProfileSummaryData; + /** + * A {@link Document} holding this profile's source code. + */ + private Document profileSummaryDocument; + private SummaryTask summaryTask; + private LinearLayout mainContent; + + public SummaryFragment() { + // Required empty public constructor + } + + /** + * Use ONLY this factory method to create a new instance of this fragment using the provided + * parameters. + * + * @param profileSummaryDocument a {@link Document} containing this profile's parsed page + * @return A new instance of fragment Summary. + */ + public static SummaryFragment newInstance(Document profileSummaryDocument) { + SummaryFragment fragment = new SummaryFragment(); + Bundle args = new Bundle(); + args.putString(PROFILE_DOCUMENT, profileSummaryDocument.toString()); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + profileSummaryDocument = Jsoup.parse(getArguments().getString(PROFILE_DOCUMENT)); + parsedProfileSummaryData = new ArrayList<>(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View rootView = inflater.inflate(R.layout.fragment_summary, container, false); + mainContent = (LinearLayout) rootView.findViewById(R.id.profile_activity_content); + if (!parsedProfileSummaryData.isEmpty()) + populateLayout(); + return rootView; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (parsedProfileSummaryData.isEmpty()) { + summaryTask = new SummaryTask(); + summaryTask.execute(profileSummaryDocument); + } + Report.d(TAG, "onActivityCreated"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (summaryTask != null && summaryTask.getStatus() != AsyncTask.Status.RUNNING) + summaryTask.cancel(true); + } + + /** + * An {@link AsyncTask} that handles asynchronous parsing of a profile page's data. + * {@link AsyncTask#onPostExecute(Object) OnPostExecute} method calls {@link #populateLayout()} + * to build graphics. + *

+ *

Calling SummaryTask's {@link AsyncTask#execute execute} method needs to have profile's url + * as String parameter!

+ */ + public class SummaryTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "SummaryTask"; //Separate tag for AsyncTask + + protected Void doInBackground(Document... profileSummaryPage) { + parsedProfileSummaryData = parseProfileSummary(profileSummaryPage[0]); + return null; + } + + protected void onPostExecute(Void result) { + populateLayout(); + } + + /** + * This method is used to parse all available information in a user profile. + * + * @param profile {@link Document} object containing this profile's source code + * @return ArrayList containing this profile's parsed information + * @see org.jsoup.Jsoup Jsoup + */ + ArrayList parseProfileSummary(Document profile) { + //Method's variables + ArrayList parsedInformation = new ArrayList<>(); + + //Contains all summary's rows + Elements summaryRows = profile.select(".bordercolor > tbody:nth-child(1) > tr:nth-child(2) tr"); + + for (Element summaryRow : summaryRows) { + String rowText = summaryRow.text(), pHtml = ""; + + if (summaryRow.select("td").size() == 1) //Horizontal rule rows + pHtml = ""; + else if (rowText.contains("Signature") || rowText.contains("Υπογραφή")) { + //This needs special handling since it may have css + pHtml = ParseHelpers.youtubeEmbeddedFix(summaryRow); + //Add stuff to make it work in WebView + //style.css + pHtml = ("\n" + + "
\n" + pHtml + "\n
"); + } else if (!rowText.contains("Name") && !rowText.contains("Όνομα")) { //Doesn't add username twice + if (Objects.equals(summaryRow.select("td").get(1).text(), "")) + continue; + //Style parsed information with html + pHtml = "" + summaryRow.select("td").first().text() + " " + + summaryRow.select("td").get(1).text(); + } + parsedInformation.add(pHtml); + } + return parsedInformation; + } + } + + /** + * Simple method that builds the UI of a {@link SummaryFragment}. + *

Use this method only after parsing profile's data with + * {@link gr.thmmy.mthmmy.activities.profile.ProfileActivity.ProfileTask} as it reads from + * {@link #parsedProfileSummaryData}

+ */ + private void populateLayout() { + for (String profileSummaryRow : parsedProfileSummaryData) { + if (profileSummaryRow.contains("Signature") + || profileSummaryRow.contains("Υπογραφή")) { //This may contain css + WebView signatureEntry = new WebView(this.getContext()); + signatureEntry.setBackgroundColor(Color.argb(1, 255, 255, 255)); + signatureEntry.loadDataWithBaseURL("file:///android_asset/", profileSummaryRow, + "text/html", "UTF-8", null); + mainContent.addView(signatureEntry); + continue; + } + TextView entry = new TextView(this.getContext()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + entry.setTextColor(getResources().getColor(R.color.primary_text, null)); + } else { + //noinspection deprecation + entry.setTextColor(getResources().getColor(R.color.primary_text)); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + entry.setText(Html.fromHtml(profileSummaryRow, Html.FROM_HTML_MODE_LEGACY)); + } else { + //noinspection deprecation + entry.setText(Html.fromHtml(profileSummaryRow)); + } + + mainContent.addView(entry); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java new file mode 100644 index 00000000..c7ca6b73 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java @@ -0,0 +1,489 @@ +package gr.thmmy.mthmmy.activities.topic; + +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Objects; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.LoginActivity; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import gr.thmmy.mthmmy.model.Post; +import gr.thmmy.mthmmy.utils.ParseHelpers; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Activity for topics. When creating an Intent of this activity you need to bundle a String + * containing this topics's url using the key {@link #BUNDLE_TOPIC_URL} and a String containing + * this topic's title using the key {@link #BUNDLE_TOPIC_TITLE}. + */ +@SuppressWarnings("unchecked") +public class TopicActivity extends BaseActivity { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "TopicActivity"; + /** + * The key to use when putting topic's url String to {@link TopicActivity}'s Bundle. + */ + public static final String BUNDLE_TOPIC_URL = "TOPIC_URL"; + /** + * The key to use when putting topic's title String to {@link TopicActivity}'s Bundle. + */ + public static final String BUNDLE_TOPIC_TITLE = "TOPIC_TITLE"; + private static TopicTask topicTask; + //About posts + private TopicAdapter topicAdapter; + private ArrayList postsList; + private static final int NO_POST_FOCUS = -1; + private static int postFocus = NO_POST_FOCUS; + private static int postFocusPosition = 0; + //Quotes + public static final ArrayList toQuoteList = new ArrayList<>(); + //Topic's pages + private int thisPage = 1; + public static String base_url = ""; + private int numberOfPages = 1; + private final SparseArray pagesUrls = new SparseArray<>(); + //Page select + private final Handler repeatUpdateHandler = new Handler(); + private final long INITIAL_DELAY = 500; + private boolean autoIncrement = false; + private boolean autoDecrement = false; + private static final int SMALL_STEP = 1; + private static final int LARGE_STEP = 10; + private Integer pageRequestValue; + //Bottom navigation graphics + private ImageButton firstPage; + private ImageButton previousPage; + private TextView pageIndicator; + private ImageButton nextPage; + private ImageButton lastPage; + //Other variables + private MaterialProgressBar progressBar; + private String topicTitle; + private FloatingActionButton replyFAB; + private String parsedTitle; + private RecyclerView recyclerView; + private String loadedPageUrl = ""; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_topic); + + Bundle extras = getIntent().getExtras(); + topicTitle = extras.getString("TOPIC_TITLE"); + + //Initializes graphics + toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar.setTitle(topicTitle); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + createDrawer(); + + progressBar = (MaterialProgressBar) findViewById(R.id.progressBar); + + postsList = new ArrayList<>(); + + recyclerView = (RecyclerView) findViewById(R.id.topic_recycler_view); + recyclerView.setHasFixedSize(true); + LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); + recyclerView.setLayoutManager(layoutManager); + topicAdapter = new TopicAdapter(getApplicationContext(), progressBar, postsList, + topicTask); + recyclerView.setAdapter(topicAdapter); + + replyFAB = (FloatingActionButton) findViewById(R.id.topic_fab); + replyFAB.setEnabled(false); + if (!sessionManager.isLoggedIn()) replyFAB.hide(); + else { + replyFAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (sessionManager.isLoggedIn()) { + //TODO Reply + } else { + new AlertDialog.Builder(TopicActivity.this) + .setMessage("You need to be logged in to reply!") + .setPositiveButton("Login", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + Intent intent = new Intent(TopicActivity.this, LoginActivity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.push_right_in, R.anim.push_right_out); + } + }) + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + } + }) + .show(); + } + } + }); + } + + //Sets bottom navigation bar + firstPage = (ImageButton) findViewById(R.id.page_first_button); + previousPage = (ImageButton) findViewById(R.id.page_previous_button); + pageIndicator = (TextView) findViewById(R.id.page_indicator); + nextPage = (ImageButton) findViewById(R.id.page_next_button); + lastPage = (ImageButton) findViewById(R.id.page_last_button); + + initDecrementButton(firstPage, LARGE_STEP); + initDecrementButton(previousPage, SMALL_STEP); + initIncrementButton(nextPage, SMALL_STEP); + initIncrementButton(lastPage, LARGE_STEP); + + firstPage.setEnabled(false); + previousPage.setEnabled(false); + nextPage.setEnabled(false); + lastPage.setEnabled(false); + + //Gets posts + topicTask = new TopicTask(); + topicTask.execute(extras.getString(BUNDLE_TOPIC_URL)); //Attempt data parsing + } + + @Override + public void onBackPressed() { + if (drawer.isDrawerOpen()) { + drawer.closeDrawer(); + return; + } + super.onBackPressed(); + } + + @Override + protected void onResume() { + drawer.setSelection(-1); + super.onResume(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + recyclerView.setAdapter(null); + if (topicTask != null && topicTask.getStatus() != AsyncTask.Status.RUNNING) + topicTask.cancel(true); + } + + //--------------------------------------BOTTOM NAV BAR METHODS-------------------------------------- + private void initIncrementButton(ImageButton increment, final int step) { + // Increment once for a click + increment.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + if (!autoIncrement && step == LARGE_STEP) { //If just clicked go to last page + changePage(numberOfPages - 1); + return; + } + //Clicked and holden + autoIncrement = false; //Stop incrementing + incrementPageRequestValue(step); + changePage(pageRequestValue - 1); + } + }); + + // Auto increment for a long click + increment.setOnLongClickListener( + new View.OnLongClickListener() { + public boolean onLongClick(View arg0) { + autoIncrement = true; + repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), INITIAL_DELAY); + return false; + } + } + ); + + // When the button is released + increment.setOnTouchListener(new View.OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP && autoIncrement) { + changePage(pageRequestValue - 1); + } + return false; + } + }); + } + + private void initDecrementButton(ImageButton decrement, final int step) { + // Decrement once for a click + decrement.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + if (!autoDecrement && step == LARGE_STEP) { //If just clicked go to first page + changePage(0); + return; + } + //Clicked and hold + autoDecrement = false; //Stop decrementing + decrementPageRequestValue(step); + changePage(pageRequestValue - 1); + } + }); + + + // Auto decrement for a long click + decrement.setOnLongClickListener( + new View.OnLongClickListener() { + public boolean onLongClick(View arg0) { + autoDecrement = true; + repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), INITIAL_DELAY); + return false; + } + } + ); + + // When the button is released + decrement.setOnTouchListener(new View.OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP && autoDecrement) { + changePage(pageRequestValue - 1); + } + return false; + } + }); + } + + private void incrementPageRequestValue(int step) { + if (pageRequestValue < numberOfPages - step) { + pageRequestValue = pageRequestValue + step; + } else + pageRequestValue = numberOfPages; + pageIndicator.setText(pageRequestValue + "/" + String.valueOf(numberOfPages)); + } + + private void decrementPageRequestValue(int step) { + if (pageRequestValue > step) + pageRequestValue = pageRequestValue - step; + else + pageRequestValue = 1; + pageIndicator.setText(pageRequestValue + "/" + String.valueOf(numberOfPages)); + } + + private void changePage(int pageRequested) { + if (pageRequested != thisPage - 1) { + if (topicTask != null && topicTask.getStatus() != AsyncTask.Status.RUNNING) + topicTask.cancel(true); + + topicTask = new TopicTask(); + topicTask.execute(pagesUrls.get(pageRequested)); //Attempt data parsing + + } + } +//------------------------------------BOTTOM NAV BAR METHODS END------------------------------------ + + /** + * An {@link AsyncTask} that handles asynchronous fetching of a topic page and parsing it's + * data. {@link AsyncTask#onPostExecute(Object) OnPostExecute} method calls {@link RecyclerView#swapAdapter} + * to build graphics. + *

+ *

Calling TopicTask's {@link AsyncTask#execute execute} method needs to have profile's url + * as String parameter!

+ */ + class TopicTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + private static final String TAG = "TopicTask"; //Separate tag for AsyncTask + private static final int SUCCESS = 0; + private static final int NETWORK_ERROR = 1; + private static final int OTHER_ERROR = 2; + private static final int SAME_PAGE = 3; + + protected void onPreExecute() { + progressBar.setVisibility(ProgressBar.VISIBLE); + paginationEnable(false); + if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(false); + } + + protected Integer doInBackground(String... strings) { + Document document; + base_url = strings[0].substring(0, strings[0].lastIndexOf(".")); //This topic's base url + String newPageUrl = strings[0]; + + //Finds the index of message focus if present + { + postFocus = NO_POST_FOCUS; + if (newPageUrl.contains("msg")) { + String tmp = newPageUrl.substring(newPageUrl.indexOf("msg") + 3); + if (tmp.contains(";")) + postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf(";"))); + else + postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf("#"))); + } + } + + //Checks if the page to be loaded is the one already shown + if (!Objects.equals(loadedPageUrl, "") && !loadedPageUrl.contains(base_url)) { + if (newPageUrl.contains("topicseen#new")) + if (Integer.parseInt(loadedPageUrl.substring(base_url.length())) == numberOfPages) + return SAME_PAGE; + if (Objects.equals(loadedPageUrl.substring(base_url.length()) + , newPageUrl.substring(base_url.length()))) + return SAME_PAGE; + } + + loadedPageUrl = newPageUrl; + Request request = new Request.Builder() + .url(newPageUrl) + .build(); + try { + Response response = client.newCall(request).execute(); + document = Jsoup.parse(response.body().string()); + parse(document); + return SUCCESS; + } catch (IOException e) { + Report.i(TAG, "IO Exception", e); + return NETWORK_ERROR; + } catch (Exception e) { + Report.e(TAG, "Exception", e); + return OTHER_ERROR; + } + } + + protected void onPostExecute(Integer parseResult) { + //Finds the position of the focused message if present + for (int i = 0; i < postsList.size(); ++i) { + if (postsList.get(i).getPostIndex() == postFocus) { + postFocusPosition = i; + break; + } + } + + switch (parseResult) { + case SUCCESS: + progressBar.setVisibility(ProgressBar.INVISIBLE); + topicAdapter.customNotifyDataSetChanged(new TopicTask()); + if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(true); + + //Set current page + pageIndicator.setText(String.valueOf(thisPage) + "/" + String.valueOf(numberOfPages)); + pageRequestValue = thisPage; + + paginationEnable(true); + + if (topicTitle == null || Objects.equals(topicTitle, "")) + toolbar.setTitle(parsedTitle); + break; + case NETWORK_ERROR: + Toast.makeText(getBaseContext(), "Network Error", Toast.LENGTH_SHORT).show(); + break; + case SAME_PAGE: + //TODO change focus + break; + default: + //Parse failed - should never happen + Report.d(TAG, "Parse failed!"); + Toast.makeText(getBaseContext(), "Fatal Error", Toast.LENGTH_SHORT).show(); + finish(); + break; + } + } + + /** + * All the parsing a topic needs. + * + * @param topic {@link Document} object containing this topic's source code + * @see org.jsoup.Jsoup Jsoup + */ + private void parse(Document topic) { + ParseHelpers.Language language = ParseHelpers.Language.getLanguage(topic); + + //Finds topic title if missing + if (topicTitle == null || Objects.equals(topicTitle, "")) { + parsedTitle = topic.select("td[id=top_subject]").first().text(); + if (parsedTitle.contains("Topic:")) { + parsedTitle = parsedTitle.substring(parsedTitle.indexOf("Topic:") + 7 + , parsedTitle.indexOf("(Read") - 2); + } else { + parsedTitle = parsedTitle.substring(parsedTitle.indexOf("Θέμα:") + 6 + , parsedTitle.indexOf("(Αναγνώστηκε") - 2); + Report.d(TAG, parsedTitle); + } + } + + { //Finds current page's index + thisPage = TopicParser.parseCurrentPageIndex(topic, language); + } + { //Finds number of pages + numberOfPages = TopicParser.parseTopicNumberOfPages(topic, thisPage, language); + + for (int i = 0; i < numberOfPages; i++) { + //Generate each page's url from topic's base url +".15*numberOfPage" + pagesUrls.put(i, base_url + "." + String.valueOf(i * 15)); + } + } + + postsList.clear(); + postsList.addAll(TopicParser.parseTopic(topic, language)); + //postsList = TopicParser.parseTopic(topic, language); + } + } + + /** + * This class is used to implement the repetitive incrementPageRequestValue/decrementPageRequestValue + * of page value when long pressing one of the page navigation buttons. + */ + class RepetitiveUpdater implements Runnable { + private final int step; + + /** + * @param step number of pages to add/subtract on each repetition + */ + RepetitiveUpdater(int step) { + this.step = step; + } + + public void run() { + long REPEAT_DELAY = 250; + if (autoIncrement) { + incrementPageRequestValue(step); + repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), REPEAT_DELAY); + } else if (autoDecrement) { + decrementPageRequestValue(step); + repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), REPEAT_DELAY); + } + } + } + + private void paginationEnable(boolean enabled) { + firstPage.setEnabled(enabled); + previousPage.setEnabled(enabled); + nextPage.setEnabled(enabled); + lastPage.setEnabled(enabled); + } +} \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java new file mode 100644 index 00000000..7c69d9ef --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java @@ -0,0 +1,663 @@ +package gr.thmmy.mthmmy.activities.topic; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.support.annotation.NonNull; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v7.widget.CardView; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.MimeTypeMap; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.squareup.picasso.Picasso; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.board.BoardActivity; +import gr.thmmy.mthmmy.activities.profile.ProfileActivity; +import gr.thmmy.mthmmy.model.LinkTarget; +import gr.thmmy.mthmmy.model.Post; +import gr.thmmy.mthmmy.utils.CircleTransform; +import gr.thmmy.mthmmy.utils.FileManager.ThmmyFile; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_TITLE; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_URL; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_URL; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_THUMBNAIL_URL; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_USERNAME; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.base_url; +import static gr.thmmy.mthmmy.activities.topic.TopicActivity.toQuoteList; + +/** + * Custom {@link android.support.v7.widget.RecyclerView.Adapter} used for topics. + */ +class TopicAdapter extends RecyclerView.Adapter { + /** + * Debug Tag for logging debug output to LogCat + */ + private static final String TAG = "TopicAdapter"; + /** + * Int that holds thumbnail's size defined in R.dimen + */ + private static int THUMBNAIL_SIZE; + private final Context context; + private final List postsList; + /** + * Used to hold the state of visibility and other attributes for views that are animated or + * otherwise changed. Used in combination with {@link #isPostDateAndNumberVisibile}, + * {@link #isUserExtraInfoVisibile} and {@link #isQuoteButtonChecked}. + */ + private final ArrayList viewProperties = new ArrayList<>(); + /** + * Index of state indicator in the boolean array. If true post is expanded and post's date and + * number are visible. + */ + private static final int isPostDateAndNumberVisibile = 0; + /** + * Index of state indicator in the boolean array. If true user's extra info are expanded and + * visible. + */ + private static final int isUserExtraInfoVisibile = 1; + /** + * Index of state indicator in the boolean array. If true quote button for this post is checked. + */ + private static final int isQuoteButtonChecked = 2; + private final MaterialProgressBar progressBar; + private DownloadTask downloadTask; + private TopicActivity.TopicTask topicTask; + + /** + * Custom {@link RecyclerView.ViewHolder} implementation + */ + class MyViewHolder extends RecyclerView.ViewHolder { + final CardView cardView; + final LinearLayout cardChildLinear; + final FrameLayout postDateAndNumberExp; + final TextView postDate, postNum, username, subject; + final ImageView thumbnail; + final public WebView post; + final ImageButton quoteToggle; + final RelativeLayout header; + final LinearLayout userExtraInfo; + final View bodyFooterDivider; + final LinearLayout postFooter; + + final TextView specialRank, rank, gender, numberOfPosts, personalText, stars; + + MyViewHolder(View view) { + super(view); + //Initializes layout's graphic elements + //Standard stuff + cardView = (CardView) view.findViewById(R.id.card_view); + cardChildLinear = (LinearLayout) view.findViewById(R.id.card_child_linear); + postDateAndNumberExp = (FrameLayout) view.findViewById(R.id.post_date_and_number_exp); + postDate = (TextView) view.findViewById(R.id.post_date); + postNum = (TextView) view.findViewById(R.id.post_number); + thumbnail = (ImageView) view.findViewById(R.id.thumbnail); + username = (TextView) view.findViewById(R.id.username); + subject = (TextView) view.findViewById(R.id.subject); + post = (WebView) view.findViewById(R.id.post); + post.setBackgroundColor(Color.argb(1, 255, 255, 255)); + quoteToggle = (ImageButton) view.findViewById(R.id.toggle_quote_button); + bodyFooterDivider = view.findViewById(R.id.body_footer_divider); + postFooter = (LinearLayout) view.findViewById(R.id.post_footer); + + //User's extra info + header = (RelativeLayout) view.findViewById(R.id.header); + userExtraInfo = (LinearLayout) view.findViewById(R.id.user_extra_info); + specialRank = (TextView) view.findViewById(R.id.special_rank); + rank = (TextView) view.findViewById(R.id.rank); + gender = (TextView) view.findViewById(R.id.gender); + numberOfPosts = (TextView) view.findViewById(R.id.number_of_posts); + personalText = (TextView) view.findViewById(R.id.personal_text); + stars = (TextView) view.findViewById(R.id.stars); + } + + /** + * Cancels all pending Picasso requests + */ + void cleanup() { + Picasso.with(context).cancelRequest(thumbnail); + thumbnail.setImageDrawable(null); + } + } + + /** + * @param context the context of the {@link RecyclerView} + * @param postsList List of {@link Post} objects to use + */ + TopicAdapter(Context context, MaterialProgressBar progressBar, List postsList, + TopicActivity.TopicTask topicTask) { + this.context = context; + this.postsList = postsList; + + THUMBNAIL_SIZE = (int) context.getResources().getDimension(R.dimen.thumbnail_size); + for (int i = 0; i < postsList.size(); ++i) { + //Initializes properties, array's values will be false by default + viewProperties.add(new boolean[3]); + } + this.progressBar = progressBar; + downloadTask = new DownloadTask(); + this.topicTask = topicTask; + } + + @Override + public void onViewRecycled(final MyViewHolder holder) { + holder.cleanup(); + } + + @Override + public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.activity_topic_post_row, parent, false); + return new MyViewHolder(itemView); + } + + @SuppressLint("SetJavaScriptEnabled") + @Override + public void onBindViewHolder(final MyViewHolder holder, final int position) { + final Post currentPost = postsList.get(position); + + //Post's WebView parameters + holder.post.setClickable(true); + holder.post.setWebViewClient(new LinkLauncher()); + + //Avoids errors about layout having 0 width/height + holder.thumbnail.setMinimumWidth(1); + holder.thumbnail.setMinimumHeight(1); + //Sets thumbnail size + holder.thumbnail.setMaxWidth(THUMBNAIL_SIZE); + holder.thumbnail.setMaxHeight(THUMBNAIL_SIZE); + + //noinspection ConstantConditions + Picasso.with(context) + .load(currentPost.getThumbnailUrl()) + .resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE) + .centerCrop() + .error(ResourcesCompat.getDrawable(context.getResources() + , R.drawable.ic_default_user_thumbnail, null)) + .placeholder(ResourcesCompat.getDrawable(context.getResources() + , R.drawable.ic_default_user_thumbnail, null)) + .transform(new CircleTransform()) + .into(holder.thumbnail); + + //Sets username,submit date, index number, subject, post's and attached files texts + holder.username.setText(currentPost.getAuthor()); + holder.postDate.setText(currentPost.getPostDate()); + if (currentPost.getPostNumber() != 0) + holder.postNum.setText(context.getString( + R.string.user_number_of_posts, currentPost.getPostNumber())); + else + holder.postNum.setText(""); + holder.subject.setText(currentPost.getSubject()); + holder.post.loadDataWithBaseURL("file:///android_asset/", currentPost.getContent(), "text/html", "UTF-8", null); + if (currentPost.getAttachedFiles() != null && currentPost.getAttachedFiles().size() != 0) { + holder.bodyFooterDivider.setVisibility(View.VISIBLE); + int filesTextColor; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + filesTextColor = context.getResources().getColor(R.color.accent, null); + } else //noinspection deprecation + filesTextColor = context.getResources().getColor(R.color.accent); + + for (final ThmmyFile attachedFile : currentPost.getAttachedFiles()) { + final TextView attached = new TextView(context); + attached.setTextSize(10f); + attached.setClickable(true); + attached.setTypeface(Typeface.createFromAsset(context.getAssets() + , "fonts/fontawesome-webfont.ttf")); + attached.setText(faIconFromFilename(attachedFile.getFilename()) + " " + + attachedFile.getFilename() + attachedFile.getFileInfo()); + attached.setTextColor(filesTextColor); + attached.setPadding(0, 3, 0, 3); + + attached.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + downloadTask = new DownloadTask(); + downloadTask.execute(attachedFile); + } + }); + + holder.postFooter.addView(attached); + } + } else { + holder.bodyFooterDivider.setVisibility(View.GONE); + holder.postFooter.removeAllViews(); + } + + if (!currentPost.isDeleted()) { //Sets user's extra info + String mSpecialRank = currentPost.getSpecialRank(), mRank = currentPost.getRank(), mGender = currentPost.getGender(), mNumberOfPosts = currentPost.getNumberOfPosts(), mPersonalText = currentPost.getPersonalText(); + int mNumberOfStars = currentPost.getNumberOfStars(), mUserColor = currentPost.getUserColor(); + + if (!Objects.equals(mSpecialRank, "") && mSpecialRank != null) { + holder.specialRank.setText(mSpecialRank); + holder.specialRank.setVisibility(View.VISIBLE); + } else + holder.specialRank.setVisibility(View.GONE); + if (!Objects.equals(mRank, "") && mRank != null) { + holder.rank.setText(mRank); + holder.rank.setVisibility(View.VISIBLE); + } else + holder.rank.setVisibility(View.GONE); + if (!Objects.equals(mGender, "") && mGender != null) { + holder.gender.setText(mGender); + holder.gender.setVisibility(View.VISIBLE); + } else + holder.gender.setVisibility(View.GONE); + if (!Objects.equals(mNumberOfPosts, "") && mNumberOfPosts != null) { + holder.numberOfPosts.setText(mNumberOfPosts); + holder.numberOfPosts.setVisibility(View.VISIBLE); + } else + holder.numberOfPosts.setVisibility(View.GONE); + if (!Objects.equals(mPersonalText, "") && mPersonalText != null) { + holder.personalText.setText("\"" + mPersonalText + "\""); + holder.personalText.setVisibility(View.VISIBLE); + } else + holder.personalText.setVisibility(View.GONE); + if (mNumberOfStars > 0) { + holder.stars.setTypeface(Typeface.createFromAsset(context.getAssets() + , "fonts/fontawesome-webfont.ttf")); + + String aStar = context.getResources().getString(R.string.fa_icon_star); + String usersStars = ""; + for (int i = 0; i < mNumberOfStars; ++i) { + usersStars += aStar; + } + holder.stars.setText(usersStars); + holder.stars.setTextColor(mUserColor); + holder.stars.setVisibility(View.VISIBLE); + } else + holder.stars.setVisibility(View.GONE); + //Special card for special member of the month! + if (mUserColor == TopicParser.USER_COLOR_PINK) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + holder.cardChildLinear.setBackground(context.getResources(). + getDrawable(R.drawable.member_of_the_month_card, null)); + } else //noinspection deprecation + holder.cardChildLinear.setBackground(context.getResources(). + getDrawable(R.drawable.member_of_the_month_card)); + } else holder.cardChildLinear.setBackground(null); + + //Avoid's view's visibility recycling + if (viewProperties.get(position)[isUserExtraInfoVisibile]) { + holder.userExtraInfo.setVisibility(View.VISIBLE); + holder.userExtraInfo.setAlpha(1.0f); + } else { + holder.userExtraInfo.setVisibility(View.GONE); + holder.userExtraInfo.setAlpha(0.0f); + } + //Sets graphics behavior + holder.header.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //Clicking an expanded header starts profile activity + if (viewProperties.get(holder.getAdapterPosition())[isUserExtraInfoVisibile]) { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_PROFILE_URL, currentPost.getProfileURL()); + if (currentPost.getThumbnailUrl() == null) + extras.putString(BUNDLE_THUMBNAIL_URL, ""); + else + extras.putString(BUNDLE_THUMBNAIL_URL, currentPost.getThumbnailUrl()); + extras.putString(BUNDLE_USERNAME, currentPost.getAuthor()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); + tmp[isUserExtraInfoVisibile] = !tmp[isUserExtraInfoVisibile]; + viewProperties.set(holder.getAdapterPosition(), tmp); + TopicAnimations.animateUserExtraInfoVisibility(holder.userExtraInfo); + } + }); + //Clicking the expanded part of a header (the extra info) makes it collapse + holder.userExtraInfo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); + tmp[1] = false; + viewProperties.set(holder.getAdapterPosition(), tmp); + + TopicAnimations.animateUserExtraInfoVisibility(v); + } + }); + }//End of deleted profiles + //Avoid's view's visibility recycling + if (viewProperties.get(position)[isPostDateAndNumberVisibile]) { //Expanded + holder.postDateAndNumberExp.setVisibility(View.VISIBLE); + holder.postDateAndNumberExp.setAlpha(1.0f); + holder.postDateAndNumberExp.setTranslationY(0); + + holder.username.setMaxLines(Integer.MAX_VALUE); + holder.username.setEllipsize(null); + + holder.subject.setTextColor(Color.parseColor("#FFFFFF")); + holder.subject.setMaxLines(Integer.MAX_VALUE); + holder.subject.setEllipsize(null); + } else { //Collapsed + holder.postDateAndNumberExp.setVisibility(View.GONE); + holder.postDateAndNumberExp.setAlpha(0.0f); + holder.postDateAndNumberExp.setTranslationY(holder.postDateAndNumberExp.getHeight()); + + holder.username.setMaxLines(1); + holder.username.setEllipsize(TextUtils.TruncateAt.END); + + holder.subject.setTextColor(Color.parseColor("#757575")); + holder.subject.setMaxLines(1); + holder.subject.setEllipsize(TextUtils.TruncateAt.END); + } + if (viewProperties.get(position)[isQuoteButtonChecked]) + holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_checked); + else + holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_unchecked); + //Sets graphics behavior + holder.quoteToggle.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); + if (tmp[isQuoteButtonChecked]) { + if (toQuoteList.contains(currentPost.getPostNumber())) { + toQuoteList.remove(toQuoteList.indexOf(currentPost.getPostNumber())); + } else + Report.i(TAG, "An error occurred while trying to exclude post from" + + "toQuoteList, post wasn't there!"); + holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_unchecked); + } else { + toQuoteList.add(currentPost.getPostNumber()); + holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_checked); + } + tmp[isQuoteButtonChecked] = !tmp[isQuoteButtonChecked]; + viewProperties.set(holder.getAdapterPosition(), tmp); + } + }); + //Card expand/collapse when card is touched + holder.cardView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + //Change post's viewProperties accordingly + boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); + tmp[isPostDateAndNumberVisibile] = !tmp[isPostDateAndNumberVisibile]; + viewProperties.set(holder.getAdapterPosition(), tmp); + + TopicAnimations.animatePostExtraInfoVisibility(holder.postDateAndNumberExp + , holder.username, holder.subject + , Color.parseColor("#FFFFFF") + , Color.parseColor("#757575")); + } + }); + //Also when post is clicked + holder.post.setOnTouchListener(new CustomTouchListener(holder.post, holder.cardView)); + } + + void customNotifyDataSetChanged(TopicActivity.TopicTask topicTask) { + this.topicTask = topicTask; + viewProperties.clear(); + for (int i = 0; i < postsList.size(); ++i) { + //Initializes properties, array's values will be false by default + viewProperties.add(new boolean[3]); + } + notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + return postsList.size(); + } + + /** + * This class is a gesture detector for WebViews. It handles post's clicks, long clicks and + * touch and drag. + */ + private class CustomTouchListener implements View.OnTouchListener { + //Long press handling + private float downCoordinateX; + private float downCoordinateY; + private final float SCROLL_THRESHOLD = 7; + final private WebView post; + final private CardView cardView; + + //Other variables + final static int FINGER_RELEASED = 0; + final static int FINGER_TOUCHED = 1; + final static int FINGER_DRAGGING = 2; + final static int FINGER_UNDEFINED = 3; + + private int fingerState = FINGER_RELEASED; + + CustomTouchListener(WebView pPost, CardView pCard) { + post = pPost; + cardView = pCard; + } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + //Logs XY + downCoordinateX = motionEvent.getX(); + downCoordinateY = motionEvent.getY(); + + if (fingerState == FINGER_RELEASED) + fingerState = FINGER_TOUCHED; + else + fingerState = FINGER_UNDEFINED; + break; + case MotionEvent.ACTION_UP: + if (fingerState != FINGER_DRAGGING) { + //Doesn't expand the card if this was a link + WebView.HitTestResult htResult = post.getHitTestResult(); + if (htResult.getExtra() != null + && htResult.getExtra() != null) { + fingerState = FINGER_RELEASED; + return false; + } + cardView.performClick(); + } + fingerState = FINGER_RELEASED; + break; + case MotionEvent.ACTION_MOVE: + //Cancels long click if finger moved too much + if (((Math.abs(downCoordinateX - motionEvent.getX()) > SCROLL_THRESHOLD || + Math.abs(downCoordinateY - motionEvent.getY()) > SCROLL_THRESHOLD))) { + fingerState = FINGER_DRAGGING; + } else fingerState = FINGER_UNDEFINED; + break; + default: + fingerState = FINGER_UNDEFINED; + } + return false; + } + } + + /** + * This class is used to handle link clicks in WebViews. When link url is one that the app can + * handle internally, it does. Otherwise user is prompt to open the link in a browser. + */ + @SuppressWarnings("unchecked") + private class LinkLauncher extends WebViewClient { + @SuppressWarnings("deprecation") + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + final Uri uri = Uri.parse(url); + return handleUri(uri); + } + + @TargetApi(Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + final Uri uri = request.getUrl(); + return handleUri(uri); + } + + @SuppressWarnings("SameReturnValue") + private boolean handleUri(final Uri uri) { + final String uriString = uri.toString(); + + LinkTarget.Target target = LinkTarget.resolveLinkTarget(uri); + if (target.is(LinkTarget.Target.TOPIC)) { + //This url points to a topic + //Checks if this is the current topic + if (Objects.equals(uriString.substring(0, uriString.lastIndexOf(".")), base_url)) { + //Gets uri's targeted message's index number + String msgIndexReq = uriString.substring(uriString.indexOf("msg") + 3); + if (msgIndexReq.contains("#")) + msgIndexReq = msgIndexReq.substring(0, msgIndexReq.indexOf("#")); + else + msgIndexReq = msgIndexReq.substring(0, msgIndexReq.indexOf(";")); + + //Checks if this post is in the current topic's page + for (Post post : postsList) { + if (post.getPostIndex() == Integer.parseInt(msgIndexReq)) { + // TODO Don't restart Activity, Just change post focus + return true; + } + } + } + topicTask.execute(uri.toString()); + return true; + } else if (target.is(LinkTarget.Target.BOARD)) { + Intent intent = new Intent(context, BoardActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_BOARD_URL, uriString); + extras.putString(BUNDLE_BOARD_TITLE, ""); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return true; + } else if (target.is(LinkTarget.Target.PROFILE)) { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_PROFILE_URL, uriString); + extras.putString(BUNDLE_THUMBNAIL_URL, ""); + extras.putString(BUNDLE_USERNAME, ""); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return true; + } + + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + + //Method always returns true as no url should be loaded in the WebViews + return true; + } + } + + /** + * Returns a String with a single FontAwesome typeface character corresponding to this file's + * extension. + * + * @param filename String with filename containing file's extension + * @return FontAwesome character according to file's type + * @see FontAwesome + */ + @NonNull + private String faIconFromFilename(String filename) { + filename = filename.toLowerCase(); + + if (filename.contains("jpg") || filename.contains("gif") || filename.contains("jpeg") + || filename.contains("png")) + return context.getResources().getString(R.string.fa_file_image_o); + else if (filename.contains("pdf")) + return context.getResources().getString(R.string.fa_file_pdf_o); + else if (filename.contains("zip") || filename.contains("rar") || filename.contains("tar.gz")) + return context.getResources().getString(R.string.fa_file_zip_o); + else if (filename.contains("txt")) + return context.getResources().getString(R.string.fa_file_text_o); + else if (filename.contains("doc") || filename.contains("docx")) + return context.getResources().getString(R.string.fa_file_word_o); + else if (filename.contains("xls") || filename.contains("xlsx")) + return context.getResources().getString(R.string.fa_file_excel_o); + else if (filename.contains("pps")) + return context.getResources().getString(R.string.fa_file_powerpoint_o); + else if (filename.contains("mpg")) + return context.getResources().getString(R.string.fa_file_video_o); + + return context.getResources().getString(R.string.fa_file); + } + + private class DownloadTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + private static final String TAG = "DownloadTask"; //Separate tag for AsyncTask + private PowerManager.WakeLock mWakeLock; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + //Locks CPU to prevent going off + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + getClass().getName()); + mWakeLock.acquire(); + progressBar.setVisibility(View.VISIBLE); + } + + @Override + protected String doInBackground(ThmmyFile... files) { + try { + File tempFile = files[0].download(); + if (tempFile != null) { + String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + files[0].getExtension()); + + Intent intent = new Intent(); + intent.setAction(android.content.Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(tempFile), mime); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } catch (IOException e) { + Report.e(TAG, "Error while trying to download a file", e); + return e.toString(); + } + return null; + } + + @Override + protected void onPostExecute(String result) { + mWakeLock.release(); + if (result != null) + Toast.makeText(context, "Error! Download not complete.", Toast.LENGTH_SHORT).show(); + else + Toast.makeText(context, "Download complete", Toast.LENGTH_SHORT).show(); + progressBar.setVisibility(View.INVISIBLE); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAnimations.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAnimations.java new file mode 100644 index 00000000..c0206a1d --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAnimations.java @@ -0,0 +1,119 @@ +package gr.thmmy.mthmmy.activities.topic; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; + +class TopicAnimations { + //--------------------------POST'S INFO VISIBILITY CHANGE ANIMATION METHOD-------------------------- + + /** + * Method that animates view's visibility changes for post's extra info + */ + static void animatePostExtraInfoVisibility(final View dateAndPostNum, TextView username, + TextView subject, int expandedColor, int collapsedColor) { + //If the view is gone fade it in + if (dateAndPostNum.getVisibility() == View.GONE) { + //Show full username + username.setMaxLines(Integer.MAX_VALUE); //As in the android sourcecode + username.setEllipsize(null); + + //Show full subject + subject.setTextColor(expandedColor); + subject.setMaxLines(Integer.MAX_VALUE); //As in the android sourcecode + subject.setEllipsize(null); + + + dateAndPostNum.clearAnimation(); + // Prepare the View for the animation + dateAndPostNum.setVisibility(View.VISIBLE); + dateAndPostNum.setAlpha(0.0f); + + // Start the animation + dateAndPostNum.animate() + .translationY(0) + .alpha(1.0f) + .setDuration(300) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + dateAndPostNum.setVisibility(View.VISIBLE); + } + }); + } + //If the view is visible fade it out + else { + username.setMaxLines(1); //As in the android sourcecode + username.setEllipsize(TextUtils.TruncateAt.END); + + subject.setTextColor(collapsedColor); + subject.setMaxLines(1); //As in the android sourcecode + subject.setEllipsize(TextUtils.TruncateAt.END); + + dateAndPostNum.clearAnimation(); + + // Start the animation + dateAndPostNum.animate() + .translationY(dateAndPostNum.getHeight()) + .alpha(0.0f) + .setDuration(300) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + dateAndPostNum.setVisibility(View.GONE); + } + }); + } + } +//------------------------POST'S INFO VISIBILITY CHANGE ANIMATION METHOD END------------------------ + +//--------------------------USER'S INFO VISIBILITY CHANGE ANIMATION METHOD-------------------------- + + /** + * Method that animates view's visibility changes for user's extra info + */ + static void animateUserExtraInfoVisibility(final View userExtra) { + + //If the view is gone fade it in + if (userExtra.getVisibility() == View.GONE) { + + userExtra.clearAnimation(); + userExtra.setVisibility(View.VISIBLE); + userExtra.setAlpha(0.0f); + + // Start the animation + userExtra.animate() + .alpha(1.0f) + .setDuration(300) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + userExtra.setVisibility(View.VISIBLE); + } + }); + } + //If the view is visible fade it out + else { + userExtra.clearAnimation(); + + // Start the animation + userExtra.animate() + .alpha(0.0f) + .setDuration(300) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + userExtra.setVisibility(View.GONE); + } + }); + } + } +//------------------------POST'S INFO VISIBILITY CHANGE ANIMATION METHOD END------------------------ + +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java new file mode 100644 index 00000000..538dec17 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java @@ -0,0 +1,441 @@ +package gr.thmmy.mthmmy.activities.topic; + +import android.graphics.Color; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import gr.thmmy.mthmmy.model.Post; +import gr.thmmy.mthmmy.utils.FileManager.ThmmyFile; +import gr.thmmy.mthmmy.utils.ParseHelpers; +import mthmmy.utils.Report; + +/** + * Singleton used for parsing a topic. + *

Class contains the methods: