Browse Source

Version 1.0.0

master v1.0.0
Ezerous 8 years ago
parent
commit
6e8c25fecf
  1. 5
      .gitignore
  2. 31
      .gitlab-ci.yml
  3. 24
      .gitlab/issue_templates/Bug.md
  4. 5
      .gitlab/issue_templates/Improvement.md
  5. 61
      CONTRIBUTING.md
  6. 19
      README.md
  7. 1
      VERSION
  8. 41
      app/build.gradle
  9. 26
      app/src/androidTest/java/gr/thmmy/mthmmy/ExampleInstrumentedTest.java
  10. 42
      app/src/debug/google-services.json
  11. 67
      app/src/debug/java/mthmmy/utils/Report.java
  12. 59
      app/src/main/AndroidManifest.xml
  13. 139
      app/src/main/assets/apache_libraries.html
  14. BIN
      app/src/main/assets/fonts/fontawesome-webfont.ttf
  15. 78
      app/src/main/assets/mit_libraries.html
  16. 523
      app/src/main/assets/style.css
  17. 114
      app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java
  18. 213
      app/src/main/java/gr/thmmy/mthmmy/activities/LoginActivity.java
  19. 373
      app/src/main/java/gr/thmmy/mthmmy/activities/base/BaseActivity.java
  20. 78
      app/src/main/java/gr/thmmy/mthmmy/activities/base/BaseFragment.java
  21. 322
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java
  22. 320
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java
  23. 147
      app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java
  24. 113
      app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumAdapter.java
  25. 236
      app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java
  26. 85
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java
  27. 223
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java
  28. 306
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java
  29. 120
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java
  30. 242
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java
  31. 354
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java
  32. 206
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java
  33. 489
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java
  34. 663
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java
  35. 119
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAnimations.java
  36. 441
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java
  37. 96
      app/src/main/java/gr/thmmy/mthmmy/model/Board.java
  38. 58
      app/src/main/java/gr/thmmy/mthmmy/model/Category.java
  39. 139
      app/src/main/java/gr/thmmy/mthmmy/model/LinkTarget.java
  40. 315
      app/src/main/java/gr/thmmy/mthmmy/model/Post.java
  41. 75
      app/src/main/java/gr/thmmy/mthmmy/model/PostSummary.java
  42. 117
      app/src/main/java/gr/thmmy/mthmmy/model/Topic.java
  43. 75
      app/src/main/java/gr/thmmy/mthmmy/model/TopicSummary.java
  44. 341
      app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java
  45. 46
      app/src/main/java/gr/thmmy/mthmmy/utils/CircleTransform.java
  46. 54
      app/src/main/java/gr/thmmy/mthmmy/utils/CustomRecyclerView.java
  47. 163
      app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java
  48. 165
      app/src/main/java/gr/thmmy/mthmmy/utils/ParseHelpers.java
  49. 45
      app/src/main/java/gr/thmmy/mthmmy/utils/ScrollAwareFABBehavior.java
  50. 105
      app/src/main/java/gr/thmmy/mthmmy/utils/ScrollAwareLinearBehavior.java
  51. 8
      app/src/main/res/anim/push_left_in.xml
  52. 7
      app/src/main/res/anim/push_left_out.xml
  53. 8
      app/src/main/res/anim/push_right_in.xml
  54. 8
      app/src/main/res/anim/push_right_out.xml
  55. BIN
      app/src/main/res/drawable-hdpi/ic_arrow_drop_down.png
  56. BIN
      app/src/main/res/drawable-hdpi/ic_arrow_drop_up.png
  57. BIN
      app/src/main/res/drawable-hdpi/ic_format_quote_checked.png
  58. BIN
      app/src/main/res/drawable-hdpi/ic_format_quote_unchecked.png
  59. BIN
      app/src/main/res/drawable-hdpi/ic_pin.png
  60. BIN
      app/src/main/res/drawable-mdpi/ic_arrow_drop_down.png
  61. BIN
      app/src/main/res/drawable-mdpi/ic_arrow_drop_up.png
  62. BIN
      app/src/main/res/drawable-mdpi/ic_format_quote_checked.png
  63. BIN
      app/src/main/res/drawable-mdpi/ic_format_quote_unchecked.png
  64. BIN
      app/src/main/res/drawable-mdpi/ic_pin.png
  65. BIN
      app/src/main/res/drawable-xhdpi/ic_arrow_drop_down.png
  66. BIN
      app/src/main/res/drawable-xhdpi/ic_arrow_drop_up.png
  67. BIN
      app/src/main/res/drawable-xhdpi/ic_format_quote_checked.png
  68. BIN
      app/src/main/res/drawable-xhdpi/ic_format_quote_unchecked.png
  69. BIN
      app/src/main/res/drawable-xhdpi/ic_pin.png
  70. BIN
      app/src/main/res/drawable-xxhdpi/ic_arrow_drop_down.png
  71. BIN
      app/src/main/res/drawable-xxhdpi/ic_arrow_drop_up.png
  72. BIN
      app/src/main/res/drawable-xxhdpi/ic_format_quote_checked.png
  73. BIN
      app/src/main/res/drawable-xxhdpi/ic_format_quote_unchecked.png
  74. BIN
      app/src/main/res/drawable-xxhdpi/ic_pin.png
  75. BIN
      app/src/main/res/drawable-xxxhdpi/ic_arrow_drop_down.png
  76. BIN
      app/src/main/res/drawable-xxxhdpi/ic_arrow_drop_up.png
  77. BIN
      app/src/main/res/drawable-xxxhdpi/ic_format_quote_checked.png
  78. BIN
      app/src/main/res/drawable-xxxhdpi/ic_format_quote_unchecked.png
  79. BIN
      app/src/main/res/drawable-xxxhdpi/ic_pin.png
  80. BIN
      app/src/main/res/drawable/fun.jpg
  81. 13
      app/src/main/res/drawable/guest_button_border_bg.xml
  82. 9
      app/src/main/res/drawable/ic_add_fab.xml
  83. BIN
      app/src/main/res/drawable/ic_default_user_thumbnail.png
  84. 9
      app/src/main/res/drawable/ic_pm_fab.xml
  85. 10
      app/src/main/res/drawable/line_chart_gradient.xml
  86. 10
      app/src/main/res/drawable/login_button_bg.xml
  87. BIN
      app/src/main/res/drawable/logo_animated.gif
  88. 13
      app/src/main/res/drawable/member_of_the_month_card.xml
  89. 8
      app/src/main/res/drawable/page_first.xml
  90. 8
      app/src/main/res/drawable/page_last.xml
  91. 8
      app/src/main/res/drawable/page_next.xml
  92. 8
      app/src/main/res/drawable/page_previous.xml
  93. 24
      app/src/main/res/drawable/progress_bar_bg.xml
  94. 119
      app/src/main/res/layout-v21/activity_profile.xml
  95. 249
      app/src/main/res/layout-v21/activity_topic_post_row.xml
  96. 158
      app/src/main/res/layout/activity_about.xml
  97. 61
      app/src/main/res/layout/activity_board.xml
  98. 79
      app/src/main/res/layout/activity_board_sub_board.xml
  99. 77
      app/src/main/res/layout/activity_board_topic.xml
  100. 134
      app/src/main/res/layout/activity_login.xml

5
.gitignore

@ -44,4 +44,7 @@ captures/
.externalNativeBuild
### Android Patch ###
gen-external-apklibs
gen-external-apklibs
# Google Services (release build)
app/src/release/google-services.json

31
.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

24
.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)

5
.gitlab/issue_templates/Improvement.md

@ -0,0 +1,5 @@
### Description
### Proposal
### Links / references

61
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

19
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`.

1
VERSION

@ -0,0 +1 @@
1.0.0

41
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'

26
app/src/androidTest/java/gr/thmmy/mthmmy/ExampleInstrumentedTest.java

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

42
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"
}

67
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);
}
}

59
app/src/main/AndroidManifest.xml

@ -1,13 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="gr.thmmy.mthmmy">
package="gr.thmmy.mthmmy">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activities.main.MainActivity"
android:configChanges="orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".activities.LoginActivity"
android:configChanges="orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar">
</activity>
<activity
android:name=".activities.AboutActivity"
android:parentActivityName=".activities.main.MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.main.MainActivity"/>
</activity>
<activity
android:name=".activities.topic.TopicActivity"
android:configChanges="orientation|screenSize"
android:parentActivityName=".activities.main.MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.main.MainActivity"/>
</activity>
<activity
android:name=".activities.profile.ProfileActivity"
android:theme="@style/AppTheme.NoActionBar">
</activity>
<activity
android:name=".activities.board.BoardActivity"
android:configChanges="orientation|screenSize"
android:parentActivityName=".activities.main.MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.main.MainActivity"/>
</activity>
</application>
</manifest>
</manifest>

139
app/src/main/assets/apache_libraries.html

@ -0,0 +1,139 @@
<html>
<head>
<style>
body {
font-family: sans-serif;
background-color: #333333;
}
pre {
background-color: #3C3C3C;
color: #757575;
padding: 1em;
margin-left: 1em;
margin-right: 1em;
white-space: pre-wrap;
}
h4,
h5 {
display: inline;
padding: 1em;
}
a,
h4,
h5 {
color: #26A69A;
word-wrap: break-word;
}
li {
color: #26A69A;
}
</style>
</head>
<body>
<ul>
<li>
<h5><a href="https://square.github.io/okhttp/">OkHttp</a>&nbsp;v3.5.0 (Copyright ©2016 Square, Inc.)</h5>
</li>
<li>
<h5><a href="https://square.github.io/picasso/">Picasso</a>&nbsp;v2.5.2 (Copyright ©2013 Square, Inc.)</h5>
</li>
<li>
<h5><a href="https://github.com/franmontiel/PersistentCookieJar">PersistentCookieJar</a>&nbsp;v1.0.0 (Copyright ©2016 Francisco José Montiel Navarro)</h5>
</li>
<li>
<h5><a href="https://github.com/PhilJay/MPAndroidChart">MPAndroidChart</a>&nbsp;v3.0.1 (Copyright ©2016 Philipp Jahoda)</h5>
</li>
<li>
<h5><a href="https://github.com/mikepenz/MaterialDrawer">MaterialDrawer</a>&nbsp;v5.8.1 (Copyright ©2016 Mike Penz)</h5>
</li>
<li>
<h5><a href="https://github.com/mikepenz/Android-Iconics/tree/develop/fontawesome-typeface-library">Fontawesome Typeface Library</a>&nbsp;v4.7.0.0 (Copyright ©2016 Mike Penz)</h5>
</li>
<li>
<h5><a href="https://github.com/DreaminginCodeZH/MaterialProgressBar">MaterialProgressBar</a>&nbsp;v1.3.0 (Copyright ©2015 Zhang Hai)</h5>
</li>
</ul>
<pre>
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
<a href="http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0</a>
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.
</pre>
<br/>
<h4>Apache License v2.0</h4>
<pre>
Apache License
Version 2.0, January 2004
<a href="http://www.apache.org/licenses/">http://www.apache.org/licenses/</a>
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
</pre>
</body>
</html>

BIN
app/src/main/assets/fonts/fontawesome-webfont.ttf

Binary file not shown.

78
app/src/main/assets/mit_libraries.html

@ -0,0 +1,78 @@
<html>
<head>
<style>
body {
font-family: sans-serif;
background-color: #333333;
}
pre {
background-color: #3C3C3C;
color: #757575;
padding: 1em;
margin-left: 1em;
margin-right: 1em;
white-space: pre-wrap;
}
h4,
h5 {
display: inline;
padding: 1em;
}
a,
h4,
h5 {
color: #26A69A;
word-wrap: break-word;
}
li {
color: #26A69A;
}
</style>
</head>
<body>
<ul>
<li>
<h5><a href="https://jsoup.org//">jsoup</a>&nbsp;v1.10.1 (Copyright ©2009-2017, Jonathan Hedley &lt;jonathan@hedley.net&gt;)</h5>
</li>
<li>
<h5><a href="https://github.com/koral--/android-gif-drawable">android-gif-drawable</a>&nbsp;v1.2.3 (Copyright ©2016 Karol Wrótniak, Droids on Roids)</h5>
</li>
<li>
<h5><a href="https://github.com/bignerdranch/expandable-recycler-view">Expandable RecyclerView</a>&nbsp;v3.0.0-RC1 (Copyright ©2015, Big Nerd Ranch)</h5>
</li>
</ul>
<br/>
<h4>The MIT License</h4>
<pre>
Copyright &lt;YEAR&gt; &lt;COPYRIGHT HOLDER&gt;
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.
</pre>
</body>
</html>

523
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;
}

114
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);
}
}

213
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<String, Void, Integer> {
//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-------------------------------------------------
}

373
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<Void, Void, Integer> { //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-----------------------------------------------
}

78
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 {}
}

322
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<Board> parsedSubBoards = new ArrayList<>();
private final ArrayList<Topic> 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.
* <p>BoardTask's {@link AsyncTask#execute execute} method needs a boards's url as String
* parameter!</p>
*/
public class BoardTask extends AsyncTask<String, Void, Boolean> {
//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;
}
}
}

320
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<RecyclerView.ViewHolder> {
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<Board> parsedSubBoards = new ArrayList<>();
private ArrayList<Topic> parsedTopics = new ArrayList<>();
private final ArrayList<Boolean> boardExpandableVisibility = new ArrayList<>();
private final ArrayList<Boolean> topicExpandableVisibility = new ArrayList<>();
BoardAdapter(Context context, ArrayList<Board> parsedSubBoards, ArrayList<Topic> 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);
}
}
}

147
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-------------------------------------------
}

113
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<Category, Board, ForumAdapter.CategoryViewHolder, ForumAdapter.BoardViewHolder> {
private final Context context;
private final LayoutInflater layoutInflater;
private final List<Category> categories;
private final ForumFragment.ForumFragmentInteractionListener mListener;
ForumAdapter(Context context, @NonNull List<Category> 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); //?
}
}
});
}
}
}

236
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<Category> 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<Void, Void, Integer> {
private static final String TAG = "ForumTask";
private HttpUrl forumUrl = SessionManager.forumUrl; //may change upon collapse/expand
private Document document;
private final List<Category> 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);
}
}
}

85
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<RecentAdapter.ViewHolder> {
private final Context context;
private final List<TopicSummary> recentList;
private final RecentFragment.RecentFragmentInteractionListener mListener;
RecentAdapter(Context context, @NonNull List<TopicSummary> 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);
}
}
}

223
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<TopicSummary> 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<Void, Void, Integer> {
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!");
}
}
}

306
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 <b>String</b>
* containing this user's profile url using the key {@link #BUNDLE_PROFILE_URL}, a <b>String</b> containing
* this user's avatar url using the key {@link #BUNDLE_THUMBNAIL_URL} and a <b>String</b> 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}.
* <p>ProfileTask's {@link AsyncTask#execute execute} method needs a profile's url as String
* parameter!</p>
*
* @see Jsoup
*/
public class ProfileTask extends AsyncTask<String, Void, Boolean> {
//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<Fragment> mFragmentList = new ArrayList<>();
private final List<String> 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);
}
}
}

120
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<RecyclerView.ViewHolder> {
/**
* 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<PostSummary> parsedTopicSummaries;
LatestPostsAdapter(BaseFragment.FragmentInteractionListener interactionListener,
ArrayList<PostSummary> 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);
}
}
}

242
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<PostSummary> 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.
* <p>LatestPostsTask's {@link AsyncTask#execute execute} method needs a profile's url as String
* parameter!</p>
*/
public class LatestPostsTask extends AsyncTask<String, Void, Boolean> {
//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 = ("<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />" + pPost);
parsedTopicSummaries.add(new PostSummary(pTopicUrl, pTopicTitle, pDateTime, pPost));
}
}
return true;
}
}
}

354
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<Entry> postingActivityByTime = new ArrayList<>();
final private List<BarEntry> mostPopularBoardsByPosts = new ArrayList<>(), mostPopularBoardsByActivity = new ArrayList<>();
final private ArrayList<String> 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.
* <p>
* <p>Calling SummaryTask's {@link AsyncTask#execute execute} method needs to have profile's url
* as String parameter!</p>
*/
public class ProfileStatsTask extends AsyncTask<String, Void, Boolean> {
//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<String> mValues;
MyXAxisValueFormatter(ArrayList<String> values) {
this.mValues = values;
}
@Override
public String getFormattedValue(float value, AxisBase axis) {
return mValues.get((int) value);
}
}
}

206
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<String> 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.
* <p>
* <p>Calling SummaryTask's {@link AsyncTask#execute execute} method needs to have profile's url
* as String parameter!</p>
*/
public class SummaryTask extends AsyncTask<Document, Void, Void> {
//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<String> parseProfileSummary(Document profile) {
//Method's variables
ArrayList<String> 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 = ("<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />\n" +
"<div class=\"customSignature\">\n" + pHtml + "\n</div>");
} 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 = "<b>" + summaryRow.select("td").first().text() + "</b> "
+ summaryRow.select("td").get(1).text();
}
parsedInformation.add(pHtml);
}
return parsedInformation;
}
}
/**
* Simple method that builds the UI of a {@link SummaryFragment}.
* <p>Use this method <b>only after</b> parsing profile's data with
* {@link gr.thmmy.mthmmy.activities.profile.ProfileActivity.ProfileTask} as it reads from
* {@link #parsedProfileSummaryData}</p>
*/
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);
}
}
}

489
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 <b>String</b>
* containing this topics's url using the key {@link #BUNDLE_TOPIC_URL} and a <b>String</b> 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<Post> 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<Integer> toQuoteList = new ArrayList<>();
//Topic's pages
private int thisPage = 1;
public static String base_url = "";
private int numberOfPages = 1;
private final SparseArray<String> 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.
* <p>
* <p>Calling TopicTask's {@link AsyncTask#execute execute} method needs to have profile's url
* as String parameter!</p>
*/
class TopicTask extends AsyncTask<String, Void, Integer> {
//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);
}
}

663
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<TopicAdapter.MyViewHolder> {
/**
* 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<Post> 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<boolean[]> 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<Post> 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 <b>containing file's extension</b>
* @return FontAwesome character according to file's type
* @see <a href="http://fontawesome.io/">FontAwesome</a>
*/
@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<ThmmyFile, Void, String> {
//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);
}
}
}

119
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------------------------
}

441
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.
* <p>Class contains the methods:<ul><li>{@link #parseUsersViewingThisTopic(Document,
* gr.thmmy.mthmmy.utils.ParseHelpers.Language)}</li>
* <li>{@link #parseCurrentPageIndex(Document, gr.thmmy.mthmmy.utils.ParseHelpers.Language)}</li>
* <li>{@link #parseTopicNumberOfPages(Document, int, gr.thmmy.mthmmy.utils.ParseHelpers.Language)}</li>
* <li>{@link #parseTopic(Document, gr.thmmy.mthmmy.utils.ParseHelpers.Language)}</li>
*/
class TopicParser {
//User colors
private static final int USER_COLOR_BLACK = Color.parseColor("#000000");
private static final int USER_COLOR_RED = Color.parseColor("#F44336");
private static final int USER_COLOR_GREEN = Color.parseColor("#4CAF50");
private static final int USER_COLOR_BLUE = Color.parseColor("#536DFE");
static final int USER_COLOR_PINK = Color.parseColor("#FF4081");
private static final int USER_COLOR_YELLOW = Color.parseColor("#FFEB3B");
/**
* Debug Tag for logging debug output to LogCat
*/
@SuppressWarnings("unused")
private static final String TAG = "TopicParser";
/**
* Returns users currently viewing this topic.
*
* @param topic {@link Document} object containing this topic's source code
* @param language a {@link gr.thmmy.mthmmy.utils.ParseHelpers.Language} containing this topic's
* language set, this is returned by
* {@link gr.thmmy.mthmmy.utils.ParseHelpers.Language#getLanguage(Document)}
* @return String containing html with the usernames of users
* @see org.jsoup.Jsoup Jsoup
*/
static String parseUsersViewingThisTopic(Document topic, ParseHelpers.Language language) {
if (language.is(ParseHelpers.Language.GREEK))
return topic.select("td:containsOwn(διαβάζουν αυτό το θέμα)").first().html();
return topic.select("td:containsOwn(are viewing this topic)").first().html();
}
/**
* Returns current topic's page index.
*
* @param topic {@link Document} object containing this topic's source code
* @param language a {@link gr.thmmy.mthmmy.utils.ParseHelpers.Language} containing this topic's
* language set, this is returned by
* {@link gr.thmmy.mthmmy.utils.ParseHelpers.Language#getLanguage(Document)}
* @return int containing parsed topic's current page
* @see org.jsoup.Jsoup Jsoup
*/
static int parseCurrentPageIndex(Document topic, ParseHelpers.Language language) {
int parsedPage = 1;
if (language.is(ParseHelpers.Language.GREEK)) {
Elements findCurrentPage = topic.select("td:contains(Σελίδες:)>b");
for (Element item : findCurrentPage) {
if (!item.text().contains("...")
&& !item.text().contains("Σελίδες:")) {
parsedPage = Integer.parseInt(item.text());
break;
}
}
} else {
Elements findCurrentPage = topic.select("td:contains(Pages:)>b");
for (Element item : findCurrentPage) {
if (!item.text().contains("...") && !item.text().contains("Pages:")) {
parsedPage = Integer.parseInt(item.text());
break;
}
}
}
return parsedPage;
}
/**
* Returns the number of this topic's pages.
*
* @param topic {@link Document} object containing this topic's source code
* @param currentPage an int containing current page of this topic
* @param language a {@link gr.thmmy.mthmmy.utils.ParseHelpers.Language} containing this topic's
* language set, this is returned by
* {@link gr.thmmy.mthmmy.utils.ParseHelpers.Language#getLanguage(Document)}
* @return int containing the number of pages
* @see org.jsoup.Jsoup Jsoup
*/
static int parseTopicNumberOfPages(Document topic, int currentPage, ParseHelpers.Language language) {
int returnPages = 1;
if (language.is(ParseHelpers.Language.GREEK)) {
Elements pages = topic.select("td:contains(Σελίδες:)>a.navPages");
if (pages.size() != 0) {
returnPages = currentPage;
for (Element item : pages) {
if (Integer.parseInt(item.text()) > returnPages)
returnPages = Integer.parseInt(item.text());
}
}
} else {
Elements pages = topic.select("td:contains(Pages:)>a.navPages");
if (pages.size() != 0) {
returnPages = currentPage;
for (Element item : pages) {
if (Integer.parseInt(item.text()) > returnPages)
returnPages = Integer.parseInt(item.text());
}
}
}
return returnPages;
}
/**
* This method parses all the information of a topic and it's posts.
*
* @param topic {@link Document} object containing this topic's source code
* @param language a {@link gr.thmmy.mthmmy.utils.ParseHelpers.Language} containing this topic's
* language set, this is returned by
* {@link gr.thmmy.mthmmy.utils.ParseHelpers.Language#getLanguage(Document)}
* @return {@link ArrayList} of {@link Post}s
* @see org.jsoup.Jsoup Jsoup
*/
static ArrayList<Post> parseTopic(Document topic, ParseHelpers.Language language) {
//Method's variables
final int NO_INDEX = -1;
ArrayList<Post> parsedPostsList = new ArrayList<>();
Elements postRows;
//Each row is a post
if (language.is(ParseHelpers.Language.GREEK))
postRows = topic.select("form[id=quickModForm]>table>tbody>tr:matches(στις)");
else {
postRows = topic.select("form[id=quickModForm]>table>tbody>tr:matches(on)");
}
for (Element thisRow : postRows) {
//Variables for Post constructor
String p_userName, p_thumbnailUrl, p_subject, p_post, p_postDate, p_profileURL, p_rank,
p_specialRank, p_gender, p_personalText, p_numberOfPosts;
int p_postNum, p_postIndex, p_numberOfStars, p_userColor;
boolean p_isDeleted = false;
ArrayList<ThmmyFile> p_attachedFiles;
//Initialize variables
p_profileURL = null;
p_rank = "Rank";
p_specialRank = "Special rank";
p_gender = "";
p_personalText = "";
p_numberOfPosts = "";
p_numberOfStars = 0;
p_userColor = USER_COLOR_YELLOW;
p_attachedFiles = new ArrayList<>();
//Language independent parsing
//Finds thumbnail url
Element thumbnailUrl = thisRow.select("img.avatar").first();
p_thumbnailUrl = null; //In case user doesn't have an avatar
if (thumbnailUrl != null) {
p_thumbnailUrl = thumbnailUrl.attr("abs:src");
}
//Finds subject
p_subject = thisRow.select("div[id^=subject_]").first().select("a").first().text();
//Finds post's text
p_post = ParseHelpers.youtubeEmbeddedFix(thisRow.select("div").select(".post").first());
//Add stuff to make it work in WebView
//style.css
p_post = ("<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />" + p_post);
//Find post's index
//This is an int assigned by the forum used for post focusing and quotes, it is not
//the same as reply index.
Element postIndex = thisRow.select("a[name^=msg]").first();
if (postIndex == null)
p_postIndex = NO_INDEX;
else {
String tmp = postIndex.attr("name");
p_postIndex = Integer.parseInt(tmp.substring(tmp.indexOf("msg") + 3));
}
//Language dependent parsing
Element userName;
if (language.is(ParseHelpers.Language.GREEK)) {
//Finds username and profile's url
userName = thisRow.select("a[title^=Εμφάνιση προφίλ του μέλους]").first();
if (userName == null) { //Deleted profile
p_isDeleted = true;
p_userName = thisRow
.select("td:has(div.smalltext:containsOwn(Επισκέπτης))[style^=overflow]")
.first().text();
p_userName = p_userName.substring(0, p_userName.indexOf(" Επισκέπτης"));
p_userColor = USER_COLOR_BLACK;
} else {
p_userName = userName.html();
p_profileURL = userName.attr("href");
}
//Finds post's submit date
Element postDate = thisRow.select("div.smalltext:matches(στις:)").first();
p_postDate = postDate.text();
p_postDate = p_postDate.substring(p_postDate.indexOf("στις:") + 6
, p_postDate.indexOf(" »"));
//Finds post's reply index number
Element postNum = thisRow.select("div.smalltext:matches(Απάντηση #)").first();
if (postNum == null) { //Topic starter
p_postNum = 0;
} else {
String tmp_str = postNum.text().substring(12);
p_postNum = Integer.parseInt(tmp_str.substring(0, tmp_str.indexOf(" στις")));
}
//Finds attached file's urls, names and info, if present
Elements postAttachments = thisRow.select("div:containsOwn(έγινε λήψη):containsOwn(φορές.)");
if (postAttachments != null) {
Elements attachedFiles = postAttachments.select("a");
String postAttachmentsText = postAttachments.text();
for (int i = 0; i < attachedFiles.size(); ++i) {
URL attachedUrl;
//Gets file's url and filename
Element tmpAttachedFileUrlAndName = attachedFiles.get(i);
try {
attachedUrl = new URL(tmpAttachedFileUrlAndName.attr("href"));
} catch (MalformedURLException e) {
Report.e(TAG, "Attached file malformed url", e);
break;
}
String attachedFileName = tmpAttachedFileUrlAndName.text().substring(1);
//Gets file's info (size and download count)
String postAttachmentsTextSbstr = postAttachmentsText.substring(
postAttachmentsText.indexOf(attachedFileName));
String attachedFileInfo = postAttachmentsTextSbstr.substring(attachedFileName
.length(), postAttachmentsTextSbstr.indexOf("φορές.")) + "φορές.)";
p_attachedFiles.add(new ThmmyFile(attachedUrl, attachedFileName, attachedFileInfo));
}
}
} else {
//Finds username
userName = thisRow.select("a[title^=View the profile of]").first();
if (userName == null) { //Deleted profile
p_isDeleted = true;
p_userName = thisRow
.select("td:has(div.smalltext:containsOwn(Guest))[style^=overflow]")
.first().text();
p_userName = p_userName.substring(0, p_userName.indexOf(" Guest"));
p_userColor = USER_COLOR_BLACK;
} else {
p_userName = userName.html();
p_profileURL = userName.attr("href");
}
//Finds post's submit date
Element postDate = thisRow.select("div.smalltext:matches(on:)").first();
p_postDate = postDate.text();
p_postDate = p_postDate.substring(p_postDate.indexOf("on:") + 4
, p_postDate.indexOf(" »"));
//Finds post's reply index number
Element postNum = thisRow.select("div.smalltext:matches(Reply #)").first();
if (postNum == null) { //Topic starter
p_postNum = 0;
} else {
String tmp_str = postNum.text().substring(9);
p_postNum = Integer.parseInt(tmp_str.substring(0, tmp_str.indexOf(" on")));
}
//Finds attached file's urls, names and info, if present
Elements postAttachments = thisRow.select("div:containsOwn(downloaded):containsOwn(times.)");
if (postAttachments != null) {
Elements attachedFiles = postAttachments.select("a");
String postAttachmentsText = postAttachments.text();
for (int i = 0; i < attachedFiles.size(); ++i) {
URL attachedUrl;
//Gets file's url and filename
Element tmpAttachedFileUrlAndName = attachedFiles.get(i);
try {
attachedUrl = new URL(tmpAttachedFileUrlAndName.attr("href"));
} catch (MalformedURLException e) {
Report.e(TAG, "Attached file malformed url", e);
break;
}
String attachedFileName = tmpAttachedFileUrlAndName.text().substring(1);
//Gets file's info (size and download count)
String postAttachmentsTextSbstr = postAttachmentsText.substring(
postAttachmentsText.indexOf(attachedFileName));
String attachedFileInfo = postAttachmentsTextSbstr.substring(attachedFileName
.length(), postAttachmentsTextSbstr.indexOf("times.")) + "times.)";
p_attachedFiles.add(new ThmmyFile(attachedUrl, attachedFileName, attachedFileInfo));
}
}
}
if (!p_isDeleted) { //Active user
//Gets extra info
int postsLineIndex = -1;
int starsLineIndex = -1;
Element usersExtraInfo = userName.parent().nextElementSibling(); //Get sibling "div"
List<String> infoList = Arrays.asList(usersExtraInfo.html().split("<br>"));
if (language.is(ParseHelpers.Language.GREEK)) {
for (String line : infoList) {
if (line.contains("Μηνύματα:")) {
postsLineIndex = infoList.indexOf(line);
//Remove any line breaks and spaces on the start and end
p_numberOfPosts = line.replace("\n", "").replace("\r", "").trim();
}
if (line.contains("Φύλο:")) {
if (line.contains("alt=\"Άντρας\""))
p_gender = "Φύλο: Άντρας";
else
p_gender = "Φύλο: Γυναίκα";
}
if (line.contains("alt=\"*\"")) {
starsLineIndex = infoList.indexOf(line);
Document starsHtml = Jsoup.parse(line);
p_numberOfStars = starsHtml.select("img[alt]").size();
p_userColor = colorPicker(starsHtml.select("img[alt]").first()
.attr("abs:src"));
}
}
} else {
for (String line : infoList) {
if (line.contains("Posts:")) {
postsLineIndex = infoList.indexOf(line);
//Remove any line breaks and spaces on the start and end
p_numberOfPosts = line.replace("\n", "").replace("\r", "").trim();
}
if (line.contains("Gender:")) {
if (line.contains("alt=\"Male\""))
p_gender = "Gender: Male";
else
p_gender = "Gender: Female";
}
if (line.contains("alt=\"*\"")) {
starsLineIndex = infoList.indexOf(line);
Document starsHtml = Jsoup.parse(line);
p_numberOfStars = starsHtml.select("img[alt]").size();
p_userColor = colorPicker(starsHtml.select("img[alt]").first()
.attr("abs:src"));
}
}
}
//If this member has no stars yet ==> New member,
//or is just a member
if (starsLineIndex == -1 || starsLineIndex == 1) {
p_rank = infoList.get(0).trim(); //First line has the rank
p_specialRank = null; //They don't have a special rank
} else if (starsLineIndex == 2) { //This member has a special rank
p_specialRank = infoList.get(0).trim(); //First line has the special rank
p_rank = infoList.get(1).trim(); //Second line has the rank
}
for (int i = postsLineIndex + 1; i < infoList.size() - 1; ++i) {
//Searches under "Posts:"
//and above "Personal Message", "View Profile" etc buttons
String thisLine = infoList.get(i);
if (!Objects.equals(thisLine, "") && thisLine != null
&& !Objects.equals(thisLine, " \n")
&& !thisLine.contains("avatar")
&& !thisLine.contains("<a href=")) {
p_personalText = thisLine;
p_personalText = p_personalText.replace("\n", "").replace("\r", "").trim();
}
}
//Add new post in postsList, extended information needed
parsedPostsList.add(new Post(p_thumbnailUrl, p_userName, p_subject, p_post, p_postIndex
, p_postNum, p_postDate, p_profileURL, p_rank, p_specialRank, p_gender
, p_numberOfPosts, p_personalText, p_numberOfStars, p_userColor
, p_attachedFiles));
} else { //Deleted user
//Add new post in postsList, only standard information needed
parsedPostsList.add(new Post(p_thumbnailUrl, p_userName, p_subject, p_post, p_postIndex
, p_postNum, p_postDate, p_userColor, p_attachedFiles));
}
}
return parsedPostsList;
}
/**
* Returns the color of a user according to user's rank on forum.
*
* @param starsUrl String containing the URL of a user's stars
* @return an int corresponding to the right color
*/
private static int colorPicker(String starsUrl) {
if (starsUrl.contains("/star.gif"))
return USER_COLOR_YELLOW;
else if (starsUrl.contains("/starmod.gif"))
return USER_COLOR_GREEN;
else if (starsUrl.contains("/stargmod.gif"))
return USER_COLOR_BLUE;
else if (starsUrl.contains("/staradmin.gif"))
return USER_COLOR_RED;
else if (starsUrl.contains("/starweb.gif"))
return USER_COLOR_BLACK;
else if (starsUrl.contains("/oscar.gif"))
return USER_COLOR_PINK;
return USER_COLOR_YELLOW;
}
}

96
app/src/main/java/gr/thmmy/mthmmy/model/Board.java

@ -0,0 +1,96 @@
package gr.thmmy.mthmmy.model;
/**
* Class that defines a board of the forum. All member variables are declared final (thus no setters
* are supplied). Class has one constructor and getter methods for all variables.
* <p>A forum board is described by the board's url, its title, the moderators assigned to it, its
* view and reply stats, its latest post's info and url.</p>
*/
public class Board {
private final String url, title, mods, stats, lastPost, lastPostUrl;
// Suppresses default constructor
@SuppressWarnings("unused")
private Board() {
url = null;
title = null;
mods = null;
stats = null;
lastPost = null;
lastPostUrl = null;
}
/**
* Constructor specifying all class variables necessary to describe this board. All variables
* are declared final, once assigned they can not change.
*
* @param url this board's url
* @param title this board's title
* @param mods this board's assigned moderators
* @param stats this board's view and reply stats
* @param lastPost this board's latest post's info
* @param lastPostUrl this board's latest post's url
*/
public Board(String url, String title, String mods, String stats, String lastPost, String lastPostUrl) {
this.url = url;
this.title = title;
this.mods = mods;
this.stats = stats;
this.lastPost = lastPost;
this.lastPostUrl = lastPostUrl;
}
/**
* Gets this board's url.
*
* @return this board's url
*/
public String getUrl() {
return url;
}
/**
* Gets this board's title.
*
* @return this board's title
*/
public String getTitle() {
return title;
}
/**
* Gets this board's assigned moderators.
*
* @return this board's moderators
*/
public String getMods() {
return mods;
}
/**
* Gets this board's view and reply stats.
*
* @return this board's stats
*/
public String getStats() {
return stats;
}
/**
* Gets this board's latest post's info.
*
* @return latest post's info
*/
public String getLastPost() {
return lastPost;
}
/**
* Gets this board's latest post's url.
*
* @return latest post's url
*/
public String getLastPostUrl() {
return lastPostUrl;
}
}

58
app/src/main/java/gr/thmmy/mthmmy/model/Category.java

@ -0,0 +1,58 @@
package gr.thmmy.mthmmy.model;
import com.bignerdranch.expandablerecyclerview.model.Parent;
import java.util.ArrayList;
import java.util.List;
import static android.R.attr.id;
public class Category implements Parent<Board>
{
private final String title;
private final String categoryURL;
private boolean expanded = false;
private final List<Board> boards;
public Category(String title, String categoryURL) {
this.title = title;
this.categoryURL = categoryURL;
boards = new ArrayList<>();
}
public int getId() {
return id;
}
public String getTitle() {
return title;
}
public String getCategoryURL() {
return categoryURL;
}
public boolean isExpanded() {
return expanded;
}
public List<Board> getBoards() {
return boards;
}
public void setExpanded(boolean expanded) {
this.expanded = expanded;
}
@Override
public List<Board> getChildList() {
return getBoards();
}
@Override
public boolean isInitiallyExpanded() {
return expanded;
}
}

139
app/src/main/java/gr/thmmy/mthmmy/model/LinkTarget.java

@ -0,0 +1,139 @@
package gr.thmmy.mthmmy.model;
import android.net.Uri;
import android.support.annotation.NonNull;
import java.util.Objects;
import mthmmy.utils.Report;
/**
* This class consists exclusively of static classes (enums) and methods (excluding methods of inner
* classes). It can be used to resolve link targets as to whether they are pointing to the forum and
* where in the forum they may point.
*/
public class LinkTarget {
/**
* Debug Tag for logging debug output to LogCat
*/
@SuppressWarnings("unused")
private static final String TAG = "LinkTarget";
/**
* An enum describing a link's target by defining the types:<ul>
* <li>{@link #NOT_THMMY}</li>
* <li>{@link #THMMY}</li>
* <li>{@link #UNKNOWN_THMMY}</li>
* <li>{@link #TOPIC}</li>
* <li>{@link #BOARD}</li>
* <li>{@link #UNREAD_POSTS}</li>
* <li>{@link #PROFILE_SUMMARY}</li>
* <li>{@link #PROFILE_LATEST_POSTS}</li>
* <li>{@link #PROFILE_STATS}</li>
* <li>{@link #PROFILE}</li>
* </ul>
*/
public enum Target {
/**
* Link doesn't point to thmmy.
*/
NOT_THMMY,
/**
* Link points to thmmy.
*/
THMMY,
/**
* Link points to a thmmy page that's not (yet) supported by the app.
*/
UNKNOWN_THMMY,
/**
* Link points to a topic.
*/
TOPIC,
/**
* Link points to a board.
*/
BOARD,
/**
* Link points to user's unread posts.
*/
UNREAD_POSTS,
/**
* Link points to a profile's summary.
*/
PROFILE_SUMMARY,
/**
* Link points to a profile's latest posts.
*/
PROFILE_LATEST_POSTS,
/**
* Link points to a profile's stats.
*/
PROFILE_STATS,
/**
* Link points to a profile.
*/
PROFILE;
/**
* This method defines a custom equality check for {@link Target} enums. It does not check
* whether a url is equal to another.
* <p>Method returns true if parameter's Target is the same as the object and in the specific
* cases described below, false otherwise.</p><ul>
* <li>(Everything but {@link #NOT_THMMY}).is({@link #THMMY}) returns true</li>
* <li>{@link #PROFILE_SUMMARY}.is({@link #PROFILE}) returns true</li>
* <li>{@link #PROFILE_LATEST_POSTS}.is({@link #PROFILE}) returns true</li>
* <li>{@link #PROFILE_STATS}.is({@link #PROFILE}) returns true</li>
* <li>{@link #PROFILE}.is({@link #PROFILE_SUMMARY}) returns false</li>
* <li>{@link #PROFILE}.is({@link #PROFILE_LATEST_POSTS}) returns false</li>
* <li>{@link #PROFILE}.is({@link #PROFILE_STATS}) returns false</li></ul>
*
* @param other another Target
* @return true if <b>enums</b> are equal, false otherwise
*/
public boolean is(Target other) {
return (this == PROFILE_LATEST_POSTS ||
this == PROFILE_STATS ||
this == PROFILE_SUMMARY) && other == PROFILE
|| (this != NOT_THMMY && other == THMMY)
|| this == other;
}
}
/**
* Simple method the checks whether a url's target is thmmy or not.
*
* @param uri url to check
* @return true if url is pointing to thmmy, false otherwise
*/
public static boolean isThmmy(Uri uri) {
return resolveLinkTarget(uri) != Target.NOT_THMMY;
}
/**
* This method is used to determine a url's target.
*
* @param uri url to resolve
* @return resolved target
*/
public static Target resolveLinkTarget(Uri uri) {
final String host = uri.getHost();
final String uriString = uri.toString();
if (Objects.equals(host, "www.thmmy.gr")) {
if (uriString.contains("topic=")) return Target.TOPIC;
else if (uriString.contains("board=")) return Target.BOARD;
else if (uriString.contains("action=profile")) {
if (uriString.contains(";sa=showPosts"))
return Target.PROFILE_LATEST_POSTS;
else if (uriString.contains(";sa=statPanel"))
return Target.PROFILE_STATS;
else return Target.PROFILE_SUMMARY;
} else if (uriString.contains("action=unread"))
return Target.UNREAD_POSTS;
Report.v(TAG, "Unknown thmmy link found, link: " + uriString);
return Target.UNKNOWN_THMMY;
}
return Target.NOT_THMMY;
}
}

315
app/src/main/java/gr/thmmy/mthmmy/model/Post.java

@ -0,0 +1,315 @@
package gr.thmmy.mthmmy.model;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Objects;
import gr.thmmy.mthmmy.utils.FileManager.ThmmyFile;
/**
* Class that defines a topic's post. All member variables are declared final (thus no setters are
* supplied). Class has two constructors and getter methods for all variables.
* <p>A post is described by its author's thumbnail image url, author's username, its subject, its
* content, its index on the forum, its (index) number on the topic, its date of post, author's
* user color and a list of its attached files <b>when post's author is a deleted user</b>.</p>
* <p>When the author is an active user, post also needs author's profile url, rank and special rank,
* gender, number of posts, personal text and number of start to be described <b>in addition to
* previous fields</b>.</p>
*/
public class Post {
//Standard info (exists in every post)
private final String thumbnailUrl;
private final String author;
private final String subject;
private final String content;
private final int postIndex;
private final int postNumber;
private final String postDate;
private final boolean isDeleted;
private final int userColor;
private final ArrayList<ThmmyFile> attachedFiles;
//Extra info
private final String profileURL;
private final String rank;
private final String specialRank;
private final String gender;
private final String numberOfPosts;
private final String personalText;
private final int numberOfStars;
// Suppresses default constructor
@SuppressWarnings("unused")
private Post() {
thumbnailUrl = "";
author = null;
subject = null;
content = null;
postIndex = -1;
postNumber = -1;
postDate = null;
isDeleted = true;
profileURL = null;
userColor = -1;
rank = "Rank";
specialRank = "Special rank";
gender = "Gender";
numberOfPosts = "Posts: 0";
personalText = "";
numberOfStars = 0;
attachedFiles = null;
}
/**
* Constructor for active user's posts. All variables are declared final, once assigned they
* can not change. Parameters notated as {@link Nullable} can either pass null or empty
* (strings/ArrayList).
*
* @param thumbnailUrl author's thumbnail url
* @param author author's username
* @param subject post's subject
* @param content post itself
* @param postIndex post's index on the forum
* @param postNumber posts index number on this topic
* @param postDate date of submission
* @param profileURl author's profile url
* @param rank author's rank
* @param special_rank author's special rank
* @param gender author's gender
* @param numberOfPosts author's number of posts
* @param personalText author's personal text
* @param numberOfStars author's number of stars
* @param userColor author's user color
* @param attachedFiles post's attached files
*/
public Post(@Nullable String thumbnailUrl, String author, String subject, String content
, int postIndex, int postNumber, String postDate, String profileURl, @Nullable String rank
, @Nullable String special_rank, @Nullable String gender, @Nullable String numberOfPosts
, @Nullable String personalText, int numberOfStars, int userColor
, @Nullable ArrayList<ThmmyFile> attachedFiles) {
if (Objects.equals(thumbnailUrl, "")) this.thumbnailUrl = null;
else this.thumbnailUrl = thumbnailUrl;
this.author = author;
this.subject = subject;
this.content = content;
this.postIndex = postIndex;
this.postNumber = postNumber;
this.postDate = postDate;
this.isDeleted = false;
this.userColor = userColor;
this.attachedFiles = attachedFiles;
this.profileURL = profileURl;
this.rank = rank;
this.specialRank = special_rank;
this.gender = gender;
this.numberOfPosts = numberOfPosts;
this.personalText = personalText;
this.numberOfStars = numberOfStars;
}
/**
* Constructor for deleted user's posts. All variables are declared final, once assigned they
* can not change. Parameters notated as {@link Nullable} can either pass null or empty
* (strings/ArrayList).
*
* @param thumbnailUrl author's thumbnail url
* @param author author's username
* @param subject post's subject
* @param content post itself
* @param postIndex post's index on the forum
* @param postNumber posts index number on this topic
* @param postDate date of submission
* @param userColor author's user color
* @param attachedFiles post's attached files
*/
public Post(@Nullable String thumbnailUrl, String author, String subject, String content
, int postIndex, int postNumber, String postDate, int userColor
, @Nullable ArrayList<ThmmyFile> attachedFiles) {
if (Objects.equals(thumbnailUrl, "")) this.thumbnailUrl = null;
else this.thumbnailUrl = thumbnailUrl;
this.author = author;
this.subject = subject;
this.content = content;
this.postIndex = postIndex;
this.postNumber = postNumber;
this.postDate = postDate;
this.isDeleted = true;
this.userColor = userColor;
this.attachedFiles = attachedFiles;
profileURL = null;
rank = "Rank";
specialRank = "Special rank";
gender = "Gender";
numberOfPosts = "Posts: 0";
personalText = "";
numberOfStars = 0;
}
//Getters
/**
* Gets this post author's thumbnail url.
*
* @return author's thumbnail url
*/
@Nullable
public String getThumbnailUrl() {
return thumbnailUrl;
}
/**
* Gets this post
*
* @return post's content
*/
@Nullable
public String getContent() {
return content;
}
/**
* Gets this post's author.
*
* @return post's author
*/
@Nullable
public String getAuthor() {
return author;
}
/**
* Gets this post's subject.
*
* @return post's subject
*/
@Nullable
public String getSubject() {
return subject;
}
/**
* Gets this post's date of submission.
*
* @return post's date
*/
@Nullable
public String getPostDate() {
return postDate;
}
/**
* Gets post's index number on this topic.
*
* @return post's number on topic
*/
public int getPostNumber() {
return postNumber;
}
/**
* Gets this post's index on the forum.
*
* @return post's index on the forum
*/
public int getPostIndex() {
return postIndex;
}
/**
* Is true if post's author is a deleted user, false otherwise.
*
* @return true is author is deleted, false otherwise
*/
public boolean isDeleted() {
return isDeleted;
}
/**
* Gets this post's author profile url.
*
* @return author's profile url
*/
@Nullable
public String getProfileURL() {
return profileURL;
}
/**
* Gets this post's author rank.
*
* @return author's rank
*/
@Nullable
public String getRank() {
return rank;
}
/**
* Gets this post's author special rank.
*
* @return author's special rank
*/
@Nullable
public String getSpecialRank() {
return specialRank;
}
/**
* Gets this post's author gender.
*
* @return author's gender
*/
@Nullable
public String getGender() {
return gender;
}
/**
* Gets this post's author number of posts.
*
* @return author's number of posts
*/
@Nullable
public String getNumberOfPosts() {
return numberOfPosts;
}
/**
* Gets this post's author personal text.
*
* @return author's personal text
*/
@Nullable
public String getPersonalText() {
return personalText;
}
/**
* Gets this post's author number of stars.
*
* @return author's number of stars
*/
public int getNumberOfStars() {
return numberOfStars;
}
/**
* Gets this post's author user color.
*
* @return author's user color
*/
public int getUserColor() {
return userColor;
}
/**
* Gets this post's attached files.
*
* @return attached files
*/
@Nullable
public ArrayList<ThmmyFile> getAttachedFiles() {
return attachedFiles;
}
}

75
app/src/main/java/gr/thmmy/mthmmy/model/PostSummary.java

@ -0,0 +1,75 @@
package gr.thmmy.mthmmy.model;
/**
* Class that defines the summary of a post. All member variables are declared final (thus no
* setters are supplied). Class has one constructor and getter methods for all variables.
* <p>A post summary is described by its url, subject, date and time of post and its content</b>.
*/
public class PostSummary {
private final String postUrl;
private final String subject;
private final String dateTime;
private final String post;
// Suppresses default constructor
@SuppressWarnings("unused")
private PostSummary() {
this.postUrl = null;
this.subject = null;
this.dateTime = null;
this.post = null;
}
/**
* Constructor specifying all class variables necessary to summarise this post. All variables
* are declared final, once assigned they can not change.
*
* @param postUrl this post's url
* @param subject this post's subject
* @param dateTime this post's date and time of submission
* @param post this post's content
*/
public PostSummary(String postUrl, String subject, String dateTime,
String post) {
this.postUrl = postUrl;
this.subject = subject;
this.dateTime = dateTime;
this.post = post;
}
/**
* Gets this post's url.
*
* @return post's url
*/
public String getPostUrl() {
return postUrl;
}
/**
* Gets this post's subject.
*
* @return post's subject
*/
public String getSubject() {
return subject;
}
/**
* Gets this post's date and time of submission.
*
* @return post's date and time
*/
public String getDateTime() {
return dateTime;
}
/**
* Gets this post's content.
*
* @return post's content
*/
public String getPost() {
return post;
}
}

117
app/src/main/java/gr/thmmy/mthmmy/model/Topic.java

@ -0,0 +1,117 @@
package gr.thmmy.mthmmy.model;
/**
* Class that defines a topic. All member variables are declared final (thus no setters are supplied).
* Class has one constructor and getter methods for all variables.
* <p>A topic is described by its url, subject, username of creator, its date and time of this
* topic's last post, url of this topic's last post, its view and reply stats, whether it's locked or
* not and whether it's sticky or not.</b>.
*/
public class Topic extends TopicSummary {
private final String lastPostUrl, stats;
private final boolean locked, sticky;
// Suppresses default constructor
@SuppressWarnings("unused")
private Topic() {
super();
this.lastPostUrl = null;
this.stats = null;
this.locked = false;
this.sticky = false;
}
/**
* Constructor specifying all class variables necessary to describe this topic. All variables
* are declared final, once assigned they can not change.
*
* @param topicUrl this topic's url
* @param subject this topic's subject
* @param starter this topic starter's username
* @param lastPost username of topic's last post's author
* @param lastPostUrl url of topic's last post
* @param stats this topic's view and reply stats
* @param locked whether this topic is locked or not
* @param sticky whether this topic is sticky or not
*/
public Topic(String topicUrl, String subject, String starter, String lastPost, String lastPostUrl,
String stats, boolean locked, boolean sticky) {
super(topicUrl, subject, starter, lastPost);
this.lastPostUrl = lastPostUrl;
this.stats = stats;
this.locked = locked;
this.sticky = sticky;
}
/**
* Gets this topic's url.
*
* @return this topic's url
*/
public String getUrl() {
return topicUrl;
}
/**
* Gets this topic's subject.
*
* @return this topic's subject
*/
public String getSubject() {
return subject;
}
/**
* Gets this topic's starter username.
*
* @return this topic's starter username
*/
public String getStarter() {
return lastUser;
}
/**
* Gets this topic's last post's date and time.
*
* @return last post's date and time
*/
public String getLastPostDateAndTime() {
return dateTimeModified;
}
/**
* Gets this topic's last post's url.
*
* @return last post's url
*/
public String getLastPostUrl() {
return lastPostUrl;
}
/**
* Gets this topic's view and reply stats.
*
* @return this topic's view and reply stats
*/
public String getStats() {
return stats;
}
/**
* Gets this topic's lock status. True if topic is locked, false otherwise.
*
* @return this topic's lock status
*/
public boolean isLocked() {
return locked;
}
/**
* Gets this topic's sticky status. True if topic is locked, false otherwise.
*
* @return this topic's sticky status
*/
public boolean isSticky() {
return sticky;
}
}

75
app/src/main/java/gr/thmmy/mthmmy/model/TopicSummary.java

@ -0,0 +1,75 @@
package gr.thmmy.mthmmy.model;
/**
* Class that defines the summary of a topic. All member variables are declared final (thus no
* setters are supplied). Class has one constructor and getter methods for all variables.
* <p>A topic summary is described by its url, subject, username of its last author and its date and
* time of this topic's last post.</b>.
*/
public class TopicSummary {
final String topicUrl;
final String subject;
final String lastUser;
final String dateTimeModified;
// Suppresses default constructor
@SuppressWarnings("unused")
TopicSummary() {
this.topicUrl = null;
this.subject = null;
this.lastUser = null;
this.dateTimeModified = null;
}
/**
* Constructor specifying all class variables necessary to summarise this topic. All variables
* are declared final, once assigned they can not change.
*
* @param topicUrl this topic's url
* @param subject this topic's subject
* @param lastUser username of this topic's last author
* @param dateTimeModified this topic's date and time of last post
*/
public TopicSummary(String topicUrl, String subject, String lastUser, String dateTimeModified) {
this.topicUrl = topicUrl;
this.subject = subject;
this.lastUser = lastUser;
this.dateTimeModified = dateTimeModified;
}
/**
* Gets this topic's url.
*
* @return this topic's url
*/
public String getTopicUrl() {
return topicUrl;
}
/**
* Gets this topic's subject.
*
* @return this topic's subject
*/
public String getSubject() {
return subject;
}
/**
* Gets username of this topic's last author.
*
* @return username of last author
*/
public String getLastUser() {
return lastUser;
}
/**
* Gets this topic's date and time of last post.
*
* @return this topic's date and time of last post
*/
public String getDateTimeModified() {
return dateTimeModified;
}
}

341
app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java

@ -0,0 +1,341 @@
package gr.thmmy.mthmmy.session;
import android.content.SharedPreferences;
import android.support.annotation.Nullable;
import com.franmontiel.persistentcookiejar.PersistentCookieJar;
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor;
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.io.InterruptedIOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import mthmmy.utils.Report;
import okhttp3.Cookie;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
This class handles all session related operations (e.g. login, logout)
and stores data to SharedPreferences (session information and cookies).
*/
public class SessionManager
{
//Class TAG
private static final String TAG = "SessionManager";
//Generic constants
public static final HttpUrl indexUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php");
public static final HttpUrl forumUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=forum");
private static final HttpUrl loginUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=login2");
private static final String guestName = "Guest";
//Response Codes
public static final int SUCCESS = 0;
public static final int FAILURE = 1; //Generic Error
public static final int WRONG_USER = 2;
public static final int WRONG_PASSWORD = 3;
public static final int CANCELLED = 4;
public static final int CONNECTION_ERROR = 5;
public static final int EXCEPTION = 6;
// Client & Cookies
private OkHttpClient client;
private PersistentCookieJar cookieJar;
private SharedPrefsCookiePersistor cookiePersistor; //Used to explicitly edit cookies in cookieJar
//Shared Preferences & its keys
private SharedPreferences sharedPrefs;
public static final String USERNAME = "Username";
public static final String AVATAR_LINK = "AvatarLink";
public static final String HAS_AVATAR = "HasAvatar";
public static final String LOGOUT_LINK = "LogoutLink";
public static final String LOGGED_IN = "LoggedIn";
public static final String LOGIN_SCREEN_AS_DEFAULT = "LoginScreenAsDefault";
//Constructor
public SessionManager(OkHttpClient client, PersistentCookieJar cookieJar,
SharedPrefsCookiePersistor cookiePersistor, SharedPreferences sharedPrefs)
{
this.client = client;
this.cookiePersistor=cookiePersistor;
this.cookieJar = cookieJar;
this.sharedPrefs = sharedPrefs;
}
//------------------------------------AUTH BEGINS----------------------------------------------
/**
* Login function with two options: (username, password) or nothing (using saved cookies).
* Always call it in a separate thread.
*/
public int login(String... strings)
{
Report.i(TAG, "Logging in...");
//Build the login request for each case
Request request;
if (strings.length == 2)
{
clearSessionData();
String loginName = strings[0];
String password = strings[1];
RequestBody formBody = new FormBody.Builder()
.add("user", loginName)
.add("passwrd", password)
.add("cookielength", "-1") //-1 is forever
.build();
request = new Request.Builder()
.url(loginUrl)
.post(formBody)
.build();
}
else
{
request = new Request.Builder()
.url(loginUrl)
.build();
}
try {
//Make request & handle response
Response response = client.newCall(request).execute();
Document document = Jsoup.parse(response.body().string());
Element logoutButton = document.getElementById("logoutbtn"); //Attempt to find logout button
if (logoutButton != null) //If logout button exists, login was successful
{
Report.i(TAG, "Login successful!");
setPersistentCookieSession(); //Store cookies
//Edit SharedPreferences, save session's data
sharedPrefs.edit().putBoolean(LOGGED_IN, true).apply();
sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, false).apply();
sharedPrefs.edit().putString(USERNAME, extractUserName(document)).apply();
String avatar = extractAvatarLink(document);
if (avatar!=null)
{
sharedPrefs.edit().putBoolean(HAS_AVATAR,true).apply();
sharedPrefs.edit().putString(AVATAR_LINK, extractAvatarLink(document)).apply();
}
else
sharedPrefs.edit().putBoolean(HAS_AVATAR,false).apply();
sharedPrefs.edit().putString(LOGOUT_LINK, HttpUrl.parse(logoutButton.attr("href")).toString()).apply();
return SUCCESS;
}
else
{
Report.i(TAG, "Login failed.");
//Investigate login failure
Elements error = document.select("b:contains(That username does not exist.)");
if (error.size() == 1) { //Wrong username
Report.i(TAG, "Wrong Username");
return WRONG_USER;
}
error = document.select("body:contains(Password incorrect)");
if (error.size() == 1) { //Wrong password
Report.i(TAG, "Wrong Password");
return WRONG_PASSWORD;
}
//Other error e.g. session was reset server-side
clearSessionData(); //Clear invalid saved data
return FAILURE;
}
//Handle exception
}
catch (InterruptedIOException e){
Report.i(TAG, "Login InterruptedIOException"); //users cancels LoginTask
return CANCELLED;
}
catch (IOException e) {
Report.w(TAG, "Login IOException", e);
return CONNECTION_ERROR;
}
catch (Exception e) {
Report.w(TAG, "Login Exception (other)", e);
return EXCEPTION;
}
}
/**
* A function that checks the validity of the current saved session (if it exists).
* If isLoggedIn() is true, it will call login() with cookies. On failure, this can only return
* the code FAILURE. CANCELLED, CONNECTION_ERROR and EXCEPTION are simply considered a SUCCESS
* (e.g. no internet connection), at least until a more thorough handling of different
* exceptions is implemented (if considered mandatory).
* Always call it in a separate thread in a way that won't hinder performance (e.g. after
* fragments' data are retrieved).
*/
public void validateSession()
{
Report.i(TAG, "Validating session...");
if(isLoggedIn())
{
int loginResult = login();
if(loginResult != FAILURE)
return;
}
else if(isLoginScreenDefault())
return;
sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, true).apply();
clearSessionData();
}
/**
* Call this function when user explicitly chooses to continue as a guest (UI thread).
*/
public void guestLogin()
{
Report.i("TAG", "Continuing as a guest, as chosen by the user.");
clearSessionData();
sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, false).apply();
}
/**
* Logout function. Always call it in a separate thread.
*/
public int logout()
{
Report.i(TAG, "Logging out...");
Request request = new Request.Builder()
.url(sharedPrefs.getString(LOGOUT_LINK,"LogoutLink"))
.build();
try {
//Make request & handle response
Response response = client.newCall(request).execute();
Document document = Jsoup.parse(response.body().string());
Elements loginButton = document.select("[value=Login]"); //Attempt to find login button
if (!loginButton.isEmpty()) //If login button exists, logout was successful
{
Report.i(TAG, "Logout successful!");
return SUCCESS;
} else {
Report.i(TAG, "Logout failed.");
return FAILURE;
}
} catch (IOException e) {
Report.w(TAG, "Logout IOException", e);
return CONNECTION_ERROR;
} catch (Exception e) {
Report.w(TAG, "Logout Exception", e);
return EXCEPTION;
} finally {
//All data should always be cleared from device regardless the result of logout
clearSessionData();
guestLogin();
}
}
//--------------------------------------AUTH ENDS-----------------------------------------------
//---------------------------------------GETTERS------------------------------------------------
public String getUsername() {
return sharedPrefs.getString(USERNAME, "Username");
}
public String getAvatarLink() {
return sharedPrefs.getString(AVATAR_LINK, "AvatarLink");
}
public boolean hasAvatar() {
return sharedPrefs.getBoolean(HAS_AVATAR, false);
}
public boolean isLoggedIn() {
return sharedPrefs.getBoolean(LOGGED_IN, false);
}
public boolean isLoginScreenDefault() {
return sharedPrefs.getBoolean(LOGIN_SCREEN_AS_DEFAULT, true);
}
//--------------------------------------GETTERS END---------------------------------------------
//------------------------------------OTHER FUNCTIONS-------------------------------------------
private void setPersistentCookieSession()
{
List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl);
if (cookieList.size() == 2)
{
if ((cookieList.get(0).name().equals("THMMYgrC00ki3"))
&& (cookieList.get(1).name().equals("PHPSESSID"))) {
Cookie.Builder builder = new Cookie.Builder();
builder.name(cookieList.get(1).name())
.value(cookieList.get(1).value())
.domain(cookieList.get(1).domain())
.expiresAt(cookieList.get(0).expiresAt());
cookieList.remove(1);
cookieList.add(builder.build());
cookiePersistor.clear();
cookiePersistor.saveAll(cookieList);
}
}
}
private void clearSessionData()
{
cookieJar.clear();
sharedPrefs.edit().clear().apply(); //Clear session data
sharedPrefs.edit().putString(USERNAME, guestName).apply();
sharedPrefs.edit().putBoolean(LOGGED_IN, false).apply(); //User logs out
Report.i(TAG,"Session data cleared.");
}
@Nullable
private String extractUserName(Document doc)
{
if (doc != null) {
Elements user = doc.select("div[id=myuser] > h3");
if (user.size() == 1) {
String txt = user.first().ownText();
Pattern pattern = Pattern.compile(", (.*?),");
Matcher matcher = pattern.matcher(txt);
if (matcher.find())
return matcher.group(1);
}
}
Report.w(TAG,"Extracting username failed!");
return null;
}
@Nullable
private String extractAvatarLink(Document doc)
{
if (doc != null) {
Elements avatar = doc.select("#ava img");
if (avatar.size() == 1) {
return avatar.attr("src");
}
}
Report.w(TAG,"Extracting avatar's link failed!");
return null;
}
//----------------------------------OTHER FUNCTIONS END-----------------------------------------
}

46
app/src/main/java/gr/thmmy/mthmmy/utils/CircleTransform.java

@ -0,0 +1,46 @@
package gr.thmmy.mthmmy.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import com.squareup.picasso.Transformation;
/**
* Used as parameter for PICASSO library's {@link com.squareup.picasso.RequestCreator#transform(Transformation) transform} method.
* @see com.squareup.picasso.Picasso Picasso
*/
public class CircleTransform implements Transformation {
@Override
public Bitmap transform(Bitmap source) {
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
if (squaredBitmap != source) {
source.recycle();
}
Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
paint.setShader(shader);
paint.setAntiAlias(true);
float r = size / 2f;
canvas.drawCircle(r, r, r, paint);
squaredBitmap.recycle();
return bitmap;
}
@Override
public String key() {
return "circle";
}
}

54
app/src/main/java/gr/thmmy/mthmmy/utils/CustomRecyclerView.java

@ -0,0 +1,54 @@
package gr.thmmy.mthmmy.utils;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
//Custom RecyclerView, so EdgeEffect and SwipeRefresh both work
public class CustomRecyclerView extends RecyclerView {
private volatile boolean enableRefreshing = true;
public CustomRecyclerView(Context context) {
super(context);
}
public CustomRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onScrolled(int dx, int dy) {
if (dy > 0)
enableRefreshing = false;
super.onScrolled(dx, dy);
}
@Override
public void onScrollStateChanged(int state) {
if ((state != SCROLL_STATE_DRAGGING) && ((LinearLayoutManager) getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0)
enableRefreshing = true;
else if (getChildCount() == 0)
enableRefreshing = true;
else if (((LinearLayoutManager) getLayoutManager()).findFirstCompletelyVisibleItemPosition() != 0)
enableRefreshing = false;
super.onScrollStateChanged(state);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (enableRefreshing)
return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
else
return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, 0, offsetInWindow);
}
}

163
app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java

@ -0,0 +1,163 @@
package gr.thmmy.mthmmy.utils.FileManager;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Objects;
import mthmmy.utils.Report;
import okhttp3.Request;
import okhttp3.Response;
import static gr.thmmy.mthmmy.activities.base.BaseActivity.getClient;
/**
* Used for downloading and storing a file from the forum using {@link okhttp3}.
* <p>Class has one constructor: <ul><li>{@link #ThmmyFile(URL, String, String)}</li></ul>
* and the methods:<ul><li>getters</li>
* <li>{@link #download()}</li>
* <li>{@link #download()}</li></ul></p>
*/
public class ThmmyFile {
/**
* Debug Tag for logging debug output to LogCat
*/
private static final String TAG = "ThmmyFile";
private final URL fileUrl;
private final String filename, fileInfo;
private String extension, filePath;
private File file;
/**
* This constructor only creates a ThmmyFile object and <b>does not download</b> the file. To download
* the file use {@link #download()} or {@link #download()}!
*
* @param fileUrl {@link URL} object with file's url
* @param filename {@link String} with desired file name
* @param fileInfo {@link String} with any extra information (like number of downloads)
*/
public ThmmyFile(URL fileUrl, String filename, String fileInfo) {
this.fileUrl = fileUrl;
this.filename = filename;
this.fileInfo = fileInfo;
this.extension = null;
this.filePath = null;
this.file = null;
}
public URL getFileUrl() {
return fileUrl;
}
public String getFilename() {
return filename;
}
public String getFileInfo() {
return fileInfo;
}
/**
* This is null until {@link #download()} or {@link #download()} is called and has succeeded.
*
* @return String with file's extension or null
*/
@Nullable
public String getExtension() {
return extension;
}
/**
* This is null until {@link #download()} or {@link #download()} is called and has succeeded.
*
* @return String with file's path or null
*/
@Nullable
public String getFilePath() {
return filePath;
}
/**
* This is null until {@link #download()} or {@link #download()} is called and has succeeded.
*
* @return {@link File} or null
*/
@Nullable
public File getFile() {
return file;
}
private void setExtension(String extension) {
this.extension = extension;
}
private void setFilePath(String filePath) {
this.filePath = filePath;
}
/**
* Used to download the file. If download is successful file's extension and path will be assigned
* to object's fields and can be accessed using getter methods.
* <p>File is stored in sdcard1/Android/data/Downloads/packageName</p>
*
* @return the {@link File} if successful, null otherwise
* @throws IOException if the request could not be executed due to cancellation, a connectivity
* problem or timeout. Because networks can fail during an exchange, it is possible that the
* remote server accepted the request before the failure.
* @throws SecurityException if the requested file is not hosted by the forum.
*/
@Nullable
public File download() throws IOException, SecurityException {
if (!Objects.equals(fileUrl.getHost(), "www.thmmy.gr"))
throw new SecurityException("Downloading files from other sources is not supported");
Request request = new Request.Builder().url(fileUrl).build();
Response response = getClient().newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Failed to download file: " + response);
}
file = getOutputMediaFile(filename);
if (file == null) {
Report.d(TAG, "Error creating media file, check storage permissions!");
} else {
FileOutputStream fos = new FileOutputStream(file);
fos.write(response.body().bytes());
fos.close();
filePath = file.getAbsolutePath();
extension = MimeTypeMap.getFileExtensionFromUrl(
filePath.substring(filePath.lastIndexOf("/")));
}
return file;
}
@Nullable
private static File getOutputMediaFile(String fileName) {
// To be safe, you should check that the SDCard is mounted
// using Environment.getExternalStorageState() before doing this.
File mediaStorageDir = new File(Environment.getExternalStorageDirectory()
+ "/Android/data/gr.thmmy.mthmmy/"
+ "Downloads/");
// This location works best if you want the created files to be shared
// between applications and persist after your app has been uninstalled.
// Create the storage directory if it does not exist
if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdirs()) {
Report.d(TAG, "problem!");
return null;
}
}
// Create a media file name
File mediaFile;
mediaFile = new File(mediaStorageDir.getPath() + File.separator + fileName);
return mediaFile;
}
}

165
app/src/main/java/gr/thmmy/mthmmy/utils/ParseHelpers.java

@ -0,0 +1,165 @@
package gr.thmmy.mthmmy.utils;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
/**
* This class consists exclusively of static classes (enums) and methods (excluding methods of inner
* classes). It can be used to resolve a page's language and state or fix embedded videos html code.
*/
public class ParseHelpers {
/**
* Debug Tag for logging debug output to LogCat
*/
@SuppressWarnings("unused")
private static final String TAG = "ParseHelpers";
/**
* An enum describing a forum page's language by defining the types:<ul>
* <li>{@link #PAGE_INCOMPLETE}</li>
* <li>{@link #UNDEFINED_LANGUAGE}</li>
* <li>{@link #ENGLISH}</li>
* <li>{@link #ENGLISH_GUEST}</li>
* <li>{@link #GREEK}</li>
* </ul>
*/
public enum Language {
/**
* Page language is greek.
*/
GREEK,
/**
* Page language is english.
*/
ENGLISH,
/**
* Page language is english and the user is guest.
*/
ENGLISH_GUEST,
/**
* Page is incomplete. Data are not enough to determine the language.
*/
PAGE_INCOMPLETE,
/**
* Page language is not (yet) supported.
*/
UNDEFINED_LANGUAGE;
/**
* Returns one of the supported forum languages.
*
* @param page {@link Document} object containing this page's source code
* @return language of this page
* @see org.jsoup.Jsoup Jsoup
*/
public static Language getLanguage(Document page) {
Element welcoming = page.select("h3").first();
if (welcoming == null) {
Element welcomingGuest = page.select("div[id=myuser]").first();
if (welcomingGuest != null) {
if (welcomingGuest.text().contains("Welcome")) return ENGLISH_GUEST;
}
return PAGE_INCOMPLETE;
} else if (welcoming.text().contains("Καλώς ορίσατε")) return GREEK;
else if (welcoming.text().contains("Hey")) return ENGLISH;
else return UNDEFINED_LANGUAGE;
}
/**
* This method defines a custom equality check for {@link Language} enums.
* <p>Method returns true if parameter's Target is the same as the object and in the specific
* cases described below, false otherwise.</p><ul>
* <li>{@link #ENGLISH}.is({@link #ENGLISH_GUEST}) returns true</li>
* <li>{@link #ENGLISH_GUEST}.is({@link #ENGLISH}) returns true</li>
*
* @param other another Language
* @return true if <b>enums</b> are equal, false otherwise
*/
public boolean is(Language other) {
return this == ENGLISH && other == ENGLISH_GUEST
|| this == ENGLISH_GUEST && other == ENGLISH
|| this == other;
}
}
/**
* An enum describing the state of a forum page by defining the types:<ul>
* <li>{@link #UNAUTHORIZED_OR_MISSING}</li>
* <li>{@link #NEW_PM}</li>
* <li>{@link #READY}</li>
* </ul>
*/
public enum State {
/**
* This page is either missing or is off limits.
*/
UNAUTHORIZED_OR_MISSING,
/**
* The page has a popup window from a new personal message.
*/
NEW_PM,
/**
* The page is ready for use.
*/
READY;
/**
* This method checks the state of a page.
*
* @param page a {@link Document} containing this page's source code
* @return page's State
*/
public static State getState(Document page) {
Elements warnings = page.select("form[id=frmLogin] tr.catbg~tr>td.windowbg");
if (warnings != null) {
for (Element warning : warnings) {
if (warning.text().contains("The topic or board you are looking for appears " +
"to be either missing or off limits to you."))
return UNAUTHORIZED_OR_MISSING;
}
}
return READY;
}
}
/**
* This method fixes html so that embedded videos will render properly and be lightweight.
*
* @param html an {@link Element} containing the (outer) html to be fixed
* @return fixed html String
*/
public static String youtubeEmbeddedFix(Element html) {
//Fixes embedded videos
Elements noembedTag = html.select("noembed");
ArrayList<String> embededVideosUrls = new ArrayList<>();
for (Element _noembed : noembedTag) {
embededVideosUrls.add(_noembed.text().substring(_noembed.text()
.indexOf("href=\"https://www.youtube.com/watch?") + 38
, _noembed.text().indexOf("target") - 2));
}
String fixed = html.outerHtml();
int tmp_counter = 0;
while (fixed.contains("<embed")) {
if (tmp_counter > embededVideosUrls.size())
break;
fixed = fixed.replace(
fixed.substring(fixed.indexOf("<embed"), fixed.indexOf("/noembed>") + 9)
, "<div class=\"yt\">"
+ "<a href=\"https://www.youtube.com/"
+ embededVideosUrls.get(tmp_counter) + "\" target=\"_blank\">"
+ "<img class=\"embedded-video-play\" "
+ "src=\"http://www.youtube.com/yt/brand/media/image/YouTube_light_color_icon.png\""
+ "</a>"
+ "<img src=\"https://img.youtube.com/vi/"
+ embededVideosUrls.get(tmp_counter)
+ "/default.jpg\" alt=\"\" border=\"0\" width=\"40%\">"
+ "</div>");
}
return fixed;
}
}

45
app/src/main/java/gr/thmmy/mthmmy/utils/ScrollAwareFABBehavior.java

@ -0,0 +1,45 @@
package gr.thmmy.mthmmy.utils;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
/**
* Extends FloatingActionButton's behavior so the button will hide when scrolling down and show
* otherwise.
*/
@SuppressWarnings("WeakerAccess")
public class ScrollAwareFABBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
@SuppressWarnings("UnusedParameters")
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
super();
}
@Override
public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
final View directTargetChild, final View target, final int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
@Override
public void onNestedScroll(final CoordinatorLayout coordinatorLayout,
final FloatingActionButton child,
final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,dxUnconsumed, dyUnconsumed);
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
child.hide(new FloatingActionButton.OnVisibilityChangedListener() {
@Override
public void onHidden(FloatingActionButton fab) {
super.onHidden(fab);
fab.setVisibility(View.INVISIBLE);
}
});
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
child.show();
}
}
}

105
app/src/main/java/gr/thmmy/mthmmy/utils/ScrollAwareLinearBehavior.java

@ -0,0 +1,105 @@
package gr.thmmy.mthmmy.utils;
import android.animation.Animator;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewPropertyAnimator;
/**
* Extends LinearLayout's behavior. Used for bottom navigation bar.
* <p>When a nested ScrollView is scrolled down, the view will disappear.
* When the ScrollView is scrolled back up, the view will reappear.</p>
*/
@SuppressWarnings("WeakerAccess")
public class ScrollAwareLinearBehavior extends CoordinatorLayout.Behavior<View> {
private static final int ANIMATION_DURATION = 100;
public ScrollAwareLinearBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScroll (CoordinatorLayout coordinatorLayout, View bottomNavBar, View target,
int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed){
super.onNestedScroll(coordinatorLayout, bottomNavBar, target, dxConsumed, dyConsumed,dxUnconsumed, dyUnconsumed);
if (dyConsumed > 0 && bottomNavBar.getVisibility() == View.VISIBLE) {
hide(bottomNavBar);
} else if (dyConsumed < 0 && bottomNavBar.getVisibility() != View.VISIBLE) {
show(bottomNavBar);
}
}
/**
* Animates the hiding of a bottom navigation bar.
* @param bottomNavBar bottom navigation bar View
*/
private void hide(final View bottomNavBar) {
ViewPropertyAnimator animator = bottomNavBar.animate()
.translationY(bottomNavBar.getHeight())
.setInterpolator(new FastOutSlowInInterpolator())
.setDuration(ANIMATION_DURATION);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
bottomNavBar.setVisibility(View.INVISIBLE);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
/**
* Animates the showing of a bottom navigation bar.
* @param bottomNavBar bottom navigation bar View
*/
private void show(final View bottomNavBar) {
ViewPropertyAnimator animator = bottomNavBar.animate()
.translationY(0)
.setInterpolator(new FastOutSlowInInterpolator())
.setDuration(ANIMATION_DURATION);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator){
bottomNavBar.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
}

8
app/src/main/res/anim/push_left_in.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="100%p"
android:toXDelta="0"/>
</set>

7
app/src/main/res/anim/push_left_out.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0"
android:toXDelta="-100%p"/>
</set>

8
app/src/main/res/anim/push_right_in.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="-100%p"
android:toXDelta="0"/>
</set>

8
app/src/main/res/anim/push_right_out.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0"
android:toXDelta="100%p"/>
</set>

BIN
app/src/main/res/drawable-hdpi/ic_arrow_drop_down.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

BIN
app/src/main/res/drawable-hdpi/ic_arrow_drop_up.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

BIN
app/src/main/res/drawable-hdpi/ic_format_quote_checked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

BIN
app/src/main/res/drawable-hdpi/ic_format_quote_unchecked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

BIN
app/src/main/res/drawable-hdpi/ic_pin.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

BIN
app/src/main/res/drawable-mdpi/ic_arrow_drop_down.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

BIN
app/src/main/res/drawable-mdpi/ic_arrow_drop_up.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

BIN
app/src/main/res/drawable-mdpi/ic_format_quote_checked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

BIN
app/src/main/res/drawable-mdpi/ic_format_quote_unchecked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

BIN
app/src/main/res/drawable-mdpi/ic_pin.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

BIN
app/src/main/res/drawable-xhdpi/ic_arrow_drop_down.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

BIN
app/src/main/res/drawable-xhdpi/ic_arrow_drop_up.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

BIN
app/src/main/res/drawable-xhdpi/ic_format_quote_checked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

BIN
app/src/main/res/drawable-xhdpi/ic_format_quote_unchecked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

BIN
app/src/main/res/drawable-xhdpi/ic_pin.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

BIN
app/src/main/res/drawable-xxhdpi/ic_arrow_drop_down.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

BIN
app/src/main/res/drawable-xxhdpi/ic_arrow_drop_up.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

BIN
app/src/main/res/drawable-xxhdpi/ic_format_quote_checked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

BIN
app/src/main/res/drawable-xxhdpi/ic_format_quote_unchecked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

BIN
app/src/main/res/drawable-xxhdpi/ic_pin.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

BIN
app/src/main/res/drawable-xxxhdpi/ic_arrow_drop_down.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

BIN
app/src/main/res/drawable-xxxhdpi/ic_arrow_drop_up.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

BIN
app/src/main/res/drawable-xxxhdpi/ic_format_quote_checked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

BIN
app/src/main/res/drawable-xxxhdpi/ic_format_quote_unchecked.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

BIN
app/src/main/res/drawable-xxxhdpi/ic_pin.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

BIN
app/src/main/res/drawable/fun.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

13
app/src/main/res/drawable/guest_button_border_bg.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@null"/>
<stroke
android:width="1dip"
android:color="@color/accent"/>
<corners android:radius="5dip"/>
<padding
android:bottom="0dip"
android:left="0dip"
android:right="0dip"
android:top="0dip"/>
</shape>

9
app/src/main/res/drawable/ic_add_fab.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

BIN
app/src/main/res/drawable/ic_default_user_thumbnail.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

9
app/src/main/res/drawable/ic_pm_fab.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M21.99,4c0,-1.1 -0.89,-2 -1.99,-2L4,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4 -0.01,-18zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</vector>

10
app/src/main/res/drawable/line_chart_gradient.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:endColor="@color/accent"
android:startColor="@color/background"
android:type="linear"/>
</shape>

10
app/src/main/res/drawable/login_button_bg.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/accent"/>
<corners android:radius="5dip"/>
<padding
android:bottom="0dip"
android:left="0dip"
android:right="0dip"
android:top="0dip"/>
</shape>

BIN
app/src/main/res/drawable/logo_animated.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

13
app/src/main/res/drawable/member_of_the_month_card.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/card_background"/>
<stroke
android:width="1dip"
android:color="@color/member_of_the_month"/>
<corners android:radius="5dip"/>
<padding
android:bottom="0dip"
android:left="0dip"
android:right="0dip"
android:top="0dip"/>
</shape>

8
app/src/main/res/drawable/page_first.xml

@ -0,0 +1,8 @@
<!-- drawable/page_first.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path android:fillColor="#FFF" android:pathData="M18.41,16.59L13.82,12L18.41,7.41L17,6L11,12L17,18L18.41,16.59M6,6H8V18H6V6Z" />
</vector>

8
app/src/main/res/drawable/page_last.xml

@ -0,0 +1,8 @@
<!-- drawable/page_last.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path android:fillColor="#FFF" android:pathData="M5.59,7.41L10.18,12L5.59,16.59L7,18L13,12L7,6L5.59,7.41M16,6H18V18H16V6Z" />
</vector>

8
app/src/main/res/drawable/page_next.xml

@ -0,0 +1,8 @@
<!-- drawable/page_next.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path android:fillColor="#FFF" android:pathData="M16,18H18V6H16M6,18L14.5,12L6,6V18Z" />
</vector>

8
app/src/main/res/drawable/page_previous.xml

@ -0,0 +1,8 @@
<!-- drawable/page_previous.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path android:fillColor="#FFF" android:pathData="M6,18V6H8V18H6M9.5,12L18,6V18L9.5,12Z" />
</vector>

24
app/src/main/res/drawable/progress_bar_bg.xml

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360">
<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:thicknessRatio="8"
android:useLevel="false">
<size
android:width="76dip"
android:height="76dip"/>
<gradient
android:angle="0"
android:endColor="@color/accent"
android:startColor="@color/accent"
android:type="sweep"
android:useLevel="false"
/>
</shape>
</rotate>

119
app/src/main/res/layout-v21/activity_profile.xml

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activities.profile.ProfileActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ToolbarTheme">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/main_collapsing"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/user_thumbnail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
android:fitsSystemWindows="true"
android:src="@drawable/ic_default_user_thumbnail"
android:transitionName="user_thumbnail"
app:layout_collapseMode="parallax"/>
<TextView
android:id="@+id/profile_activity_personal_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@color/primary_text"
android:visibility="gone"/>
</LinearLayout>
</android.support.design.widget.CollapsingToolbarLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:gravity="center"
app:popupTheme="@style/ToolbarTheme">
<TextView
android:id="@+id/profile_activity_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/username"
android:textColor="@color/accent"
android:textSize="25sp"/>
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/profile_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:tabGravity="fill"
app:tabMode="fixed"
app:tabSelectedTextColor="@color/accent"
app:tabTextColor="@color/white"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/profile_tab_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top|start"
android:background="@color/background"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|center"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progressBar"
style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
app:layout_anchor="@id/profile_tab_container"
app:layout_anchorGravity="top|center"
app:mpb_indeterminateTint="@color/accent"
app:mpb_progressStyle="horizontal"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/profile_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_pm_fab"/>
</android.support.design.widget.CoordinatorLayout>

249
app/src/main/res/layout-v21/activity_topic_post_row.xml

@ -0,0 +1,249 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingEnd="4dp"
android:paddingStart="4dp"
tools:ignore="SmallSp">
<FrameLayout
android:id="@+id/post_date_and_number_exp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:animateLayoutChanges="true"
android:visibility="gone">
<TextView
android:id="@+id/post_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:paddingEnd="5dp"
android:paddingStart="5dp"
android:text=""
android:textColor="@color/card_expand_text_color"
android:textSize="8sp"
/>
<TextView
android:id="@+id/post_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:paddingEnd="5dp"
android:paddingStart="5dp"
android:text=""
android:textColor="@color/card_expand_text_color"
android:textSize="8sp"
/>
</FrameLayout>
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:foreground="?android:attr/selectableItemBackground"
card_view:cardBackgroundColor="@color/card_background"
card_view:cardCornerRadius="5dp"
card_view:cardElevation="2dp"
card_view:cardPreventCornerOverlap="false"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:id="@+id/card_child_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp">
<FrameLayout
android:id="@+id/thumbnail_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
android:maxHeight="@dimen/thumbnail_size"
android:maxWidth="@dimen/thumbnail_size"
android:src="@drawable/ic_default_user_thumbnail"
android:transitionName="user_thumbnail"/>
</FrameLayout>
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/thumbnail_holder"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/post_author"
android:textColor="@color/primary_text"
android:textStyle="bold"/>
<TextView
android:id="@+id/subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/username"
android:layout_toEndOf="@+id/thumbnail_holder"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/post_subject"
/>
</RelativeLayout>
<ImageButton
android:id="@+id/toggle_quote_button"
android:layout_width="@dimen/quote_button"
android:layout_height="@dimen/quote_button"
android:layout_marginEnd="9dp"
android:layout_marginTop="9dp"
android:background="@color/card_background"
android:clickable="true"
android:contentDescription="@string/post_quote_button"
android:src="@drawable/ic_format_quote_unchecked"/>
</LinearLayout>
<LinearLayout
android:id="@+id/user_extra_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="3dp"
android:visibility="gone"
android:weightSum="1.0">
<TextView
android:id="@+id/special_rank"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone"/>
<TextView
android:id="@+id/rank"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone"/>
<TextView
android:id="@+id/stars"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="10sp"
android:visibility="gone">
</TextView>
<TextView
android:id="@+id/gender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone"/>
<TextView
android:id="@+id/number_of_posts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone"/>
<TextView
android:id="@+id/personal_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:textStyle="italic"
android:visibility="gone"/>
</LinearLayout>
<View
android:id="@+id/header_body_devider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="9dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:background="@color/divider"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp">
<WebView
android:id="@+id/post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:background="@color/card_background"
android:clickable="true"
android:text="@string/post"
/>
</FrameLayout>
<View
android:id="@+id/body_footer_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="9dp"
android:background="@color/divider"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/post_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="9dp"
android:paddingLeft="16dp"
android:paddingRight="16dp">
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>

158
app/src/main/res/layout/activity_about.xml

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activities.AboutActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ToolbarTheme">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ToolbarTheme">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<ScrollView
android:id="@+id/scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="64dp"
android:background="@color/background">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.eternalpixels.toinfinity.Info">
<pl.droidsonroids.gif.GifImageView
android:id="@+id/logoView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:contentDescription="@string/logo"
android:src="@drawable/logo_animated"
/>
<TextView
android:id="@+id/version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/logoView"
android:layout_centerHorizontal="true"
android:clickable="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/accent"
android:textStyle="italic"/>
<TextView
android:id="@+id/header_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/libraries"
android:textColor="@color/accent"
android:layout_below="@+id/version"
android:layout_alignParentStart="true"
android:textStyle="bold" />
<TextView
android:id="@+id/libraries_text"
android:layout_below="@+id/header_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/libraries_text"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/iron"
android:layout_alignParentStart="true" />
<TextView
android:layout_below="@+id/libraries_text"
android:onClick="displayApacheLibraries"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/apache_v2_0_libraries"
android:id="@+id/apache_libs"
android:layout_alignParentStart="true"
android:textColor="@color/accent" />
<TextView
android:id="@+id/mit_libs"
android:onClick="displayMITLibraries"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/the_mit_libraries"
android:layout_below="@+id/apache_libs"
android:layout_alignParentStart="true"
android:textColor="@color/accent" />
<TextView
android:id="@+id/header_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/contact"
android:textColor="@color/accent"
android:layout_below="@+id/mit_libs"
android:layout_alignParentStart="true"
android:textStyle="bold" />
<TextView
android:id="@+id/contact_text"
android:layout_below="@+id/header_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="email|web"
android:text="@string/contact_text"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/iron"
android:layout_alignParentStart="true" />
</RelativeLayout>
</ScrollView>
<FrameLayout
android:id="@+id/trollPicFrame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone">
<ImageView
android:id="@+id/trollPic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/trollPic"
android:foregroundGravity="center"
android:src="@drawable/fun"
/>
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>

61
app/src/main/res/layout/activity_board.xml

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activities.topic.TopicActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ToolbarTheme">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ToolbarTheme">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/board_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top|start"
android:layout_marginTop="64dp"
android:background="@color/background"
android:scrollbars="none"
tools:context="gr.thmmy.mthmmy.activities.topic.TopicActivity">
</android.support.v7.widget.RecyclerView>
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progressBar"
style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|center"
app:mpb_indeterminateTint="@color/accent"
app:mpb_progressStyle="horizontal"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/board_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_add_fab"/>
</android.support.design.widget.CoordinatorLayout>

79
app/src/main/res/layout/activity_board_sub_board.xml

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/child_board_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/card_background"
android:clickable="true"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:orientation="horizontal">
<TextView
android:id="@+id/child_board_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/child_board_title"
android:textColor="@color/accent"
android:textSize="22sp"/>
<ImageButton
android:id="@+id/child_board_expand_collapse_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/card_background"
android:contentDescription="@string/child_board_button"
android:src="@drawable/ic_arrow_drop_down"/>
</LinearLayout>
<LinearLayout
android:id="@+id/child_board_expandable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/child_board_mods"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:layout_marginTop="1dp"
android:text="@string/child_board_mods"
android:textColor="@color/secondary_text"
android:textSize="12sp"
android:textStyle="italic"/>
<TextView
android:id="@+id/child_board_stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:layout_marginTop="1dp"
android:text="@string/child_board_stats"
android:textColor="@color/secondary_text"
android:textSize="12sp"/>
<TextView
android:id="@+id/child_board_last_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:layout_marginTop="1dp"
android:clickable="true"
android:text="@string/child_board_last_post"
android:textColor="@color/primary_text"
android:textSize="12sp"/>
</LinearLayout>
</LinearLayout>

77
app/src/main/res/layout/activity_board_topic.xml

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/topic_row_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:orientation="horizontal">
<TextView
android:id="@+id/topic_subject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/topic_subject"
android:textColor="@color/primary_text"
android:textSize="18sp"/>
<ImageButton
android:id="@+id/topic_expand_collapse_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/background"
android:contentDescription="@string/child_board_button"
android:src="@drawable/ic_arrow_drop_down"/>
</LinearLayout>
<LinearLayout
android:id="@+id/topic_expandable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/topic_started_by"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:layout_marginTop="1dp"
android:text="@string/topic_started_by"
android:textColor="@color/secondary_text"
android:textSize="12sp"/>
<TextView
android:id="@+id/topic_stats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:layout_marginTop="1dp"
android:text="@string/topic_stats"
android:textColor="@color/secondary_text"
android:textSize="12sp"/>
<TextView
android:id="@+id/topic_last_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:layout_marginTop="1dp"
android:clickable="true"
android:text="@string/topic_last_post"
android:textColor="@color/primary_text"
android:textSize="12sp"/>
</LinearLayout>
</LinearLayout>

134
app/src/main/res/layout/activity_login.xml

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@color/background"
android:fitsSystemWindows="true">
<ScrollView
android:id="@+id/inner_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:focusableInTouchMode="true"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingRight="24dp">
<Space
android:layout_width="match_parent"
android:layout_height="100dp"/>
<pl.droidsonroids.gif.GifImageView
android:id="@+id/logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:contentDescription="@string/thmmy_img_description"
android:src="@drawable/logo_animated"
/>
<Space
android:layout_width="match_parent"
android:layout_height="50dp"/>
<!-- Username Label -->
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp">
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
android:inputType="textPersonName"/>
</android.support.design.widget.TextInputLayout>
<!-- Password Label -->
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
app:passwordToggleEnabled="true">
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_password"
android:inputType="textPassword"/>
</android.support.design.widget.TextInputLayout>
<!-- Login Button -->
<android.support.v7.widget.AppCompatButton
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:layout_marginTop="30dp"
android:background="@drawable/login_button_bg"
android:padding="12dp"
android:text="@string/btn_login"
android:textSize="18sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="center"
android:text="@string/login_or_guest"
android:textAllCaps="false"
android:textColor="@color/secondary_text"
android:textSize="10sp"
tools:ignore="SmallSp"/>
<android.support.v7.widget.AppCompatButton
android:id="@+id/btnContinueAsGuest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:layout_marginTop="30dip"
android:background="@drawable/guest_button_border_bg"
android:text="@string/btn_continue_as_guest"
android:textAllCaps="false"
android:textSize="15sp"/>
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/login_progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible">
<ProgressBar
style="?android:attr/progressBarStyleInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/progress_bar_bg"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:text="@string/login_spinner"
android:textColor="@color/accent"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
</RelativeLayout>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save