Browse Source

Version 1.8.0

master v1.8.0
Ezerous 5 years ago
parent
commit
251f6c5fa9
No known key found for this signature in database GPG Key ID: 262B2954BBA319E3
  1. 2
      README.md
  2. 39
      app/build.gradle
  3. 9
      app/proguard-rules.pro
  4. 7
      app/src/main/AndroidManifest.xml
  5. 54
      app/src/main/assets/apache_libraries.html
  6. 229
      app/src/main/assets/epl_libraries.html
  7. 28
      app/src/main/assets/libraries_style.css
  8. 42
      app/src/main/assets/mit_libraries.html
  9. 114
      app/src/main/assets/other_libraries.html
  10. 8
      app/src/main/assets/style.css
  11. BIN
      app/src/main/ic_launcher-playstore.png
  12. 51
      app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java
  13. 30
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java
  14. 8
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java
  15. 40
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksActivity.java
  16. 114
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksFragment.java
  17. 158
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksTopicFragment.java
  18. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/create_content/CreateContentActivity.java
  19. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsActivity.java
  20. 17
      app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java
  21. 15
      app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java
  22. 38
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java
  23. 26
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java
  24. 101
      app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadAdapter.java
  25. 276
      app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadFragment.java
  26. 54
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java
  27. 11
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java
  28. 17
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java
  29. 3
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java
  30. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java
  31. 1
      app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsActivity.java
  32. 21
      app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsFragment.java
  33. 5
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutAdapter.java
  34. 12
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxFragment.java
  35. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxTask.java
  36. 63
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java
  37. 38
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java
  38. 11
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java
  39. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditTask.java
  40. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReplyTask.java
  41. 27
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java
  42. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadActivity.java
  43. 6
      app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadFieldsBuilderActivity.java
  44. 69
      app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java
  45. 50
      app/src/main/java/gr/thmmy/mthmmy/base/BaseApplication.java
  46. 21
      app/src/main/java/gr/thmmy/mthmmy/model/ThmmyFile.java
  47. 39
      app/src/main/java/gr/thmmy/mthmmy/model/Topic.java
  48. 55
      app/src/main/java/gr/thmmy/mthmmy/model/TopicSummary.java
  49. 5
      app/src/main/java/gr/thmmy/mthmmy/services/DownloadHelper.java
  50. 113
      app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java
  51. 18
      app/src/main/java/gr/thmmy/mthmmy/session/ValidateSessionTask.java
  52. 47
      app/src/main/java/gr/thmmy/mthmmy/utils/CircleTransform.java
  53. 73
      app/src/main/java/gr/thmmy/mthmmy/utils/DateTimeUtils.java
  54. 9
      app/src/main/java/gr/thmmy/mthmmy/utils/NetworkTask.java
  55. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReporter.java
  56. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReportingTree.java
  57. 39
      app/src/main/java/gr/thmmy/mthmmy/utils/io/AssetUtils.java
  58. 54
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java
  59. 5
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseTask.java
  60. 112
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParser.java
  61. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/CenterVerticalSpan.java
  62. 65
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/ImageDownloadDialogBuilder.java
  63. 63
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/PhotoViewUtils.java
  64. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/ScrollAwareFABBehavior.java
  65. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/ScrollAwareLinearBehavior.java
  66. 2
      app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java
  67. 2
      app/src/main/java/gr/thmmy/mthmmy/views/AppCompatSpinnerWithoutDefault.java
  68. 2
      app/src/main/java/gr/thmmy/mthmmy/views/CustomLinearLayoutManager.java
  69. 2
      app/src/main/java/gr/thmmy/mthmmy/views/CustomRecyclerView.java
  70. 93
      app/src/main/java/gr/thmmy/mthmmy/views/ReactiveWebView.java
  71. 232
      app/src/main/java/gr/thmmy/mthmmy/views/RelativeTimeTextView.java
  72. 2
      app/src/main/java/gr/thmmy/mthmmy/views/ToggledBackgroundButton.java
  73. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/EditorView.java
  74. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiInputField.java
  75. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiKeyboard.java
  76. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiKeyboardAdapter.java
  77. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/FormatButtonsAdapter.java
  78. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/IEmojiKeyboard.java
  79. 5
      app/src/main/res/drawable/ic_file_not_found.xml
  80. 26
      app/src/main/res/drawable/ic_launcher_foreground.xml
  81. 5
      app/src/main/res/drawable/ic_mark_as_read.xml
  82. 9
      app/src/main/res/layout-v21/activity_profile.xml
  83. 2
      app/src/main/res/layout-v21/activity_topic_post_row.xml
  84. 30
      app/src/main/res/layout/activity_about.xml
  85. 2
      app/src/main/res/layout/activity_board.xml
  86. 4
      app/src/main/res/layout/activity_create_content.xml
  87. 2
      app/src/main/res/layout/activity_downloads.xml
  88. 9
      app/src/main/res/layout/activity_profile.xml
  89. 6
      app/src/main/res/layout/activity_topic.xml
  90. 2
      app/src/main/res/layout/activity_topic_edit_row.xml
  91. 2
      app/src/main/res/layout/activity_topic_post_row.xml
  92. 2
      app/src/main/res/layout/activity_topic_quick_reply_row.xml
  93. 6
      app/src/main/res/layout/activity_upload.xml
  94. 15
      app/src/main/res/layout/fragment_bookmarks.xml
  95. 2
      app/src/main/res/layout/fragment_forum.xml
  96. 2
      app/src/main/res/layout/fragment_latest_posts_row.xml
  97. 2
      app/src/main/res/layout/fragment_recent.xml
  98. 2
      app/src/main/res/layout/fragment_recent_row.xml
  99. 7
      app/src/main/res/layout/fragment_shoutbox.xml
  100. 2
      app/src/main/res/layout/fragment_shoutbox_shout_row.xml

2
README.md

@ -4,7 +4,7 @@
[![Discord Channel](https://img.shields.io/badge/discord-public@mTHMMY-738bd7.svg?style=flat)][discord-server]
![Last Commit](https://img.shields.io/github/last-commit/ThmmyNoLife/mTHMMY/develop.svg?style=flat)
![mTHMMY logo](app/src/main/res/mipmap-xhdpi/ic_launcher.png)
![mTHMMY logo](app/src/main/res/mipmap-xhdpi/ic_launcher_round.png)
A mobile app for [thmmy.gr](https://www.thmmy.gr).

39
app/build.gradle

@ -7,15 +7,15 @@ apply plugin: 'io.fabric'
android {
compileSdkVersion 29
buildToolsVersion = '29.0.2'
buildToolsVersion = '29.0.3'
defaultConfig {
vectorDrawables.useSupportLibrary = true
applicationId "gr.thmmy.mthmmy"
minSdkVersion 19
targetSdkVersion 29
versionCode 22
versionName "1.7.4"
versionCode 23
versionName "1.8.0"
archivesBaseName = "mTHMMY-v$versionName"
buildConfigField "String", "CURRENT_BRANCH", "\"" + getCurrentBranch() + "\""
buildConfigField "String", "COMMIT_HASH", "\"" + getCommitHash() + "\""
@ -29,6 +29,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
multiDexEnabled true
def date = new Date().format('ddMMyy_HHmmss')
archivesBaseName = archivesBaseName + "-$date"
// Disable fabric build ID generation for debug builds
@ -72,37 +73,45 @@ tasks.whenTaskAdded { task ->
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":emojis")
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha04'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.exifinterface:exifinterface:1.1.0-beta01'
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.firebase:firebase-core:17.0.0'
implementation 'com.google.firebase:firebase-messaging:19.0.1'
implementation 'androidx.exifinterface:exifinterface:1.2.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.firebase:firebase-analytics:17.4.2'
implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.snatik:storage:2.1.0'
implementation 'com.squareup.okhttp3:okhttp:3.12.0' //TODO: Warning: okhttp has dropped support for Android v.19 since okhttp 3.13!
implementation 'com.squareup.picasso:picasso:2.5.2'
implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
implementation 'com.squareup.okhttp3:okhttp:3.12.12' //TODO: Warning: okhttp has dropped support for Android v.19 since okhttp 3.13!
implementation 'org.jsoup:jsoup:1.10.3' //TODO: Warning: upgrading from 1.10.3 will break stuff!
implementation 'joda-time:joda-time:2.10.4'
implementation 'com.github.franmontiel:PersistentCookieJar:1.0.1'
implementation 'com.github.PhilJay:MPAndroidChart:3.0.3'
implementation 'com.mikepenz:materialdrawer:6.1.1'
implementation 'com.mikepenz:fontawesome-typeface:4.7.0.0@aar'
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.19'
implementation 'com.bignerdranch.android:expandablerecyclerview:3.0.0-RC1'//TODO: deprecated!
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'me.zhanghai.android.materialprogressbar:library:1.4.2'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'ru.noties:markwon:2.0.2'
implementation 'net.gotev:uploadservice:3.5.2'
implementation 'net.gotev:uploadservice-okhttp:3.4.2' //TODO: Warning: v.3.5 depends on okhttp 3.13!
implementation 'com.itkacher.okhttpprofiler:okhttpprofiler:1.0.5' //Plugin: https://plugins.jetbrains.com/plugin/11249-okhttp-profiler
implementation 'com.itkacher.okhttpprofiler:okhttpprofiler:1.0.7' //Plugin: https://plugins.jetbrains.com/plugin/11249-okhttp-profiler
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.powermock:powermock-core:2.0.2'
testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
}
apply plugin: 'com.google.gms.google-services'

9
app/proguard-rules.pro

@ -29,8 +29,13 @@
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*
# Picasso
-dontwarn com.squareup.okhttp.**
#Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# Android-Iconics (fontawesome-typeface)
-keep class .R

7
app/src/main/AndroidManifest.xml

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="gr.thmmy.mthmmy"
android:installLocation="auto">
@ -17,7 +18,10 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:requestLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
@ -67,6 +71,7 @@
android:name=".activities.LoginActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
tools:ignore="LockedOrientationActivity"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustPan" />

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

@ -1,48 +1,12 @@
<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>
<link rel="stylesheet" type="text/css" href="libraries_style.css" />
</head>
<body>
<ul>
<li>
<h5><a href="https://square.github.io/okhttp/">OkHttp</a>&nbsp;v3.14.2 (Copyright ©2019 Square, Inc.)</h5>
</li>
<li>
<h5><a href="https://square.github.io/picasso/">Picasso</a>&nbsp;v2.5.2 (Copyright ©2013 Square, Inc.)</h5>
<h5><a href="https://square.github.io/okhttp/">OkHttp</a>&nbsp;v3.12.12 (Copyright ©2019 Square, Inc.)</h5>
</li>
<li>
<h5><a href="https://github.com/franmontiel/PersistentCookieJar">PersistentCookieJar</a>&nbsp;v1.0.1 (Copyright ©2016 Francisco José Montiel Navarro)</h5>
@ -53,6 +17,9 @@
<li>
<h5><a href="https://github.com/mikepenz/MaterialDrawer">MaterialDrawer</a>&nbsp;v6.1.1 (Copyright ©2018 Mike Penz)</h5>
</li>
<li>
<h5><a href="https://github.com/chrisbanes/PhotoView">PhotoView</a>&nbsp;v2.3.0 (Copyright ©2018 Chris Banes)</h5>
</li>
<li>
<h5><a href="https://github.com/mikepenz/Android-Iconics">Android-Iconics</a>&nbsp;v2.9.5 (Copyright ©2016 Mike Penz)</h5>
</li>
@ -72,7 +39,16 @@
<h5><a href="https://github.com/ajoberstar/grgit">Grgit</a>&nbsp;v3.0.0 (Copyright ©2018 Andrew Oberstar)</h5>
</li>
<li>
<h5><a href=https://github.com/itkacher/OkHttpProfiler">OkHttpProfiler</a>&nbsp;v1.0.5</h5>
<h5><a href="https://github.com/JodaOrg/joda-time">Joda-Time</a>&nbsp;v2.10.4 (Copyright ©2002-2019 Joda.org)</h5>
</li>
<li>
<h5><a href="https://github.com/powermock/powermock">PowerMock</a>&nbsp;v2.0.2</h5>
</li>
<li>
<h5><a href="https://github.com/sromku/android-storage">android-storage</a>&nbsp;v2.1.0</h5>
</li>
<li>
<h5><a href=https://github.com/itkacher/OkHttpProfiler">OkHttpProfiler</a>&nbsp;v1.0.7</h5>
</li>
</ul>

229
app/src/main/assets/epl_libraries.html

@ -0,0 +1,229 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="libraries_style.css" />
</head>
<body>
<ul>
<li>
<h5><a href="https://github.com/junit-team/junit4">JUnit</a>&nbsp;v4.12 (Copyright © 2002-2019, JUnit)</h5>
</li>
</ul>
<br/>
<h4>Eclipse Public License v1.0</h4>
<pre>
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from a
Contributor if it was added to the Program by such Contributor itself or anyone
acting on such Contributor's behalf. Contributions do not include additions to
the Program which: (i) are separate modules of software distributed in
conjunction with the Program under their own license agreement, and (ii) are
not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents " mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the
licenses to its Contributions set forth herein, no assurances are provided by
any Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility to
acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are
offered by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the
Program.
Contributors may not remove or alter any copyright notices contained within the
Program.
Each Contributor must identify itself as the originator of its Contribution, if
any, in a manner that reasonably allows subsequent Recipients to identify the
originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a manner
which does not create potential liability for other Contributors. Therefore, if
a Contributor includes the Program in a commercial product offering, such
Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
every other Contributor ("Indemnified Contributor") against any losses, damages
and costs (collectively "Losses") arising from claims, lawsuits and other legal
actions brought by a third party against the Indemnified Contributor to the
extent caused by the acts or omissions of such Commercial Contributor in
connection with its distribution of the Program in a commercial product
offering. The obligations in this section do not apply to any claims or Losses
relating to any actual or alleged intellectual property infringement. In order
to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
Contributor in writing of such claim, and b) allow the Commercial Contributor
to control, and cooperate with the Commercial Contributor in, the defense and
any related settlement negotiations. The Indemnified Contributor may
participate in any such claim at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If that
Commercial Contributor then makes performance claims, or offers warranties
related to Product X, those performance claims and warranties are such
Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a court
requires any other Contributor to pay any damages as a result, the Commercial
Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED 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. Each
Recipient is solely responsible for determining the appropriateness of using
and distributing the Program and assumes all risks associated with its exercise
of rights under this Agreement, including but not limited to the risks and
costs of program errors, compliance with applicable laws, damage to or loss of
data, programs or equipment, and unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under applicable
law, it shall not affect the validity or enforceability of the remainder of the
terms of this Agreement, and without further action by the parties hereto, such
provision shall be reformed to the minimum extent necessary to make such
provision valid and enforceable.
If Recipient institutes patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other software or
hardware) infringes such Recipient's patent(s), then such Recipient's rights
granted under Section 2(b) shall terminate as of the date such litigation is
filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and does
not cure such failure in a reasonable period of time after becoming aware of
such noncompliance. If all Recipient's rights under this Agreement terminate,
Recipient agrees to cease use and distribution of the Program as soon as
reasonably practicable. However, Recipient's obligations under this Agreement
and any licenses granted by Recipient relating to the Program shall continue
and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to time.
No one other than the Agreement Steward has the right to modify this Agreement.
The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to
serve as the Agreement Steward to a suitable separate entity. Each new version
of the Agreement will be given a distinguishing version number. The Program
(including Contributions) may always be distributed subject to the version of
the Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly stated
in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to
the intellectual property of any Contributor under this Agreement, whether
expressly, by implication, estoppel or otherwise. All rights in the Program not
expressly granted under this Agreement are reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.
</pre>
</body>
</html>

28
app/src/main/assets/libraries_style.css

@ -0,0 +1,28 @@
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;
word-wrap: break-word;
}
h4, h5 {
display: inline;
padding: 1em;
}
a, h4, h5 {
color: #26A69A;
word-wrap: break-word;
}
li {
color: #26A69A;
}

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

@ -1,52 +1,22 @@
<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>
<link rel="stylesheet" type="text/css" href="libraries_style.css" />
</head>
<body>
<ul>
<li>
<h5><a href="https://jsoup.org//">jsoup</a>&nbsp;v1.10.3 (Copyright ©2009-2017, Jonathan Hedley &lt;jonathan@hedley.net&gt;)</h5>
<h5><a href="https://jsoup.org">jsoup</a>&nbsp;v1.10.3 (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.12 (Copyright ©2013 -2018 Karol Wrótniak, Droids on Roids)</h5>
<h5><a href="https://github.com/koral--/android-gif-drawable">android-gif-drawable</a>&nbsp;v1.2.19 (Copyright ©2013 -2020 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>
<li>
<h5><a href="https://github.com/LachlanMcKee/timber-junit-rule">Timber JUnit-Rule</a>&nbsp;v1.0.1 (Copyright ©2017, Lachlan McKee)</h5>
</li>
</ul>

114
app/src/main/assets/other_libraries.html

@ -0,0 +1,114 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="libraries_style.css" />
</head>
<body>
<ul>
<li>
<h5><a href="https://github.com/bumptech/glide">Glide</a>&nbsp;v4.11.0</h5>
</li>
</ul>
<br/>
<h4>Glide License</h4>
<pre>
License for everything not in third_party and not otherwise marked:
Copyright 2014 Google, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of Google, Inc.
---------------------------------------------------------------------------------------------
License for third_party/disklrucache:
Copyright 2012 Jake Wharton
Copyright 2011 The Android Open Source Project
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/">http://www.apache.org/licenses/</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.
---------------------------------------------------------------------------------------------
License for third_party/gif_decoder:
Copyright (c) 2013 Xcellent Creations, Inc.
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.
---------------------------------------------------------------------------------------------
License for third_party/gif_encoder/AnimatedGifEncoder.java and
third_party/gif_encoder/LZWEncoder.java:
No copyright asserted on the source code of this class. May be used for any
purpose, however, refer to the Unisys LZW patent for restrictions on use of
the associated LZWEncoder class. Please forward any corrections to
kweiner@fmsware.com.
-----------------------------------------------------------------------------
License for third_party/gif_encoder/NeuQuant.java
Copyright (c) 1994 Anthony Dekker
NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See
"Kohonen neural networks for optimal colour quantization" in "Network:
Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of
the algorithm.
Any party obtaining a copy of these files from the author, directly or
indirectly, is granted, free of charge, a full and unrestricted irrevocable,
world-wide, paid up, royalty-free, nonexclusive right and license to deal in
this software and documentation files (the "Software"), including without
limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons who
receive copies from any such party to do so, with the only requirement being
that this copyright notice remain intact.
</pre>
</body>
</html>

8
app/src/main/assets/style.css

@ -537,4 +537,12 @@ img
color: #a51111 !important;
}
span[style="background-color: yellow;"]
{
color: black !important;
}
[style="color: white;"] > span[style="background-color: yellow;"]
{
color: white !important;
}

BIN
app/src/main/ic_launcher-playstore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

51
app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java

@ -1,7 +1,9 @@
package gr.thmmy.mthmmy.activities;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableString;
@ -26,6 +28,7 @@ import gr.thmmy.mthmmy.BuildConfig;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.io.AssetUtils;
public class AboutActivity extends BaseActivity {
private static final int TIME_INTERVAL = 1000;
@ -39,6 +42,7 @@ public class AboutActivity extends BaseActivity {
private AlertDialog alertDialog;
private FrameLayout easterEggImage;
@SuppressWarnings("ConstantConditions")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -134,29 +138,41 @@ public class AboutActivity extends BaseActivity {
hideEasterEgg();
}
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();
if(alertDialog.getWindow()!=null)
alertDialog.getWindow().setLayout(width, height);
public void displayLibraries(View v) {
String libraryType = v.getTag().toString();
String title="", fileName="";
switch(libraryType) {
case "APACHE":
title=getString(R.string.apache_v2_0_libraries);
fileName="apache_libraries.html";
break;
case "MIT":
title=getString(R.string.the_mit_libraries);
fileName="mit_libraries.html";
break;
case "EPL":
title=getString(R.string.epl_libraries);
fileName="epl_libraries.html";
break;
case "OTHER":
title=getString(R.string.other_libraries);
fileName="other_libraries.html";
break;
default:
break;
}
public void displayMITLibraries(View v) {
String htmlContent = AssetUtils.readFileToText(this,fileName);
LayoutInflater inflater = LayoutInflater.from(this);
WebView webView = (WebView) inflater.inflate(R.layout.dialog_licenses, coordinatorLayout, false);
webView.loadUrl("file:///android_asset/mit_libraries.html");
webView.setBackgroundColor(Color.argb(1, 255, 255, 255));
webView.loadDataWithBaseURL("file:///android_asset/", htmlContent, "text/html", "UTF-8", null);
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))
.setTitle(title)
.setView(webView)
.setPositiveButton(android.R.string.ok, null)
.show();
@ -164,9 +180,10 @@ public class AboutActivity extends BaseActivity {
alertDialog.getWindow().setLayout(width, height);
}
@SuppressLint("SourceLockedOrientationActivity")
private void showEasterEgg(){
if(getResources().getConfiguration().orientation==ActivityInfo.SCREEN_ORIENTATION_PORTRAIT){
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); //TODO: why?
appBar.setVisibility(View.INVISIBLE);
mainContent.setVisibility(View.INVISIBLE);
easterEggImage.setVisibility(View.VISIBLE);

30
app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java

@ -21,6 +21,8 @@ import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.LoginActivity;
@ -96,7 +98,9 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
}
thisPageBookmark = new Bookmark(boardTitle, ThmmyPage.getBoardId(boardUrl), true);
if (boardTitle != null && !Objects.equals(boardTitle, ""))
setBoardBookmark(findViewById(R.id.bookmark));
createDrawer();
progressBar = findViewById(R.id.progressBar);
@ -156,9 +160,9 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
@Override
public void onLoadMore() {
if (pagesLoaded < numberOfPages && parsedTopics.get(parsedTopics.size() - 1) != null) {
if (pagesLoaded < numberOfPages && !parsedTopics.isEmpty() && parsedTopics.get(parsedTopics.size() - 1) != null) {
parsedTopics.add(null);
boardAdapter.notifyItemInserted(parsedSubBoards.size() + parsedTopics.size());
boardAdapter.notifyItemInserted(parsedSubBoards.size() + parsedTopics.size()); // This gets a warning and should be changed
//Load data
boardTask = new BoardTask();
@ -275,7 +279,7 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
if (topicRows != null && !topicRows.isEmpty()) {
for (Element topicRow : topicRows) {
if (!Objects.equals(topicRow.className(), "titlebg")) {
String pTopicUrl, pSubject, pStartedBy, pLastPost, pLastPostUrl, pStats;
String pTopicUrl, pSubject, pStarter, pLastUser="", pLastPostDateTime="00:00:00", pLastPost, pLastPostUrl, pStats;
boolean pLocked = false, pSticky = false, pUnread = false;
Elements topicColumns = topicRow.select(">td");
{
@ -290,21 +294,21 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
if (column.select("a[id^=newicon]").first() != null)
pUnread = true;
}
pStartedBy = topicColumns.get(3).text();
pStarter = 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 if (pLastPost.contains("από")) {
pLastPost = pLastPost.substring(0, pLastPost.indexOf("από")) +
"\n" + pLastPost.substring(pLastPost.indexOf("από"));
} else {
Timber.wtf("Board parsing about to fail. pLastPost came with: %s", pLastPost);
Pattern pattern = Pattern.compile("(.+)\\s(by|από)\\s(.+)$");
Matcher matcher = pattern.matcher(pLastPost);
if (matcher.find()){
pLastPostDateTime = matcher.group(1);
pLastUser = matcher.group(3);
}
else
throw new ParseException("Parsing failed (pLastPost came with: \"" + pLastPost + "\")");
pLastPostUrl = topicColumns.last().select("a:has(img)").first().attr("href");
tempTopics.add(new Topic(pTopicUrl, pSubject, pStartedBy, pLastPost, pLastPostUrl,
tempTopics.add(new Topic(pTopicUrl, pSubject, pStarter, pLastUser, pLastPostDateTime, pLastPostUrl,
pStats, pLocked, pSticky, pUnread));
}
}

8
app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java

@ -39,8 +39,8 @@ class BoardAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final int VIEW_TYPE_LOADING = 4;
private final Context context;
private ArrayList<Board> parsedSubBoards = new ArrayList<>();
private ArrayList<Topic> parsedTopics = new ArrayList<>();
private ArrayList<Board> parsedSubBoards;
private ArrayList<Topic> parsedTopics;
private final ArrayList<Boolean> boardExpandableVisibility = new ArrayList<>();
private final ArrayList<Boolean> topicExpandableVisibility = new ArrayList<>();
@ -187,7 +187,7 @@ class BoardAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
topicViewHolder.topicRow.setOnClickListener(view -> {
Intent intent = new Intent(context, TopicActivity.class);
Bundle extras = new Bundle();
extras.putString(BUNDLE_TOPIC_URL, topic.getUrl());
extras.putString(BUNDLE_TOPIC_URL, topic.getTopicUrl());
extras.putString(BUNDLE_TOPIC_TITLE, topic.getSubject());
intent.putExtras(extras);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
@ -232,7 +232,7 @@ class BoardAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
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.setText(context.getString(R.string.topic_last_post, topic.getLastPostDateTime(), topic.getLastUser()));
topicViewHolder.topicLastPost.setOnClickListener(view -> {
Intent intent = new Intent(context, TopicActivity.class);
Bundle extras = new Bundle();

40
app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksActivity.java

@ -29,6 +29,9 @@ import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL;
//TODO proper handling with adapter etc.
//TODO after clicking bookmark and then back button should return to this activity
public class BookmarksActivity extends BaseActivity {
private static final String TOPIC_URL = "https://www.thmmy.gr/smf/index.php?topic=";
private static final String BOARD_URL = "https://www.thmmy.gr/smf/index.php?board=";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -48,8 +51,8 @@ public class BookmarksActivity extends BaseActivity {
//Creates the adapter that will return a fragment for each section of the activity
SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
sectionsPagerAdapter.addFragment(BookmarksTopicFragment.newInstance(1, Bookmark.arrayListToString(getTopicsBookmarked())), "Topics");
sectionsPagerAdapter.addFragment(BookmarksBoardFragment.newInstance(2, Bookmark.arrayListToString(getBoardsBookmarked())), "Boards");
sectionsPagerAdapter.addFragment(BookmarksFragment.newInstance(1, Bookmark.arrayListToString(getTopicsBookmarked()), BookmarksFragment.Type.TOPIC), "Topics");
sectionsPagerAdapter.addFragment(BookmarksFragment.newInstance(2, Bookmark.arrayListToString(getBoardsBookmarked()), BookmarksFragment.Type.BOARD), "Boards");
//Sets up the ViewPager with the sections adapter.
ViewPager viewPager = findViewById(R.id.bookmarks_container);
@ -65,44 +68,57 @@ public class BookmarksActivity extends BaseActivity {
super.onResume();
}
public boolean onTopicInteractionListener(String interactionType, Bookmark bookmarkedTopic) {
public boolean onFragmentRowInteractionListener(BookmarksFragment.Type type, String interactionType, Bookmark bookmark) {
if(type== BookmarksFragment.Type.TOPIC)
return onTopicInteractionListener(interactionType, bookmark);
else if (type==BookmarksFragment.Type.BOARD)
return onBoardInteractionListener(interactionType, bookmark);
return false;
}
private boolean onTopicInteractionListener(String interactionType, Bookmark bookmarkedTopic) {
switch (interactionType) {
case BookmarksTopicFragment.INTERACTION_CLICK_TOPIC_BOOKMARK:
case BookmarksFragment.INTERACTION_CLICK_TOPIC_BOOKMARK:
Intent intent = new Intent(BookmarksActivity.this, TopicActivity.class);
Bundle extras = new Bundle();
extras.putString(BUNDLE_TOPIC_URL, "https://www.thmmy.gr/smf/index.php?topic="
extras.putString(BUNDLE_TOPIC_URL, TOPIC_URL
+ bookmarkedTopic.getId() + "." + 2147483647);
extras.putString(BUNDLE_TOPIC_TITLE, bookmarkedTopic.getTitle());
intent.putExtras(extras);
startActivity(intent);
break;
case BookmarksTopicFragment.INTERACTION_TOGGLE_TOPIC_NOTIFICATION:
case BookmarksFragment.INTERACTION_TOGGLE_TOPIC_NOTIFICATION:
return toggleNotification(bookmarkedTopic);
case BookmarksTopicFragment.INTERACTION_REMOVE_TOPIC_BOOKMARK:
case BookmarksFragment.INTERACTION_REMOVE_TOPIC_BOOKMARK:
removeBookmark(bookmarkedTopic);
Toast.makeText(BookmarksActivity.this, "Bookmark removed", Toast.LENGTH_SHORT).show();
break;
default:
break;
}
return true;
}
public boolean onBoardInteractionListener(String interactionType, Bookmark bookmarkedBoard) {
private boolean onBoardInteractionListener(String interactionType, Bookmark bookmarkedBoard) {
switch (interactionType) {
case BookmarksBoardFragment.INTERACTION_CLICK_BOARD_BOOKMARK:
case BookmarksFragment.INTERACTION_CLICK_BOARD_BOOKMARK:
Intent intent = new Intent(BookmarksActivity.this, BoardActivity.class);
Bundle extras = new Bundle();
extras.putString(BUNDLE_BOARD_URL, "https://www.thmmy.gr/smf/index.php?board="
extras.putString(BUNDLE_BOARD_URL, BOARD_URL
+ bookmarkedBoard.getId() + ".0");
extras.putString(BUNDLE_BOARD_TITLE, bookmarkedBoard.getTitle());
intent.putExtras(extras);
startActivity(intent);
break;
case BookmarksBoardFragment.INTERACTION_TOGGLE_BOARD_NOTIFICATION:
case BookmarksFragment.INTERACTION_TOGGLE_BOARD_NOTIFICATION:
return toggleNotification(bookmarkedBoard);
case BookmarksBoardFragment.INTERACTION_REMOVE_BOARD_BOOKMARK:
case BookmarksFragment.INTERACTION_REMOVE_BOARD_BOOKMARK:
removeBookmark(bookmarkedBoard);
Toast.makeText(BookmarksActivity.this, "Bookmark removed", Toast.LENGTH_SHORT).show();
break;
default:
break;
}
return true;
}

114
app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksBoardFragment.java → app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksFragment.java

@ -1,7 +1,6 @@
package gr.thmmy.mthmmy.activities.bookmarks;
import android.app.Activity;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
@ -21,38 +20,55 @@ import java.util.ArrayList;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.model.Bookmark;
/**
* A {@link Fragment} subclass.
* Use the {@link BookmarksBoardFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class BookmarksBoardFragment extends Fragment {
public class BookmarksFragment extends Fragment {
enum Type {TOPIC, BOARD}
private static final String ARG_SECTION_NUMBER = "SECTION_NUMBER";
private static final String ARG_BOARD_BOOKMARKS = "BOARD_BOOKMARKS";
private static final String ARG_BOOKMARKS = "BOOKMARKS";
static final String INTERACTION_CLICK_TOPIC_BOOKMARK = "CLICK_TOPIC_BOOKMARK";
static final String INTERACTION_TOGGLE_TOPIC_NOTIFICATION = "TOGGLE_TOPIC_NOTIFICATION";
static final String INTERACTION_REMOVE_TOPIC_BOOKMARK = "REMOVE_TOPIC_BOOKMARK";
static final String INTERACTION_CLICK_BOARD_BOOKMARK = "CLICK_BOARD_BOOKMARK";
static final String INTERACTION_TOGGLE_BOARD_NOTIFICATION = "TOGGLE_BOARD_NOTIFICATION";
static final String INTERACTION_REMOVE_BOARD_BOOKMARK= "REMOVE_BOARD_BOOKMARK";
private ArrayList<Bookmark> boardBookmarks = null;
private TextView nothingBookmarkedTextView;
private ArrayList<Bookmark> bookmarks = null;
private Type type;
private String interactionClick, interactionToggle, interactionRemove;
private static Drawable notificationsEnabledButtonImage;
private static Drawable notificationsDisabledButtonImage;
private Drawable notificationsEnabledButtonImage;
private Drawable notificationsDisabledButtonImage;
// Required empty public constructor
public BookmarksBoardFragment() { }
public BookmarksFragment() {/* Required empty public constructor */}
private BookmarksFragment(Type type) {
this.type=type;
if(type==Type.TOPIC){
this.interactionClick=INTERACTION_CLICK_TOPIC_BOOKMARK;
this.interactionToggle=INTERACTION_TOGGLE_TOPIC_NOTIFICATION;
this.interactionRemove=INTERACTION_REMOVE_TOPIC_BOOKMARK;
}
else if (type==Type.BOARD){
this.interactionClick=INTERACTION_CLICK_BOARD_BOOKMARK;
this.interactionToggle=INTERACTION_TOGGLE_BOARD_NOTIFICATION;
this.interactionRemove=INTERACTION_REMOVE_BOARD_BOOKMARK;
}
}
/**
* Use ONLY this factory method to create a new instance of
* this fragment using the provided parameters.
* the desired fragment using the provided parameters.
*
* @return A new instance of fragment Forum.
*/
public static BookmarksBoardFragment newInstance(int sectionNumber, String boardBookmarks) {
BookmarksBoardFragment fragment = new BookmarksBoardFragment();
protected static BookmarksFragment newInstance(int sectionNumber, String bookmarks, Type type) {
BookmarksFragment fragment = new BookmarksFragment(type);
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
args.putString(ARG_BOARD_BOOKMARKS, boardBookmarks);
args.putString(ARG_BOOKMARKS, bookmarks);
fragment.setArguments(args);
return fragment;
}
@ -61,9 +77,9 @@ public class BookmarksBoardFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
String bundledBoardBookmarks = getArguments().getString(ARG_BOARD_BOOKMARKS);
if (bundledBoardBookmarks != null) {
boardBookmarks = Bookmark.stringToArrayList(bundledBoardBookmarks);
String bundledBookmarks = getArguments().getString(ARG_BOOKMARKS);
if (bundledBookmarks != null) {
bookmarks = Bookmark.stringToArrayList(bundledBookmarks);
}
}
@ -83,74 +99,68 @@ public class BookmarksBoardFragment extends Fragment {
Bundle savedInstanceState) {
// Inflates the layout for this fragment
final View rootView = layoutInflater.inflate(R.layout.fragment_bookmarks, container, false);
//bookmarks_board_container
//bookmarks container
final LinearLayout bookmarksLinearView = rootView.findViewById(R.id.bookmarks_container);
nothingBookmarkedTextView = rootView.findViewById(R.id.nothing_bookmarked);
if(this.boardBookmarks != null && !this.boardBookmarks.isEmpty()) {
for (final Bookmark bookmarkedBoard : boardBookmarks) {
if (bookmarkedBoard != null && bookmarkedBoard.getTitle() != null) {
if(this.bookmarks != null && !this.bookmarks.isEmpty()) {
hideNothingBookmarked();
for (final Bookmark bookmark : bookmarks) {
if (bookmark != null && bookmark.getTitle() != null) {
final LinearLayout row = (LinearLayout) layoutInflater.inflate(
R.layout.fragment_bookmarks_row, bookmarksLinearView, false);
row.setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity){
((BookmarksActivity) activity).onBoardInteractionListener(INTERACTION_CLICK_BOARD_BOOKMARK, bookmarkedBoard);
}
if (activity instanceof BookmarksActivity)
((BookmarksActivity) activity).onFragmentRowInteractionListener(type, interactionClick, bookmark);
});
((TextView) row.findViewById(R.id.bookmark_title)).setText(bookmarkedBoard.getTitle());
((TextView) row.findViewById(R.id.bookmark_title)).setText(bookmark.getTitle());
final ImageButton notificationsEnabledButton = row.findViewById(R.id.toggle_notification);
if (!bookmarkedBoard.isNotificationsEnabled()) {
if (!bookmark.isNotificationsEnabled()) {
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage);
}
notificationsEnabledButton.setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity) {
if (((BookmarksActivity) activity).onBoardInteractionListener(INTERACTION_TOGGLE_BOARD_NOTIFICATION, bookmarkedBoard)) {
if (((BookmarksActivity) activity).onFragmentRowInteractionListener(type, interactionToggle, bookmark))
notificationsEnabledButton.setImageDrawable(notificationsEnabledButtonImage);
} else {
else
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage);
}
}
});
(row.findViewById(R.id.remove_bookmark)).setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity){
((BookmarksActivity) activity).onBoardInteractionListener(INTERACTION_REMOVE_BOARD_BOOKMARK, bookmarkedBoard);
boardBookmarks.remove(bookmarkedBoard);
((BookmarksActivity) activity).onFragmentRowInteractionListener(type, interactionRemove, bookmark);
bookmarks.remove(bookmark);
}
row.setVisibility(View.GONE);
if (boardBookmarks.isEmpty()){
bookmarksLinearView.addView(bookmarksListEmptyMessage());
if (bookmarks.isEmpty()){
showNothingBookmarked();
}
});
bookmarksLinearView.addView(row);
}
}
} else
bookmarksLinearView.addView(bookmarksListEmptyMessage());
showNothingBookmarked();
return rootView;
}
private TextView bookmarksListEmptyMessage() {
TextView emptyBookmarksCategory = new TextView(this.getContext());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 12, 0, 0);
emptyBookmarksCategory.setLayoutParams(params);
emptyBookmarksCategory.setText(getString(R.string.empty_board_bookmarks));
emptyBookmarksCategory.setTypeface(emptyBookmarksCategory.getTypeface(), Typeface.BOLD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
emptyBookmarksCategory.setTextColor(this.getContext().getColor(R.color.primary_text));
else {
//noinspection deprecation
emptyBookmarksCategory.setTextColor(this.getContext().getResources().getColor(R.color.primary_text));
private void showNothingBookmarked() {
if(nothingBookmarkedTextView!=null)
nothingBookmarkedTextView.setVisibility(View.VISIBLE);
}
emptyBookmarksCategory.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
return emptyBookmarksCategory;
private void hideNothingBookmarked(){
if(nothingBookmarkedTextView!=null)
nothingBookmarkedTextView.setVisibility(View.INVISIBLE);
}
}

158
app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksTopicFragment.java

@ -1,158 +0,0 @@
package gr.thmmy.mthmmy.activities.bookmarks;
import android.app.Activity;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
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 androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import java.util.ArrayList;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.model.Bookmark;
/**
* A {@link Fragment} subclass.
* Use the {@link BookmarksTopicFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class BookmarksTopicFragment extends Fragment {
private static final String ARG_SECTION_NUMBER = "SECTION_NUMBER";
private static final String ARG_TOPIC_BOOKMARKS = "TOPIC_BOOKMARKS";
static final String INTERACTION_CLICK_TOPIC_BOOKMARK = "CLICK_TOPIC_BOOKMARK";
static final String INTERACTION_TOGGLE_TOPIC_NOTIFICATION = "TOGGLE_TOPIC_NOTIFICATION";
static final String INTERACTION_REMOVE_TOPIC_BOOKMARK = "REMOVE_TOPIC_BOOKMARK";
ArrayList<Bookmark> topicBookmarks = null;
private static Drawable notificationsEnabledButtonImage;
private static Drawable notificationsDisabledButtonImage;
// Required empty public constructor
public BookmarksTopicFragment() {
}
/**
* 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 BookmarksTopicFragment newInstance(int sectionNumber, String topicBookmarks) {
BookmarksTopicFragment fragment = new BookmarksTopicFragment();
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
args.putString(ARG_TOPIC_BOOKMARKS, topicBookmarks);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
String bundledTopicBookmarks = getArguments().getString(ARG_TOPIC_BOOKMARKS);
if (bundledTopicBookmarks != null) {
topicBookmarks = Bookmark.stringToArrayList(bundledTopicBookmarks);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
notificationsEnabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_on, null);
else
notificationsEnabledButtonImage = VectorDrawableCompat.create(getResources(), R.drawable.ic_notification_on, null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
notificationsDisabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_off, null);
else
notificationsDisabledButtonImage = VectorDrawableCompat.create(getResources(), R.drawable.ic_notification_off, null);
}
@Override
public View onCreateView(@NonNull LayoutInflater layoutInflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflates the layout for this fragment
final View rootView = layoutInflater.inflate(R.layout.fragment_bookmarks, container, false);
//bookmarks_topic_container
final LinearLayout bookmarksLinearView = rootView.findViewById(R.id.bookmarks_container);
if(this.topicBookmarks != null && !this.topicBookmarks.isEmpty()) {
for (final Bookmark bookmarkedTopic : topicBookmarks) {
if (bookmarkedTopic != null && bookmarkedTopic.getTitle() != null) {
final LinearLayout row = (LinearLayout) layoutInflater.inflate(
R.layout.fragment_bookmarks_row, bookmarksLinearView, false);
row.setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity) {
((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_CLICK_TOPIC_BOOKMARK, bookmarkedTopic);
}
});
((TextView) row.findViewById(R.id.bookmark_title)).setText(bookmarkedTopic.getTitle());
final ImageButton notificationsEnabledButton = row.findViewById(R.id.toggle_notification);
if (!bookmarkedTopic.isNotificationsEnabled()) {
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage);
}
notificationsEnabledButton.setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity) {
if (((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_TOGGLE_TOPIC_NOTIFICATION, bookmarkedTopic)) {
notificationsEnabledButton.setImageDrawable(notificationsEnabledButtonImage);
} else {
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage);
}
}
});
(row.findViewById(R.id.remove_bookmark)).setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity) {
((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_REMOVE_TOPIC_BOOKMARK, bookmarkedTopic);
topicBookmarks.remove(bookmarkedTopic);
}
row.setVisibility(View.GONE);
if (topicBookmarks.isEmpty()){
bookmarksLinearView.addView(bookmarksListEmptyMessage());
}
});
bookmarksLinearView.addView(row);
}
}
} else {
bookmarksLinearView.addView(bookmarksListEmptyMessage());
}
return rootView;
}
private TextView bookmarksListEmptyMessage() {
TextView emptyBookmarksCategory = new TextView(this.getContext());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 12, 0, 0);
emptyBookmarksCategory.setLayoutParams(params);
emptyBookmarksCategory.setText(getString(R.string.empty_topic_bookmarks));
emptyBookmarksCategory.setTypeface(emptyBookmarksCategory.getTypeface(), Typeface.BOLD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
emptyBookmarksCategory.setTextColor(this.getContext().getColor(R.color.primary_text));
} else {
//noinspection deprecation
emptyBookmarksCategory.setTextColor(this.getContext().getResources().getColor(R.color.primary_text));
}
emptyBookmarksCategory.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
return emptyBookmarksCategory;
}
}

4
app/src/main/java/gr/thmmy/mthmmy/activities/create_content/CreateContentActivity.java

@ -15,9 +15,9 @@ import com.google.android.material.textfield.TextInputLayout;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.settings.SettingsActivity;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.editorview.EditorView;
import gr.thmmy.mthmmy.editorview.EmojiKeyboard;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.views.editorview.EditorView;
import gr.thmmy.mthmmy.views.editorview.EmojiKeyboard;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber;

4
app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsActivity.java

@ -159,7 +159,7 @@ public class DownloadsActivity extends BaseActivity implements DownloadsAdapter.
public void onLoadMore() {
if (pagesLoaded < numberOfPages) {
parsedDownloads.add(null);
downloadsAdapter.notifyItemInserted(parsedDownloads.size());
downloadsAdapter.notifyItemInserted(parsedDownloads.size()); //This gets a warning - change it!
//Load data
parseDownloadPageTask = new ParseDownloadPageTask();
@ -287,7 +287,7 @@ public class DownloadsActivity extends BaseActivity implements DownloadsAdapter.
OkHttpClient client = BaseApplication.getInstance().getClient();
String fileName = null;
try {
Response response = client.newCall(new Request.Builder().url(download.getUrl()).build()).execute();
Response response = client.newCall(new Request.Builder().url(download.getUrl()).head().build()).execute();
String contentDisposition = response.headers("Content-Disposition").toString(); //check if link provides an attachment
if (contentDisposition.contains("attachment"))
fileName = contentDisposition.split("\"")[1];

17
app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java

@ -34,7 +34,7 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final int VIEW_TYPE_LOADING = 1;
private final Context context;
private ArrayList<Download> parsedDownloads = new ArrayList<>();
private ArrayList<Download> parsedDownloads;
private final ArrayList<Boolean> downloadExpandableVisibility = new ArrayList<>();
DownloadsAdapter(Context context, ArrayList<Download> parsedDownloads) {
@ -77,9 +77,7 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
}
if (download.getType() == Download.DownloadItemType.DOWNLOADS_CATEGORY) {
downloadViewHolder.downloadRow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
downloadViewHolder.downloadRow.setOnClickListener(view -> {
Intent intent = new Intent(context, DownloadsActivity.class);
Bundle extras = new Bundle();
extras.putString(BUNDLE_DOWNLOADS_URL, download.getUrl());
@ -87,7 +85,6 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
intent.putExtras(extras);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
});
if (downloadExpandableVisibility.get(downloadViewHolder.getAdapterPosition())) {
@ -97,9 +94,7 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
downloadViewHolder.informationExpandable.setVisibility(View.GONE);
downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_down_accent_24dp);
}
downloadViewHolder.informationExpandableBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
downloadViewHolder.informationExpandableBtn.setOnClickListener(view -> {
final boolean visible = downloadExpandableVisibility.get(downloadViewHolder.
getAdapterPosition());
if (visible) {
@ -110,7 +105,6 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_up_accent_24dp);
}
downloadExpandableVisibility.set(downloadViewHolder.getAdapterPosition(), !visible);
}
});
downloadViewHolder.title.setTypeface(Typeface.createFromAsset(context.getAssets()
, "fonts/fontawesome-webfont.ttf"));
@ -124,16 +118,13 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
downloadViewHolder.title.setText(tmp);
}
} else {
downloadViewHolder.downloadRow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
downloadViewHolder.downloadRow.setOnClickListener(view -> {
try {
((BaseActivity) context).downloadFile(new ThmmyFile(
new URL(download.getUrl()), download.getFileName(), null));
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

15
app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java

@ -30,10 +30,10 @@ import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.Board;
import gr.thmmy.mthmmy.model.Category;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.CustomRecyclerView;
import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@ -158,19 +158,24 @@ public class ForumFragment extends BaseFragment {
@Override
public void onDestroy() {
super.onDestroy();
if (forumTask != null && forumTask.getStatus() != AsyncTask.Status.RUNNING)
if (forumTask!=null){
try{
if(forumTask.isRunning())
forumTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
}
public interface ForumFragmentInteractionListener extends FragmentInteractionListener {
void onForumFragmentInteraction(Board board);
}
public void onForumTaskStarted() {
private void onForumTaskStarted() {
progressBar.setVisibility(ProgressBar.VISIBLE);
}
public void onForumTaskFinished(int resultCode, ArrayList<Category> fetchedCategories) {
private void onForumTaskFinished(int resultCode, ArrayList<Category> fetchedCategories) {
if (resultCode == NetworkResultCodes.SUCCESSFUL) {
categories.clear();
categories.addAll(fetchedCategories);
@ -191,7 +196,7 @@ public class ForumFragment extends BaseFragment {
private class ForumTask extends NewParseTask<ArrayList<Category>> {
private HttpUrl forumUrl = SessionManager.forumUrl; //may change upon collapse/expand
public ForumTask(OnTaskStartedListener onTaskStartedListener,
ForumTask(OnTaskStartedListener onTaskStartedListener,
OnNetworkTaskFinishedListener<ArrayList<Category>> onParseTaskFinishedListener) {
super(onTaskStartedListener, onParseTaskFinishedListener);
}

38
app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java

@ -1,6 +1,5 @@
package gr.thmmy.mthmmy.activities.main.recent;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -12,8 +11,11 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.views.RelativeTimeTextView;
import timber.log.Timber;
/**
@ -21,12 +23,10 @@ import gr.thmmy.mthmmy.model.TopicSummary;
* 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;
RecentAdapter(@NonNull List<TopicSummary> topicSummaryList, BaseFragment.FragmentInteractionListener listener) {
this.recentList = topicSummaryList;
mListener = (RecentFragment.RecentFragmentInteractionListener) listener;
}
@ -42,23 +42,29 @@ class RecentAdapter extends RecyclerView.Adapter<RecentAdapter.ViewHolder> {
@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(recentList.get(position).getLastUser());
holder.topic = recentList.get(position);
TopicSummary topicSummary = recentList.get(position);
holder.mTitleView.setText(topicSummary.getSubject());
if(BaseApplication.getInstance().isDisplayRelativeTimeEnabled()){
String timestamp = topicSummary.getLastPostTimestamp();
try{
holder.mDateTimeView.setReferenceTime(Long.valueOf(timestamp));
}
catch(NumberFormatException e){
Timber.e(e, "Invalid number format: %s", timestamp);
holder.mDateTimeView.setText(topicSummary.getLastPostSimplifiedDateTime());
}
}
else
holder.mDateTimeView.setText(topicSummary.getLastPostSimplifiedDateTime());
holder.mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
holder.mUserView.setText(topicSummary.getLastUser());
holder.topic = topicSummary;
holder.mView.setOnClickListener(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); //?
}
}
});
}
@ -72,7 +78,7 @@ class RecentAdapter extends RecyclerView.Adapter<RecentAdapter.ViewHolder> {
final View mView;
final TextView mTitleView;
final TextView mUserView;
final TextView mDateTimeView;
final RelativeTimeTextView mDateTimeView;
public TopicSummary topic;
ViewHolder(View view) {

26
app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java

@ -26,10 +26,10 @@ import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.CustomRecyclerView;
import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Response;
import timber.log.Timber;
@ -85,7 +85,6 @@ public class RecentFragment extends BaseFragment {
if (topicSummaries.isEmpty()) {
recentTask = new RecentTask(this::onRecentTaskStarted, this::onRecentTaskFinished);
recentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.indexUrl.toString());
}
Timber.d("onActivityCreated");
}
@ -100,7 +99,7 @@ public class RecentFragment extends BaseFragment {
// Set the adapter
if (rootView instanceof RelativeLayout) {
progressBar = rootView.findViewById(R.id.progressBar);
recentAdapter = new RecentAdapter(getActivity(), topicSummaries, fragmentInteractionListener);
recentAdapter = new RecentAdapter(topicSummaries, fragmentInteractionListener);
CustomRecyclerView recyclerView = rootView.findViewById(R.id.list);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(recyclerView.getContext());
@ -128,8 +127,13 @@ public class RecentFragment extends BaseFragment {
@Override
public void onDestroy() {
super.onDestroy();
if (recentTask!=null){
try{
if(recentTask.isRunning())
recentTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
}
@ -186,21 +190,11 @@ public class RecentFragment extends BaseFragment {
String dateTime = recent.get(i + 2).text();
pattern = Pattern.compile("\\[(.*)]");
matcher = pattern.matcher(dateTime);
if (matcher.find()) {
dateTime = matcher.group(1);
if (dateTime.contains(" am") || dateTime.contains(" pm") ||
dateTime.contains(" πμ") || dateTime.contains(" μμ")) {
dateTime = dateTime.replaceAll(":[0-5][0-9] ", " ");
} else {
dateTime = dateTime.substring(0, dateTime.lastIndexOf(":"));
}
if (!dateTime.contains(",")) {
dateTime = dateTime.replaceAll(".+? ([0-9])", "$1");
}
} else
if (matcher.find())
fetchedRecent.add(new TopicSummary(link, title, lastUser, matcher.group(1)));
else
throw new ParseException("Parsing failed (dateTime)");
fetchedRecent.add(new TopicSummary(link, title, lastUser, dateTime));
}
return fetchedRecent;
}

101
app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadAdapter.java

@ -11,92 +11,60 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.views.RelativeTimeTextView;
import timber.log.Timber;
class UnreadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<TopicSummary> unreadList;
private final UnreadFragment.UnreadFragmentInteractionListener mListener;
private final MarkReadInteractionListener markReadListener;
private final int VIEW_TYPE_ITEM = 0;
private final int VIEW_TYPE_NADA = 1;
private final int VIEW_TYPE_MARK_READ = 2;
UnreadAdapter(@NonNull List<TopicSummary> topicSummaryList,
BaseFragment.FragmentInteractionListener listener,
MarkReadInteractionListener markReadInteractionListener) {
BaseFragment.FragmentInteractionListener listener) {
this.unreadList = topicSummaryList;
mListener = (UnreadFragment.UnreadFragmentInteractionListener) listener;
markReadListener = markReadInteractionListener;
}
@Override
public int getItemViewType(int position) {
if (unreadList.get(position).getDateTimeModified() == null) return VIEW_TYPE_MARK_READ;
return unreadList.get(position).getTopicUrl() == null ? VIEW_TYPE_NADA : VIEW_TYPE_ITEM;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_ITEM) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_unread_row, parent, false);
return new ViewHolder(view);
} else if (viewType == VIEW_TYPE_NADA) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_unread_empty_row, parent, false);
return new EmptyViewHolder(view);
} else if (viewType == VIEW_TYPE_MARK_READ) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_unread_mark_read_row, parent, false);
return new MarkReadViewHolder(view);
}
return null;
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof UnreadAdapter.EmptyViewHolder) {
final UnreadAdapter.EmptyViewHolder emptyViewHolder = (UnreadAdapter.EmptyViewHolder) holder;
emptyViewHolder.text.setText(unreadList.get(holder.getAdapterPosition()).getDateTimeModified());
} else if (holder instanceof UnreadAdapter.ViewHolder) {
TopicSummary topicSummary = unreadList.get(holder.getAdapterPosition());
final UnreadAdapter.ViewHolder viewHolder = (UnreadAdapter.ViewHolder) holder;
viewHolder.mTitleView.setText(unreadList.get(holder.getAdapterPosition()).getSubject());
viewHolder.mDateTimeView.setText(unreadList.get(holder.getAdapterPosition()).getDateTimeModified());
viewHolder.mUserView.setText(unreadList.get(position).getLastUser());
viewHolder.topic = unreadList.get(holder.getAdapterPosition());
viewHolder.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.onUnreadFragmentInteraction(viewHolder.topic); //?
viewHolder.mTitleView.setText(topicSummary.getSubject());
if(BaseApplication.getInstance().isDisplayRelativeTimeEnabled()){
String timestamp = topicSummary.getLastPostTimestamp();
try{
viewHolder.mDateTimeView.setReferenceTime(Long.valueOf(timestamp));
}
catch(NumberFormatException e){
Timber.e(e, "Invalid number format: %s", timestamp);
viewHolder.mDateTimeView.setText(topicSummary.getLastPostSimplifiedDateTime());
}
});
} else if (holder instanceof UnreadAdapter.MarkReadViewHolder) {
final UnreadAdapter.MarkReadViewHolder markReadViewHolder = (UnreadAdapter.MarkReadViewHolder) holder;
markReadViewHolder.text.setText(unreadList.get(holder.getAdapterPosition()).getSubject());
markReadViewHolder.topic = unreadList.get(holder.getAdapterPosition());
}
else
viewHolder.mDateTimeView.setText(topicSummary.getLastPostSimplifiedDateTime());
markReadViewHolder.mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewHolder.mUserView.setText(topicSummary.getLastUser());
viewHolder.topic = topicSummary;
viewHolder.mView.setOnClickListener(v -> {
if (null != mListener) {
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
markReadListener.onMarkReadInteraction(unreadList.get(holder.getAdapterPosition()).getTopicUrl());
}
mListener.onUnreadFragmentInteraction(viewHolder.topic); //?
}
});
}
}
@Override
public int getItemCount() {
@ -107,7 +75,7 @@ class UnreadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final View mView;
final TextView mTitleView;
final TextView mUserView;
final TextView mDateTimeView;
final RelativeTimeTextView mDateTimeView;
public TopicSummary topic;
ViewHolder(View view) {
@ -118,29 +86,4 @@ class UnreadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
mDateTimeView = view.findViewById(R.id.dateTime);
}
}
private static class EmptyViewHolder extends RecyclerView.ViewHolder {
final TextView text;
EmptyViewHolder(View view) {
super(view);
text = view.findViewById(R.id.text);
}
}
private static class MarkReadViewHolder extends RecyclerView.ViewHolder {
final View mView;
final TextView text;
public TopicSummary topic;
MarkReadViewHolder(View view) {
super(view);
mView = view;
text = view.findViewById(R.id.mark_read);
}
}
interface MarkReadInteractionListener {
void onMarkReadInteraction(String markReadLinkUrl);
}
}

276
app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadFragment.java

@ -1,4 +1,3 @@
package gr.thmmy.mthmmy.activities.main.unread;
import android.os.AsyncTask;
@ -7,32 +6,36 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
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.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.CustomRecyclerView;
import gr.thmmy.mthmmy.session.ValidateSessionTask;
import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
@ -50,14 +53,19 @@ public class UnreadFragment extends BaseFragment {
private MaterialProgressBar progressBar;
private SwipeRefreshLayout swipeRefreshLayout;
private FloatingActionButton markAsReadFAB;
private TextView noUnreadTopicsTextView;
private UnreadAdapter unreadAdapter;
private List<TopicSummary> topicSummaries;
private String markAsReadUrl;
private int numberOfPages = 0;
private int loadedPages = 0;
private UnreadTask unreadTask;
private MarkReadTask markReadTask;
private ValidateSessionTask validateSessionTask;
// Required empty public constructor
public UnreadFragment() {}
@ -81,18 +89,22 @@ public class UnreadFragment extends BaseFragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
topicSummaries = new ArrayList<>();
markAsReadUrl = BaseApplication.getInstance().getSessionManager().getMarkAllAsReadLink();
if(markAsReadUrl==null){
Timber.i("MarkAsRead URL is null.");
startValidateSessionTask();
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (topicSummaries.isEmpty()){
unreadTask = new UnreadTask(this::onUnreadTaskStarted, this::onUnreadTaskFinished);
unreadTask = new UnreadTask(this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskCancelled, this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
}
markReadTask = new MarkReadTask();
Timber.d("onActivityCreated");
markReadTask = new MarkReadTask(this::onMarkReadTaskStarted, this::onMarkReadTaskFinished);
}
@ -103,15 +115,21 @@ public class UnreadFragment extends BaseFragment {
final View rootView = inflater.inflate(R.layout.fragment_unread, container, false);
// Set the adapter
if (rootView instanceof RelativeLayout) {
if (rootView instanceof CoordinatorLayout) {
progressBar = rootView.findViewById(R.id.progressBar);
unreadAdapter = new UnreadAdapter(topicSummaries,
fragmentInteractionListener, markReadLinkUrl -> {
if (!markReadTask.isRunning() && !unreadTask.isRunning()) {
markReadTask = new MarkReadTask();
markReadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, markReadLinkUrl);
noUnreadTopicsTextView = rootView.findViewById(R.id.no_unread_topics);
markAsReadFAB = rootView.findViewById(R.id.unread_fab);
if(topicSummaries.isEmpty()){
hideMarkAsReadFAB();
noUnreadTopicsTextView.setVisibility(View.VISIBLE);
}
});
else{
noUnreadTopicsTextView.setVisibility(View.INVISIBLE);
showMarkAsReadFAB();
}
unreadAdapter = new UnreadAdapter(topicSummaries, fragmentInteractionListener);
CustomRecyclerView recyclerView = rootView.findViewById(R.id.list);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(recyclerView.getContext());
@ -125,15 +143,7 @@ public class UnreadFragment extends BaseFragment {
swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.primary);
swipeRefreshLayout.setColorSchemeResources(R.color.accent);
swipeRefreshLayout.setOnRefreshListener(
() -> {
if (!unreadTask.isRunning()) {
numberOfPages = 0;
loadedPages = 0;
unreadTask = new UnreadTask(this::onUnreadTaskStarted, this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
}
}
this::startUnreadTask
);
}
@ -143,46 +153,126 @@ public class UnreadFragment extends BaseFragment {
@Override
public void onDestroy() {
super.onDestroy();
if (unreadTask!=null && unreadTask.isRunning())
unreadTask.cancel(true);
if (markReadTask!=null && markReadTask.isRunning())
cancelUnreadTaskIfRunning();
if (markReadTask!=null){
try{
if(markReadTask.isRunning())
markReadTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
if (validateSessionTask!=null){
try{
if(validateSessionTask.isRunning())
validateSessionTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
if(topicSummaries!=null)
topicSummaries.clear();
}
private void startUnreadTask(){
if (unreadTask!=null) {
try{
if(!unreadTask.isRunning()){
numberOfPages = 0;
loadedPages = 0;
unreadTask = new UnreadTask(UnreadFragment.this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskCancelled, UnreadFragment.this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
}
}
catch (NullPointerException ignored){ }
}
}
private void startValidateSessionTask(){
validateSessionTask = new ValidateSessionTask();
validateSessionTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void cancelUnreadTaskIfRunning(){
if (unreadTask!=null){
try{
if(unreadTask.isRunning())
unreadTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
}
public interface UnreadFragmentInteractionListener extends FragmentInteractionListener {
void onUnreadFragmentInteraction(TopicSummary topicSummary);
}
//---------------------------------------ASYNC TASK-----------------------------------
private void showMarkAsReadFAB() {
markAsReadFAB.setOnClickListener(v -> showMarkAsReadConfirmationDialog());
markAsReadFAB.show();
markAsReadFAB.setTag(true);
}
private void hideMarkAsReadFAB() {
markAsReadFAB.setOnClickListener(null);
markAsReadFAB.hide();
markAsReadFAB.setTag(false);
}
private void showMarkAsReadConfirmationDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle);
builder.setTitle("Mark all as read");
builder.setMessage("Are you sure that you want to mark ALL topics as read?");
builder.setPositiveButton("Yep", (dialogInterface, i) -> {
if (!markReadTask.isRunning() && markAsReadUrl!=null){
markReadTask = new MarkReadTask(this::onMarkReadTaskStarted, this::onMarkReadTaskFinished);
markReadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, markAsReadUrl);
}
});
builder.setNegativeButton("Nope", (dialogInterface, i) -> {});
builder.create().show();
}
private void hideProgressUI(){
progressBar.setVisibility(ProgressBar.INVISIBLE);
swipeRefreshLayout.setRefreshing(false);
}
//---------------------------------------UNREAD TASK-----------------------------------
private void onUnreadTaskStarted() {
progressBar.setVisibility(ProgressBar.VISIBLE);
}
private void onUnreadTaskCancelled() {
swipeRefreshLayout.setRefreshing(false);
}
private void onUnreadTaskFinished(int resultCode, ArrayList<TopicSummary> fetchedUnread) {
if (resultCode == NetworkResultCodes.SUCCESSFUL) {
if(fetchedUnread!=null && !fetchedUnread.isEmpty()){
if(!fetchedUnread.isEmpty()){
if(loadedPages==0)
topicSummaries.clear();
topicSummaries.addAll(fetchedUnread);
unreadAdapter.notifyDataSetChanged();
noUnreadTopicsTextView.setVisibility(View.INVISIBLE);
showMarkAsReadFAB();
}
else {
topicSummaries.clear();
hideMarkAsReadFAB();
noUnreadTopicsTextView.setVisibility(View.VISIBLE);
}
unreadAdapter.notifyDataSetChanged();
loadedPages++;
if (loadedPages < numberOfPages) {
unreadTask = new UnreadTask(this::onUnreadTaskStarted, this::onUnreadTaskFinished);
unreadTask = new UnreadTask(this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskCancelled, this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString() + ";start=" + loadedPages * 20);
}
else {
progressBar.setVisibility(ProgressBar.INVISIBLE);
swipeRefreshLayout.setRefreshing(false);
}
else
hideProgressUI();
}
else{
progressBar.setVisibility(ProgressBar.INVISIBLE);
swipeRefreshLayout.setRefreshing(false);
hideProgressUI();
if (resultCode == NetworkResultCodes.NETWORK_ERROR)
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
else
@ -192,9 +282,8 @@ public class UnreadFragment extends BaseFragment {
}
private class UnreadTask extends NewParseTask<ArrayList<TopicSummary>> {
UnreadTask(OnTaskStartedListener onTaskStartedListener, OnNetworkTaskFinishedListener<ArrayList<TopicSummary>> onParseTaskFinishedListener) {
super(onTaskStartedListener, onParseTaskFinishedListener);
UnreadTask(OnTaskStartedListener onTaskStartedListener, OnTaskCancelledListener onTaskCancelledListener, OnNetworkTaskFinishedListener<ArrayList<TopicSummary>> onParseTaskFinishedListener) {
super(onTaskStartedListener, onTaskCancelledListener, onParseTaskFinishedListener);
}
@Override
@ -202,7 +291,6 @@ public class UnreadFragment extends BaseFragment {
Elements unread = document.select("table.bordercolor[cellspacing=1] tr:not(.titlebg)");
ArrayList<TopicSummary> fetchedTopicSummaries = new ArrayList<>();
if (!unread.isEmpty()) {
//topicSummaries.clear();
for (Element row : unread) {
Elements information = row.select("td");
String link = information.last().select("a").first().attr("href");
@ -211,17 +299,9 @@ public class UnreadFragment extends BaseFragment {
Element lastUserAndDate = information.get(6);
String lastUser = lastUserAndDate.select("a").text();
String dateTime = lastUserAndDate.select("span").html();
//dateTime = dateTime.replace("<br>", "");
dateTime = dateTime.substring(0, dateTime.indexOf("<br>"));
dateTime = dateTime.replace("<b>", "");
dateTime = dateTime.replace("</b>", "");
if (dateTime.contains(" am") || dateTime.contains(" pm") ||
dateTime.contains(" πμ") || dateTime.contains(" μμ"))
dateTime = dateTime.replaceAll(":[0-5][0-9] ", " ");
else
dateTime = dateTime.substring(0, dateTime.lastIndexOf(":"));
if (!dateTime.contains(","))
dateTime = dateTime.replaceAll(".+? ([0-9])", "$1");
fetchedTopicSummaries.add(new TopicSummary(link, title, lastUser, dateTime));
}
@ -230,7 +310,6 @@ public class UnreadFragment extends BaseFragment {
Element pagesElement = null, markRead = null;
if (topBar != null) {
pagesElement = topBar.select("td.middletext").first();
markRead = document.select("table:not(.bordercolor):not([width])").select("a")
.first();
}
@ -243,79 +322,68 @@ public class UnreadFragment extends BaseFragment {
numberOfPages = 1;
}
if (markRead != null && loadedPages == numberOfPages - 1)
fetchedTopicSummaries.add(new TopicSummary(markRead.attr("href"), markRead.text(), null,
null));
} else {
String message = document.select("table.bordercolor[cellspacing=1]").first().text();
if (message.contains("No messages")) { //It's english
message = "No unread posts!";
} else { //It's greek
message = "Δεν υπάρχουν μη αναγνωσμένα μηνύματα!";
if (markRead != null && loadedPages == numberOfPages - 1){
String retrievedMarkAsReadUrl = markRead.attr("href");
if(!retrievedMarkAsReadUrl.equals(markAsReadUrl)) {
markAsReadUrl = retrievedMarkAsReadUrl;
BaseApplication.getInstance().getSessionManager().refreshSescFromUrl(retrievedMarkAsReadUrl);
}
fetchedTopicSummaries.add(new TopicSummary(null, null, null, message));
}
return fetchedTopicSummaries;
}
return new ArrayList<>();
}
@Override
protected int getResultCode(Response response, ArrayList<TopicSummary> data) {
protected int getResultCode(Response response, ArrayList<TopicSummary> topicSummaries) {
return NetworkResultCodes.SUCCESSFUL;
}
}
private class MarkReadTask extends AsyncTask<String, Void, Integer> {
private static final int SUCCESS = 0;
private static final int NETWORK_ERROR = 1;
private static final int OTHER_ERROR = 2;
@Override
protected void onPreExecute() {
//---------------------------------------MARKREAD TASK------------------------------------------
private void onMarkReadTaskStarted() {
cancelUnreadTaskIfRunning();
progressBar.setVisibility(ProgressBar.VISIBLE);
}
@Override
protected Integer doInBackground(String... strings) {
Request request = new Request.Builder()
.url(strings[0])
.build();
try {
client.newCall(request).execute();
return SUCCESS;
} catch (IOException e) {
Timber.i(e, "IO Exception");
return NETWORK_ERROR;
} catch (Exception e) {
Timber.e(e, "Exception");
return OTHER_ERROR;
private void onMarkReadTaskFinished(int resultCode, Boolean isSessionVerified) {
hideProgressUI();
if (resultCode == NetworkResultCodes.SUCCESSFUL) {
if (!isSessionVerified){
Toast.makeText(getContext(), "Session verification failed", Toast.LENGTH_SHORT).show();
startValidateSessionTask();
}
else
startUnreadTask();
}
else{
hideProgressUI();
if (resultCode == NetworkResultCodes.NETWORK_ERROR)
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
else
Toast.makeText(getContext(), "Unexpected error," +
" please contact the developers with the details", Toast.LENGTH_LONG).show();
}
@Override
protected void onPostExecute(Integer result) {
progressBar.setVisibility(ProgressBar.GONE);
if (result == NETWORK_ERROR) {
Toast.makeText(getContext()
, "Task was unsuccessful!\n Please check your internet conneciton.",
Toast.LENGTH_LONG).show();
} else if (result == OTHER_ERROR) {
Toast.makeText(getContext()
, "Fatal error!\n Task aborted...", Toast.LENGTH_LONG).show();
} else {
if (!unreadTask.isRunning()) {
numberOfPages = 0;
loadedPages = 0;
unreadTask = new UnreadTask(UnreadFragment.this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
}
private class MarkReadTask extends NewParseTask<Boolean> {
MarkReadTask(OnTaskStartedListener onTaskStartedListener, OnNetworkTaskFinishedListener<Boolean> onParseTaskFinishedListener) {
super(onTaskStartedListener, onParseTaskFinishedListener);
}
@Override
protected Boolean parse(Document document, Response response) throws ParseException {
Elements sessionVerificationFailed = document.select("td:containsOwn(Session " +
"verification failed. Please try logging out and back in again, and then try " +
"again.), td:containsOwn(Η επαλήθευση συνόδου απέτυχε. Παρακαλούμε κάντε " +
"αποσύνδεση, επανασύνδεση και ξαναδοκιμάστε.)");
return sessionVerificationFailed.isEmpty();
}
//TODO: Maybe extend this task and use isRunning() from ExternalAsyncTask instead (?)
public boolean isRunning(){
return getStatus() == AsyncTask.Status.RUNNING;
@Override
protected int getResultCode(Response response, Boolean isSessionVerified) {
return NetworkResultCodes.SUCCESSFUL;
}
}
}

54
app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java

@ -19,15 +19,14 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.res.ResourcesCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.bumptech.glide.Glide;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
import com.squareup.picasso.Picasso;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
@ -46,12 +45,11 @@ import gr.thmmy.mthmmy.activities.topic.TopicActivity;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.model.PostSummary;
import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.utils.CenterVerticalSpan;
import gr.thmmy.mthmmy.utils.CircleTransform;
import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.Parcel;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.ui.CenterVerticalSpan;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Response;
import timber.log.Timber;
@ -59,6 +57,7 @@ import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE;
import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.emojiTagToHtml;
import static gr.thmmy.mthmmy.utils.ui.PhotoViewUtils.displayPhotoViewImage;
/**
* Activity for user profile. When creating an Intent of this activity you need to bundle a <b>String</b>
@ -129,10 +128,10 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
avatarView = findViewById(R.id.user_thumbnail);
if (!Objects.equals(avatarUrl, ""))
//noinspection ConstantConditions
loadAvatar();
loadAvatar(false);
else
loadDefaultAvatar();
loadAvatar(true);
usernameView = findViewById(R.id.profile_activity_username);
usernameView.setTypeface(Typeface.createFromAsset(this.getAssets()
, "fonts/fontawesome-webfont.ttf"));
@ -213,29 +212,21 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
if (pmFAB.getVisibility() != View.GONE) pmFAB.setEnabled(false);
}
private void loadAvatar(){
Picasso.with(this)
.load(avatarUrl)
.fit()
.centerCrop()
.error(Objects.requireNonNull(ResourcesCompat.getDrawable(this.getResources()
, R.drawable.ic_default_user_avatar, null)))
.placeholder(Objects.requireNonNull(ResourcesCompat.getDrawable(this.getResources()
, R.drawable.ic_default_user_avatar, null)))
.transform(new CircleTransform())
.into(avatarView);
private void loadAvatar(Boolean loadDefault){
String avatarUri;
if(loadDefault)
avatarUri = "R.drawable.ic_default_user_avatar";
else {
avatarUri = avatarUrl;
if(avatarUrl!=null)
avatarView.setOnClickListener(v -> displayPhotoViewImage(ProfileActivity.this, avatarUrl));
}
private void loadDefaultAvatar(){
Picasso.with(this)
.load(R.drawable.ic_default_user_avatar)
.fit()
.centerCrop()
.error(Objects.requireNonNull(ResourcesCompat.getDrawable(this.getResources()
, R.drawable.ic_default_user_avatar, null)))
.placeholder(Objects.requireNonNull(ResourcesCompat.getDrawable(this.getResources()
, R.drawable.ic_default_user_avatar, null)))
.transform(new CircleTransform())
Glide.with(this)
.load(avatarUri)
.circleCrop()
.error(R.drawable.ic_default_user_avatar)
.placeholder(R.drawable.ic_default_user_avatar)
.into(avatarView);
}
@ -318,11 +309,10 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
usernameView.setText(usernameSpan);
} else if (usernameView.getText() != username) usernameView.setText(username);
if (avatarUrl != null && !Objects.equals(avatarUrl, ""))
//noinspection ConstantConditions
loadAvatar();
loadAvatar(false);
else
loadDefaultAvatar();
if (personalText != null) {
loadAvatar(true);
if (personalText != null && !personalText.isEmpty()) {
personalTextView.setText(personalText);
personalTextView.setVisibility(View.VISIBLE);
}

11
app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java

@ -1,10 +1,11 @@
package gr.thmmy.mthmmy.activities.profile.latestPosts;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.RelativeLayout;
import android.widget.TextView;
@ -16,6 +17,7 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.PostSummary;
import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.views.ReactiveWebView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
/**
@ -26,11 +28,13 @@ class LatestPostsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int VIEW_TYPE_EMPTY = -1;
private static final int VIEW_TYPE_ITEM = 0;
private static final int VIEW_TYPE_LOADING = 1;
private final Context context;
private final LatestPostsFragment.LatestPostsFragmentInteractionListener interactionListener;
private final ArrayList<PostSummary> parsedTopicSummaries;
LatestPostsAdapter(BaseFragment.FragmentInteractionListener interactionListener,
LatestPostsAdapter(Context context, BaseFragment.FragmentInteractionListener interactionListener,
ArrayList<PostSummary> parsedTopicSummaries) {
this.context = context;
this.interactionListener = (LatestPostsFragment.LatestPostsFragmentInteractionListener) interactionListener;
this.parsedTopicSummaries = parsedTopicSummaries;
}
@ -64,6 +68,7 @@ class LatestPostsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
return null;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
if (holder instanceof LatestPostViewHolder) {
@ -99,7 +104,7 @@ class LatestPostsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final RelativeLayout latestPostsRow;
final TextView postTitle;
final TextView postDate;
final WebView post;
final ReactiveWebView post;
LatestPostViewHolder(View itemView) {
super(itemView);

17
app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java

@ -6,7 +6,6 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -25,14 +24,13 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.PostSummary;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.deobfuscateElements;
/**
* Use the {@link LatestPostsFragment#newInstance} factory method to create an instance of this fragment.
*/
@ -87,7 +85,7 @@ public class LatestPostsFragment extends BaseFragment implements LatestPostsAdap
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);
latestPostsAdapter = new LatestPostsAdapter(this.getContext(), fragmentInteractionListener, parsedTopicSummaries);
RecyclerView mainContent = rootView.findViewById(R.id.profile_latest_posts_recycler);
mainContent.setAdapter(latestPostsAdapter);
final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
@ -176,13 +174,9 @@ public class LatestPostsFragment extends BaseFragment implements LatestPostsAdap
}
protected void onPostExecute(Boolean result) {
if (!result) { //Parse failed!
Timber.d("Parse failed!");
Toast.makeText(getContext()
, "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show();
getActivity().finish();
}
//Parse was successful
if (Boolean.FALSE.equals(result))
Timber.e(new ParseException("Parsing failed(latest posts)"),"ParseException");
progressBar.setVisibility(ProgressBar.INVISIBLE);
latestPostsAdapter.notifyDataSetChanged();
isLoadingMore = false;
@ -208,7 +202,6 @@ public class LatestPostsFragment extends BaseFragment implements LatestPostsAdap
return true;
}
deobfuscateElements(latestPostsRows, false);
for (Element row : latestPostsRows) {
String pTopicUrl, pTopicTitle, pDateTime, pPost;
if (Integer.parseInt(row.attr("cellpadding")) == 4) {

3
app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java

@ -46,8 +46,6 @@ import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.deobfuscateElements;
public class StatsFragment extends Fragment {
/**
* The key to use when putting profile's url String to {@link StatsFragment}'s Bundle.
@ -173,7 +171,6 @@ public class StatsFragment extends Fragment {
return false;
{
Elements titleRows = statsPage.select("table.bordercolor[align]>tbody>tr.titlebg");
deobfuscateElements(titleRows, false);
generalStatisticsTitle = titleRows.first().text();
if (userHasPosts) {
postingActivityByTimeTitle = titleRows.get(1).text();

4
app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java

@ -27,9 +27,6 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.deobfuscateElements;
/**
* Use the {@link SummaryFragment#newInstance} factory method to create an instance of this fragment.
*/
@ -135,7 +132,6 @@ public class SummaryFragment extends Fragment {
//Contains all summary's rows
Elements summaryRows = profile.select(".bordercolor > tbody:nth-child(1) > tr:nth-child(2) tr");
deobfuscateElements(summaryRows, false);
for (Element summaryRow : summaryRows) {
String rowText = summaryRow.text(), pHtml = "";

1
app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsActivity.java

@ -9,6 +9,7 @@ import gr.thmmy.mthmmy.base.BaseActivity;
public class SettingsActivity extends BaseActivity {
public static final String DEFAULT_HOME_TAB = "pref_app_main_default_tab_key";
public static final String DISPLAY_RELATIVE_TIME = "pref_app_display_relative_time_key";
public static final String NOTIFICATION_LED_KEY = "pref_notification_led_enable_key";
public static final String NOTIFICATION_VIBRATION_KEY = "pref_notification_vibration_enable_key";
public static final String POSTING_APP_SIGNATURE_ENABLE_KEY = "pref_posting_app_signature_enable_key";

21
app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsFragment.java

@ -42,6 +42,8 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
public static final String SELECTED_RINGTONE = "selectedRingtoneKey";
private static final String SILENT_SELECTED = "STFU";
private static final String UNREAD = "Unread";
private SharedPreferences settingsFile;
private PREFS_TYPE prefs_type = PREFS_TYPE.NOT_SET;
@ -65,7 +67,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
defaultHomeTabValues.add("1");
if(isLoggedIn = BaseApplication.getInstance().getSessionManager().isLoggedIn()){
defaultHomeTabEntries.add("Unread");
defaultHomeTabEntries.add(UNREAD);
defaultHomeTabValues.add("2");
}
}
@ -171,16 +173,16 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
if(isLoggedIn&& prefs_type==PREFS_TYPE.GUEST) {
prefs_type = PREFS_TYPE.USER;
setPreferencesFromResource(R.xml.app_preferences_user, getPreferenceScreen().getKey());
if(!defaultHomeTabEntries.contains("Unread")){
defaultHomeTabEntries.add("Unread");
if(!defaultHomeTabEntries.contains(UNREAD)){
defaultHomeTabEntries.add(UNREAD);
defaultHomeTabValues.add("2");
}
}
else if(!isLoggedIn&&prefs_type==PREFS_TYPE.USER){
prefs_type = PREFS_TYPE.GUEST;
setPreferencesFromResource(R.xml.app_preferences_guest,getPreferenceScreen().getKey());
if(defaultHomeTabEntries.contains("Unread")){
defaultHomeTabEntries.remove("Unread");
if(defaultHomeTabEntries.contains(UNREAD)){
defaultHomeTabEntries.remove(UNREAD);
defaultHomeTabValues.remove("2");
}
}
@ -201,7 +203,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
BaseApplication.getInstance().startFirebaseCrashlyticsCollection();
else {
Timber.i("Crashlytics collection will be disabled after restarting.");
Toast.makeText(BaseApplication.getInstance().getApplicationContext(), "This change will take effect once you restart the app.", Toast.LENGTH_SHORT).show();
displayRestartAppToTakeEffectToast();
}
} else if (key.equals(getString(R.string.pref_privacy_analytics_enable_key))) {
enabled = sharedPreferences.getBoolean(key, false);
@ -210,6 +212,13 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
Timber.i("Analytics collection enabled.");
else
Timber.i("Analytics collection disabled.");
} else if (key.equals(getString(R.string.pref_app_display_relative_time_key))
&& BaseApplication.getInstance().isDisplayRelativeTimeEnabled()!=sharedPreferences.getBoolean(key, false)){
displayRestartAppToTakeEffectToast();
}
}
private void displayRestartAppToTakeEffectToast(){
Toast.makeText(BaseApplication.getInstance().getApplicationContext(), "This change will take effect once you restart the app.", Toast.LENGTH_SHORT).show();
}
}

5
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutAdapter.java

@ -23,7 +23,8 @@ import gr.thmmy.mthmmy.activities.profile.ProfileActivity;
import gr.thmmy.mthmmy.activities.topic.TopicActivity;
import gr.thmmy.mthmmy.model.Shout;
import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.utils.CustomRecyclerView;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import gr.thmmy.mthmmy.views.ReactiveWebView;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_TITLE;
@ -85,7 +86,7 @@ public class ShoutAdapter extends CustomRecyclerView.Adapter<ShoutAdapter.ShoutV
static class ShoutViewHolder extends CustomRecyclerView.ViewHolder {
TextView author, dateTime;
WebView shoutContent;
ReactiveWebView shoutContent;
ShoutViewHolder(@NonNull View itemView) {
super(itemView);

12
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxFragment.java

@ -19,13 +19,13 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.editorview.EditorView;
import gr.thmmy.mthmmy.editorview.EmojiKeyboard;
import gr.thmmy.mthmmy.model.Shout;
import gr.thmmy.mthmmy.model.Shoutbox;
import gr.thmmy.mthmmy.utils.CustomRecyclerView;
import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.viewmodel.ShoutboxViewModel;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import gr.thmmy.mthmmy.views.editorview.EditorView;
import gr.thmmy.mthmmy.views.editorview.EmojiKeyboard;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber;
@ -105,9 +105,10 @@ public class ShoutboxFragment extends Fragment {
Timber.i("Shoutbox loaded successfully");
shoutAdapter.setShouts(shoutbox.getShouts());
shoutAdapter.notifyDataSetChanged();
editorView.setVisibility(shoutbox.getShoutSend() == null ? View.GONE : View.VISIBLE);
}
});
shoutboxViewModel.setOnShoutboxTaskStarted(this::onShoutboxTaskSarted);
shoutboxViewModel.setOnShoutboxTaskStarted(this::onShoutboxTaskStarted);
shoutboxViewModel.setOnShoutboxTaskFinished(this::onShoutboxTaskFinished);
shoutboxViewModel.setOnSendShoutTaskStarted(this::onSendShoutTaskStarted);
shoutboxViewModel.setOnSendShoutTaskFinished(this::onSendShoutTaskFinished);
@ -115,9 +116,10 @@ public class ShoutboxFragment extends Fragment {
shoutboxViewModel.loadShoutbox(false);
}
private void onShoutboxTaskSarted() {
private void onShoutboxTaskStarted() {
Timber.i("Starting shoutbox task...");
progressBar.setVisibility(View.VISIBLE);
editorView.setVisibility(View.GONE);
}
private void onSendShoutTaskStarted() {

2
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxTask.java

@ -45,7 +45,7 @@ public class ShoutboxTask extends NewParseTask<Shoutbox> {
String formUrl = shoutboxForm.attr("action");
String sc = shoutboxForm.select("input[name=sc]").first().attr("value");
String shoutName = shoutboxForm.select("input[name=tp-shout-name]").first().attr("value");
// TODO: make shout send nullable and disable shouting
Element shoutSendInput = shoutboxForm.select("input[name=shout_send]").first();
String shoutSend = null;
if (shoutSendInput != null)

63
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java

@ -2,6 +2,8 @@ package gr.thmmy.mthmmy.activities.topic;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@ -47,16 +49,16 @@ import gr.thmmy.mthmmy.activities.topic.tasks.ReplyTask;
import gr.thmmy.mthmmy.activities.topic.tasks.TopicTask;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.editorview.EmojiKeyboard;
import gr.thmmy.mthmmy.model.Bookmark;
import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.utils.CustomLinearLayoutManager;
import gr.thmmy.mthmmy.utils.HTMLUtils;
import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import gr.thmmy.mthmmy.viewmodel.TopicViewModel;
import gr.thmmy.mthmmy.views.CustomLinearLayoutManager;
import gr.thmmy.mthmmy.views.editorview.EmojiKeyboard;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber;
@ -81,6 +83,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
public static final String BUNDLE_TOPIC_TITLE = "TOPIC_TITLE";
private MaterialProgressBar progressBar;
private TextView toolbarTitle;
private CustomLinearLayoutManager layoutManager;
private RecyclerView recyclerView;
//Posts related
private TopicAdapter topicAdapter;
@ -123,6 +126,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
private Snackbar snackbar;
private TopicViewModel viewModel;
private EmojiKeyboard emojiKeyboard;
private AlertDialog topicInfoDialog;
//Fix for vector drawables on android <21
static {
@ -161,6 +165,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
toolbarTitle.setMarqueeRepeatLimit(-1);
toolbarTitle.setText(topicTitle);
toolbarTitle.setSelected(true);
this.setToolbarOnLongClickListener(topicPageUrl);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -177,12 +182,13 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
recyclerView = findViewById(R.id.topic_recycler_view);
recyclerView.setHasFixedSize(true);
//LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext());
CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager(
layoutManager = new CustomLinearLayoutManager(
getApplicationContext(), topicPageUrl);
recyclerView.setLayoutManager(layoutManager);
topicAdapter = new TopicAdapter(this, emojiKeyboard, topicItems);
recyclerView.setAdapter(topicAdapter);
recyclerView.setItemViewCacheSize(17); //Every page has maximum 15 posts + Poll + EditorView
replyFAB = findViewById(R.id.topic_fab);
replyFAB.hide();
@ -253,8 +259,8 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
usersViewing.setText(HTMLUtils.getSpannableFromHtml(this, topicViewers));
});
builder.setView(infoDialog);
AlertDialog dialog = builder.create();
dialog.show();
topicInfoDialog = builder.create();
topicInfoDialog.show();
return true;
case R.id.menu_share:
Intent sendIntent = new Intent(android.content.Intent.ACTION_SEND);
@ -312,6 +318,10 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
@Override
protected void onDestroy() {
super.onDestroy();
if(topicInfoDialog!=null){
topicInfoDialog.dismiss();
topicInfoDialog=null;
}
recyclerView.setAdapter(null);
viewModel.stopLoading();
}
@ -653,11 +663,11 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
Toast.makeText(this, "Failed to remove vote", Toast.LENGTH_LONG).show();
}
});
// observe the chages in data
// observe the changes in data
viewModel.getPageIndicatorIndex().observe(this, pageIndicatorIndex -> {
if (pageIndicatorIndex == null) return;
pageIndicator.setText(String.valueOf(pageIndicatorIndex) + "/" +
String.valueOf(viewModel.getPageCount()));
pageIndicator.setText(pageIndicatorIndex + "/" +
viewModel.getPageCount());
});
viewModel.getTopicTitle().observe(this, newTopicTitle -> {
if (newTopicTitle == null) return;
@ -682,11 +692,19 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
}
});
viewModel.getTopicItems().observe(this, postList -> {
if (postList == null) progressBar.setVisibility(ProgressBar.VISIBLE);
if (postList == null)
progressBar.setVisibility(ProgressBar.VISIBLE);
recyclerView.getRecycledViewPool().clear(); //Avoid inconsistency detected bug
topicItems.clear();
/* A workaround to avoid automatic scrolling when a new page
page is loaded (it happens sometimes only)*/
recyclerView.setAdapter(topicAdapter);
if (postList != null) {
topicItems.addAll(postList);
topicAdapter.notifyDataSetChanged();
}
});
/*viewModel.getFocusedPostIndex().observe(this, focusedPostIndex -> {
if (focusedPostIndex == null) return;
@ -788,4 +806,31 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
}
});
}
/**This method sets a long click listener on the title of the topic. Once the
* listener gets triggered, it copies the link url of the topic in the clipboard.
* This method is getting called on the onCreate() of the TopicActivity*/
void setToolbarOnLongClickListener(String url) {
toolbar.setOnLongClickListener(view -> {
//Try to set the clipboard text
try {
//Create a ClipboardManager
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(BUNDLE_TOPIC_URL, url));
//Make a toast to inform the user that the url was copied
Toast.makeText(
TopicActivity.this,
TopicActivity.this.getString(R.string.link_copied_msg),
Toast.LENGTH_SHORT).show();
}
//Something happened. Probably the device does not support this (report to Firebase)
catch (NullPointerException e) {
Timber.e(e, "Error while trying to copy topic's url.");
}
return true;
});
}
}

38
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java

@ -43,17 +43,16 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatButton;
import androidx.core.content.res.ResourcesCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.github.mikephil.charting.charts.HorizontalBarChart;
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.squareup.picasso.Picasso;
import java.text.DecimalFormat;
import java.util.ArrayList;
@ -65,17 +64,17 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.board.BoardActivity;
import gr.thmmy.mthmmy.activities.profile.ProfileActivity;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.editorview.EditorView;
import gr.thmmy.mthmmy.editorview.IEmojiKeyboard;
import gr.thmmy.mthmmy.model.Poll;
import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyFile;
import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.utils.CircleTransform;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import gr.thmmy.mthmmy.utils.parsing.ThmmyParser;
import gr.thmmy.mthmmy.viewmodel.TopicViewModel;
import gr.thmmy.mthmmy.views.ReactiveWebView;
import gr.thmmy.mthmmy.views.editorview.EditorView;
import gr.thmmy.mthmmy.views.editorview.IEmojiKeyboard;
import timber.log.Timber;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
@ -364,7 +363,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
holder.post.setWebViewClient(new LinkLauncher());
//noinspection ConstantConditions
loadAvatar(currentPost.getThumbnailURL(), holder.thumbnail);
loadAvatar(currentPost.getThumbnailURL(), holder.thumbnail, holder.itemView.getContext());
//Sets username,submit date, index number, subject, post's and attached files texts
holder.username.setText(currentPost.getAuthor());
@ -645,7 +644,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
Post reply = (Post) topicItems.get(position);
//noinspection ConstantConditions
loadAvatar(getSessionManager().getAvatarLink(), holder.thumbnail);
loadAvatar(getSessionManager().getAvatarLink(), holder.thumbnail, holder.itemView.getContext());
holder.username.setText(getSessionManager().getUsername());
holder.itemView.setAlpha(1f);
@ -737,7 +736,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final EditMessageViewHolder holder = (EditMessageViewHolder) currentHolder;
//noinspection ConstantConditions
loadAvatar(getSessionManager().getAvatarLink(), holder.thumbnail);
loadAvatar(getSessionManager().getAvatarLink(), holder.thumbnail, holder.itemView.getContext());
holder.username.setText(getSessionManager().getUsername());
holder.editSubject.setText(currentPost.getSubject());
@ -807,16 +806,15 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
}
}
private void loadAvatar(String imageUrl, ImageView imageView) {
Picasso.with(context)
private void loadAvatar(String imageUrl, ImageView imageView, Context context) {
if(imageUrl!=null)
imageUrl = imageUrl.trim();
Glide.with(context)
.load(imageUrl)
.fit()
.centerCrop()
.error(Objects.requireNonNull(ResourcesCompat.getDrawable(context.getResources()
, R.drawable.ic_default_user_avatar_darker, null)))
.placeholder(Objects.requireNonNull(ResourcesCompat.getDrawable(context.getResources()
, R.drawable.ic_default_user_avatar_darker, null)))
.transform(new CircleTransform())
.circleCrop()
.error(R.drawable.ic_default_user_avatar_darker)
.placeholder(R.drawable.ic_default_user_avatar_darker)
.into(imageView);
}
@ -832,7 +830,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final LinearLayout cardChildLinear;
final TextView postDate, postNum, username, subject;
final ImageView thumbnail;
final public WebView post;
final public ReactiveWebView post;
final ImageButton quoteToggle, overflowButton;
final RelativeLayout header;
final LinearLayout userExtraInfo;
@ -969,7 +967,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
if (viewModel.getCurrentPageIndex() == viewModel.getPageCount()) {
//same page
postFocusListener.onPostFocusChange(getItemCount() - 1);
Timber.e("new");
Timber.d("new");
return true;
}
}
@ -981,7 +979,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
for (int i = 0; i < topicItems.size(); i++) {
if (topicItems.get(i) instanceof Post && ((Post) topicItems.get(i)).getPostIndex() == testAgainst) {
//same page
Timber.e(Integer.toString(i));
Timber.d(Integer.toString(i));
postFocusListener.onPostFocusChange(i);
return true;
}

11
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java

@ -18,6 +18,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.Poll;
import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyFile;
@ -25,6 +26,8 @@ import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp;
/**
* Singleton used for parsing a topic.
@ -175,6 +178,7 @@ public class TopicParser {
p_specialRank, p_gender, p_personalText, p_numberOfPosts, p_postLastEditDate,
p_postURL, p_deletePostURL, p_editPostURL;
int p_postNum, p_postIndex, p_numberOfStars, p_userColor;
long p_timestamp;
boolean p_isDeleted = false, p_isUserMentionedInPost = false;
ArrayList<ThmmyFile> p_attachedFiles;
@ -191,6 +195,7 @@ public class TopicParser {
p_postLastEditDate = null;
p_deletePostURL = null;
p_editPostURL = null;
p_timestamp = 0;
//Language independent parsing
//Finds thumbnail url
@ -267,6 +272,12 @@ public class TopicParser {
p_postDate = p_postDate.substring(p_postDate.indexOf("στις:") + 6
, p_postDate.indexOf(" »"));
if (BaseApplication.getInstance().isDisplayRelativeTimeEnabled()) {
String timestamp = convertToTimestamp(p_postDate);
if(timestamp!=null)
p_timestamp = Long.valueOf(timestamp);
}
//Finds post's reply index number
Element postNum = thisRow.select("div.smalltext:matches(Απάντηση #)").first();
if (postNum == null) { //Topic starter

4
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditTask.java

@ -2,6 +2,7 @@ package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Selector;
@ -9,7 +10,6 @@ import org.jsoup.select.Selector;
import java.io.IOException;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@ -46,7 +46,7 @@ public class PrepareForEditTask extends AsyncTask<String, Void, PrepareForEditRe
String postText, commitEditURL, numReplies, seqnum, sc, topic, icon;
OkHttpClient client = BaseApplication.getInstance().getClient();
Response response = client.newCall(request).execute();
document = ParseHelpers.parse(response.body().string());
document = Jsoup.parse(response.body().string());
Element form = document.select("form#postmodify").first();

4
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReplyTask.java

@ -2,6 +2,7 @@ package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.parser.Parser;
import org.jsoup.select.Selector;
@ -9,7 +10,6 @@ import org.jsoup.select.Selector;
import java.io.IOException;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@ -43,7 +43,7 @@ public class PrepareForReplyTask extends AsyncTask<Integer, Void, PrepareForRepl
String numReplies, seqnum, sc, topic;
try {
Response response = client.newCall(request).execute();
document = ParseHelpers.parse(response.body().string());
document = Jsoup.parse(response.body().string());
numReplies = replyPageUrl.substring(replyPageUrl.indexOf("num_replies=") + 12);
seqnum = document.select("input[name=seqnum]").first().attr("value");

27
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java

@ -2,10 +2,14 @@ package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import gr.thmmy.mthmmy.activities.topic.TopicParser;
@ -41,18 +45,27 @@ public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
@Override
protected TopicTaskResult doInBackground(String... strings) {
Document topic = null;
String newPageUrl = strings[0];
//TODO: Perhaps decode all URLs app-wide (i.e. in BaseApplication)?
try {
//Decodes e.g. any %3B to ;
newPageUrl = URLDecoder.decode(newPageUrl, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
Timber.e(e, "Unsupported Encoding");
}
//Finds the index of message focus if present
int postFocus = 0;
{
//TODO: Better parseInt handling - may rarely fail
if (newPageUrl.contains("msg")) {
String tmp = newPageUrl.substring(newPageUrl.indexOf("msg") + 3);
if (tmp.contains(";"))
postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf(";")));
postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf(';')));
else if (tmp.contains("#"))
postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf("#")));
}
postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf('#')));
}
Request request = new Request.Builder()
@ -60,7 +73,7 @@ public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
.build();
try {
Response response = BaseApplication.getInstance().getClient().newCall(request).execute();
topic = ParseHelpers.parse(response.body().string());
topic = Jsoup.parse(response.body().string());
ParseHelpers.Language language = ParseHelpers.Language.getLanguage(topic);
@ -120,10 +133,10 @@ public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
}
private boolean isUnauthorized(Document document) {
return document != null && document.select("body:contains(The topic or board you" +
return document != null && !document.select("body:contains(The topic or board you" +
" are looking for appears to be either missing or off limits to you.)," +
"body:contains(Το θέμα ή πίνακας που ψάχνετε ή δεν υπάρχει ή δεν " +
"είναι προσβάσιμο από εσάς.)").size() > 0;
"είναι προσβάσιμο από εσάς.)").isEmpty();
}
@Override

2
app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadActivity.java

@ -64,11 +64,11 @@ import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.UploadCategory;
import gr.thmmy.mthmmy.model.UploadFile;
import gr.thmmy.mthmmy.services.UploadsReceiver;
import gr.thmmy.mthmmy.utils.AppCompatSpinnerWithoutDefault;
import gr.thmmy.mthmmy.utils.FileUtils;
import gr.thmmy.mthmmy.utils.TakePhoto;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseTask;
import gr.thmmy.mthmmy.views.AppCompatSpinnerWithoutDefault;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber;

6
app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadFieldsBuilderActivity.java

@ -156,11 +156,11 @@ public class UploadFieldsBuilderActivity extends BaseActivity {
private String buildTitle() {
switch (typeRadio.getCheckedRadioButtonId()) {
case R.id.upload_fields_builder_radio_button_exams:
return "[" + courseMinifiedName + "] - " + "Θέματα εξετάσεων " + getPeriod() + " " + year.getText().toString();
return "[" + courseMinifiedName + "] " + "Θέματα εξετάσεων " + getPeriod() + " " + year.getText().toString();
case R.id.upload_fields_builder_radio_button_exam_solutions:
return "[" + courseMinifiedName + "] - " + "Λύσεις θεμάτων " + getPeriod() + " " + year.getText().toString();
return "[" + courseMinifiedName + "] " + "Λύσεις θεμάτων " + getPeriod() + " " + year.getText().toString();
case R.id.upload_fields_builder_radio_button_notes:
return "[" + courseMinifiedName + "] - " + "Σημειώσεις παραδόσεων " + year.getText().toString();
return "[" + courseMinifiedName + "] " + "Σημειώσεις παραδόσεων " + year.getText().toString();
default:
return null;
}

69
app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java

@ -45,10 +45,7 @@ import com.snatik.storage.Storage;
import net.gotev.uploadservice.UploadService;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import gr.thmmy.mthmmy.R;
@ -67,6 +64,7 @@ import gr.thmmy.mthmmy.services.DownloadHelper;
import gr.thmmy.mthmmy.services.UploadsReceiver;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.FileUtils;
import gr.thmmy.mthmmy.utils.io.AssetUtils;
import gr.thmmy.mthmmy.viewmodel.BaseViewModel;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.OkHttpClient;
@ -390,7 +388,7 @@ public abstract class BaseActivity extends AppCompatActivity {
DrawerBuilder drawerBuilder = new DrawerBuilder()
.withActivity(this)
.withToolbar(toolbar)
.withDrawerWidthDp((int) BaseApplication.getInstance().getDpWidth() / 2)
.withDrawerWidthDp((int) BaseApplication.getInstance().getWidthInDp() / 2)
.withSliderBackgroundColor(ContextCompat.getColor(this, R.color.primary_light))
.withAccountHeader(accountHeader)
.withOnDrawerItemClickListener((view, position, drawerItem) -> {
@ -428,7 +426,7 @@ public abstract class BaseActivity extends AppCompatActivity {
if (!sessionManager.isLoggedIn()) //When logged out or if user is guest
startLoginActivity();
else
new LogoutTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); //Avoid delays between onPreExecute() and doInBackground()
showLogoutDialog();
} else if (drawerItem.equals(ABOUT_ID)) {
if (!(BaseActivity.this instanceof AboutActivity)) {
Intent intent = new Intent(BaseActivity.this, AboutActivity.class);
@ -543,6 +541,17 @@ public abstract class BaseActivity extends AppCompatActivity {
//}
}
}
private void showLogoutDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle);
builder.setTitle("Logout");
builder.setMessage("Are you sure that you want to logout?");
builder.setPositiveButton("Yep", (dialogInterface, i) -> {
new LogoutTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); //Avoid delays between onPreExecute() and doInBackground()
});
builder.setNegativeButton("Nope", (dialogInterface, i) -> {});
builder.create().show();
}
//-----------------------------------------LOGOUT END-----------------------------------------------
//---------------------------------------------BOOKMARKS--------------------------------------------
@ -557,24 +566,21 @@ public abstract class BaseActivity extends AppCompatActivity {
protected void setTopicBookmark(MenuItem thisPageBookmarkMenuButton) {
this.thisPageBookmarkMenuButton = thisPageBookmarkMenuButton;
if (thisPageBookmark.matchExists(topicsBookmarked)) {
if (thisPageBookmark.matchExists(topicsBookmarked))
thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_true_accent_24dp);
} else {
else
thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_false_accent_24dp);
}
}
protected void refreshTopicBookmark() {
if (thisPageBookmarkMenuButton == null) {
return;
}
if (thisPageBookmarkMenuButton == null) return;
loadSavedBookmarks();
if (thisPageBookmark.matchExists(topicsBookmarked)) {
if (thisPageBookmark.matchExists(topicsBookmarked))
thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_true_accent_24dp);
} else {
else
thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_false_accent_24dp);
}
}
protected void topicMenuBookmarkClick() {
if (thisPageBookmark.matchExists(topicsBookmarked)) {
@ -729,7 +735,6 @@ public abstract class BaseActivity extends AppCompatActivity {
}
}
@Override
public void onRequestPermissionsResult(int permsRequestCode, @NonNull String[] permissions
, @NonNull int[] grantResults) {
@ -754,13 +759,7 @@ public abstract class BaseActivity extends AppCompatActivity {
}
}
//Uses temp file - called after permission grant
private void downloadFile() {
if (checkPerms())
prepareDownload(tempThmmyFile);
}
private void prepareDownload(ThmmyFile thmmyFile) {
private void prepareDownload(@NonNull ThmmyFile thmmyFile) {
String fileName = thmmyFile.getFilename();
if (FileUtils.fileNameExists(fileName))
openDownloadPrompt(thmmyFile);
@ -768,7 +767,7 @@ public abstract class BaseActivity extends AppCompatActivity {
DownloadHelper.enqueueDownload(thmmyFile);
}
private void openDownloadPrompt(final ThmmyFile thmmyFile) {
private void openDownloadPrompt(@NonNull final ThmmyFile thmmyFile) {
View view = getLayoutInflater().inflate(R.layout.download_prompt_dialog, null);
final BottomSheetDialog dialog = new BottomSheetDialog(this);
dialog.setContentView(view);
@ -832,32 +831,14 @@ public abstract class BaseActivity extends AppCompatActivity {
privacyPolicyTextView.setPadding(30, 20, 30, 20);
privacyPolicyTextView.setTextColor(ContextCompat.getColor(this, R.color.primary_text));
SpannableConfiguration configuration = SpannableConfiguration.builder(this).linkResolver(new LinkResolverDef()).build();
StringBuilder stringBuilder = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(getAssets().open("PRIVACY.md")));
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
Markwon.setMarkdown(privacyPolicyTextView, configuration, stringBuilder.toString());
String privacyPolicy = AssetUtils.readFileToText(BaseActivity.this,"PRIVACY.md");
if(privacyPolicy!=null){
Markwon.setMarkdown(privacyPolicyTextView, configuration, privacyPolicy);
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle);
builder.setView(privacyPolicyTextView);
builder.setPositiveButton("Close", (dialogInterface, i) -> dialogInterface.dismiss());
builder.show();
} catch (IOException e) {
Timber.e(e, "Error reading Privacy Policy from assets.");
} catch (Exception e) {
Timber.e(e, "Error in Privacy Policy dialog.");
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
Timber.e(e, "Error in Privacy Policy dialog (closing reader).");
}
}
}

50
app/src/main/java/gr/thmmy/mthmmy/base/BaseApplication.java

@ -13,6 +13,7 @@ import android.widget.ImageView;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.bumptech.glide.Glide;
import com.crashlytics.android.Crashlytics;
import com.crashlytics.android.core.CrashlyticsCore;
import com.franmontiel.persistentcookiejar.PersistentCookieJar;
@ -21,12 +22,10 @@ import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersisto
import com.google.firebase.FirebaseApp;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor;
import com.jakewharton.picasso.OkHttp3Downloader;
import com.mikepenz.fontawesome_typeface_library.FontAwesome;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader;
import com.mikepenz.materialdrawer.util.DrawerImageLoader;
import com.squareup.picasso.Picasso;
import net.gotev.uploadservice.UploadService;
import net.gotev.uploadservice.okhttp.OkHttpStack;
@ -40,7 +39,7 @@ import java.util.concurrent.TimeUnit;
import gr.thmmy.mthmmy.BuildConfig;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.CrashReportingTree;
import gr.thmmy.mthmmy.utils.crashreporting.CrashReportingTree;
import io.fabric.sdk.android.Fabric;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
@ -49,6 +48,8 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.settings.SettingsActivity.DISPLAY_RELATIVE_TIME;
public class BaseApplication extends Application {
private static BaseApplication baseApplication; //BaseApplication singleton
@ -60,11 +61,15 @@ public class BaseApplication extends Application {
private OkHttpClient client;
private SessionManager sessionManager;
private boolean displayRelativeTime;
//TODO: maybe use PreferenceManager.getDefaultSharedPreferences here as well?
private static final String SHARED_PREFS = "ThmmySharedPrefs";
//Display Metrics
private static float dpWidth;
private static float widthDp;
private static int widthPxl, heightPxl;
public static BaseApplication getInstance() {
return baseApplication;
}
@ -104,13 +109,12 @@ public class BaseApplication extends Application {
.addInterceptor(chain -> {
Request request = chain.request();
HttpUrl oldUrl = chain.request().url();
if (Objects.equals(chain.request().url().host(), "www.thmmy.gr")) {
if (!oldUrl.toString().contains("theme=4")) {
if (Objects.equals(chain.request().url().host(), "www.thmmy.gr")
&& !oldUrl.toString().contains("theme=4")) {
//Probably works but needs more testing:
HttpUrl newUrl = oldUrl.newBuilder().addQueryParameter("theme", "4").build();
request = request.newBuilder().url(newUrl).build();
}
}
return chain.proceed(request);
})
.connectTimeout(40, TimeUnit.SECONDS)
@ -137,11 +141,6 @@ public class BaseApplication extends Application {
client = builder.build();
sessionManager = new SessionManager(client, cookieJar, sharedPrefsCookiePersistor, sharedPrefs, draftsPrefs);
Picasso picasso = new Picasso.Builder(getApplicationContext())
.downloader(new OkHttp3Downloader(client))
.build();
Picasso.setSingletonInstance(picasso); //All following Picasso (with Picasso.with(Context context) requests will use this Picasso object
//Sets up upload service
UploadService.NAMESPACE = BuildConfig.APPLICATION_ID;
@ -151,12 +150,12 @@ public class BaseApplication extends Application {
DrawerImageLoader.init(new AbstractDrawerImageLoader() {
@Override
public void set(ImageView imageView, Uri uri, Drawable placeholder, String tag) {
Picasso.with(imageView.getContext()).load(uri).placeholder(placeholder).into(imageView);
Glide.with(imageView.getContext()).load(uri).circleCrop().error(placeholder).placeholder(placeholder).into(imageView);
}
@Override
public void cancel(ImageView imageView) {
Picasso.with(imageView.getContext()).cancelRequest(imageView);
Glide.with(imageView.getContext()).clear(imageView);
}
@Override
@ -172,7 +171,13 @@ public class BaseApplication extends Application {
});
DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics();
dpWidth = displayMetrics.widthPixels / displayMetrics.density;
widthPxl = displayMetrics.widthPixels;
widthDp = widthPxl / displayMetrics.density;
heightPxl = displayMetrics.heightPixels;
displayRelativeTime = settingsSharedPrefs.getBoolean(DISPLAY_RELATIVE_TIME, true);
}
//Getters
@ -188,10 +193,21 @@ public class BaseApplication extends Application {
return sessionManager;
}
public float getDpWidth() {
return dpWidth;
public float getWidthInDp() {
return widthDp;
}
public int getWidthInPixels() {
return widthPxl;
}
public int getHeightInPixels() {
return heightPxl;
}
public boolean isDisplayRelativeTimeEnabled() {
return displayRelativeTime;
}
//--------------------Firebase--------------------

21
app/src/main/java/gr/thmmy/mthmmy/model/ThmmyFile.java

@ -1,5 +1,7 @@
package gr.thmmy.mthmmy.model;
import android.webkit.URLUtil;
import java.net.URL;
public class ThmmyFile {
@ -8,27 +10,36 @@ public class ThmmyFile {
*/
private static final String TAG = "ThmmyFile";
private final URL fileUrl;
private final String filename, fileInfo;
private final String fileName, fileInfo;
/**
* This constructor only creates a ThmmyFile object and <b>does not download</b> the file.
*
* @param fileUrl {@link URL} object with file's url
* @param filename {@link String} with desired file name
* @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) {
public ThmmyFile(URL fileUrl, String fileName, String fileInfo) {
this.fileUrl = fileUrl;
this.filename = filename;
if(fileName!=null)
this.fileName = fileName;
else
this.fileName = URLUtil.guessFileName(fileUrl.toString(), null, null);
this.fileInfo = fileInfo;
}
public ThmmyFile(URL fileUrl) {
this.fileUrl = fileUrl;
this.fileName = URLUtil.guessFileName(fileUrl.toString(), null, null);
this.fileInfo = null;
}
public URL getFileUrl() {
return fileUrl;
}
public String getFilename() {
return filename;
return fileName;
}
public String getFileInfo() {

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

@ -8,7 +8,7 @@ package gr.thmmy.mthmmy.model;
* not, whether it's sticky or not and whether it contains an unread post or not.</b>.
*/
public class Topic extends TopicSummary {
private final String lastPostUrl, stats;
private final String lastPostUrl, starter, stats;
private final boolean locked, sticky, unread;
// Suppresses default constructor
@ -16,6 +16,7 @@ public class Topic extends TopicSummary {
private Topic() {
super();
this.lastPostUrl = null;
this.starter = null;
this.stats = null;
this.locked = false;
this.sticky = false;
@ -29,57 +30,31 @@ public class Topic extends TopicSummary {
* @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 lastUser 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
* @param unread whether this topic contains an unread post or not
*/
public Topic(String topicUrl, String subject, String starter, String lastPost, String lastPostUrl,
public Topic(String topicUrl, String subject, String starter, String lastUser, String LastPostDateTime, String lastPostUrl,
String stats, boolean locked, boolean sticky, boolean unread) {
super(topicUrl, subject, starter, lastPost);
super(topicUrl, subject, lastUser, LastPostDateTime);
this.lastPostUrl = lastPostUrl;
this.starter = starter;
this.stats = stats;
this.locked = locked;
this.sticky = sticky;
this.unread = unread;
}
/**
* 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;
return starter;
}
/**

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

@ -1,5 +1,8 @@
package gr.thmmy.mthmmy.model;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.simplifyDateTime;
/**
* 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.
@ -7,10 +10,12 @@ package gr.thmmy.mthmmy.model;
* time of this topic's last post.</b>.
*/
public class TopicSummary {
final String topicUrl;
final String subject;
final String lastUser;
final String dateTimeModified;
private final String topicUrl;
private final String subject;
private final String lastUser;
private final String lastPostDateTime;
private final String lastPostSimplifiedDateTime;
private final String lastPostTimestamp;
// Suppresses default constructor
@SuppressWarnings("unused")
@ -18,23 +23,27 @@ public class TopicSummary {
this.topicUrl = null;
this.subject = null;
this.lastUser = null;
this.dateTimeModified = null;
this.lastPostDateTime = null;
this.lastPostSimplifiedDateTime = null;
this.lastPostTimestamp = null;
}
/**
* Constructor specifying all class variables necessary to summarise this topic. All variables
* Constructor specifying all class variables necessary to summarize 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
* @param lastUser username of this topic's last post's author
* @param lastPostDateTime this topic's date and time of last post
*/
public TopicSummary(String topicUrl, String subject, String lastUser, String dateTimeModified) {
public TopicSummary(String topicUrl, String subject, String lastUser, String lastPostDateTime) {
this.topicUrl = topicUrl;
this.subject = subject;
this.lastUser = lastUser;
this.dateTimeModified = dateTimeModified;
this.lastPostDateTime = lastPostDateTime;
this.lastPostTimestamp = convertToTimestamp(lastPostDateTime);
this.lastPostSimplifiedDateTime = simplifyDateTime(lastPostDateTime);
}
/**
@ -56,9 +65,9 @@ public class TopicSummary {
}
/**
* Gets username of this topic's last author.
* Gets username of this topic's last post's author.
*
* @return username of last author
* @return username of last post's author
*/
public String getLastUser() {
return lastUser;
@ -69,7 +78,25 @@ public class TopicSummary {
*
* @return this topic's date and time of last post
*/
public String getDateTimeModified() {
return dateTimeModified;
public String getLastPostDateTime() {
return lastPostDateTime;
}
/**
* Gets this topic's simplified date and time of last post.
*
* @return this topic's simplified date and time of last post
*/
public String getLastPostSimplifiedDateTime() {
return lastPostSimplifiedDateTime;
}
/**
* Gets the timestamp of this topic's last post.
*
* @return the timestamp of this topic's last post
*/
public String getLastPostTimestamp() {
return lastPostTimestamp;
}
}

5
app/src/main/java/gr/thmmy/mthmmy/services/DownloadHelper.java

@ -6,6 +6,8 @@ import android.net.Uri;
import android.os.Environment;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.io.File;
import gr.thmmy.mthmmy.base.BaseApplication;
@ -34,6 +36,7 @@ public class DownloadHelper {
DownloadManager.Request request = new DownloadManager.Request(downloadURI);
Cookie thmmyCookie = BaseApplication.getInstance().getSessionManager().getThmmyCookie();
if(thmmyCookie!=null)
request.addRequestHeader("Cookie", thmmyCookie.name() + "=" + thmmyCookie.value());
request.setTitle(fileName);
request.setMimeType(getMimeType(fileName));
@ -49,6 +52,7 @@ public class DownloadHelper {
}
}
@NonNull
private static String renameFileIfExists(String originalFileName) {
final String dirPath = SAVE_DIR.getAbsolutePath();
File file = new File(dirPath, originalFileName);
@ -68,7 +72,6 @@ public class DownloadHelper {
file = new File(dirPath, String.format(nameFormat, i));
}
return file.getName();
}
}

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

@ -8,6 +8,7 @@ import androidx.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.select.Elements;
@ -18,7 +19,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.Cookie;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
@ -39,6 +39,8 @@ public class SessionManager {
private static final HttpUrl loginUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=login2");
public static final HttpUrl unreadUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=unread;all;start=0;theme=4");
public static final HttpUrl shoutboxUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=tpmod;sa=shoutbox;theme=4");
private static final String baseLogoutLink = "https://www.thmmy.gr/smf/index.php?action=logout;sesc=";
private static final String baseMarkAllAsReadLink = "https://www.thmmy.gr/smf/index.php?action=markasread;sa=all;sesc=";
private static final String guestName = "Guest";
//Response Codes
@ -63,7 +65,9 @@ public class SessionManager {
private static final String USER_ID = "UserID";
private static final String AVATAR_LINK = "AvatarLink";
private static final String HAS_AVATAR = "HasAvatar";
private static final String SESC = "Sesc";
private static final String LOGOUT_LINK = "LogoutLink";
private static final String MARK_ALL_AS_READ_LINK = "MarkAllAsReadLink";
private static final String LOGGED_IN = "LoggedIn";
private static final String LOGIN_SCREEN_AS_DEFAULT = "LoginScreenAsDefault";
@ -84,7 +88,7 @@ public class SessionManager {
* Always call it in a separate thread.
*/
public int login(String... strings) {
Timber.i("Logging in...");
Timber.d("Logging in...");
//Build the login request for each case
Request request;
@ -112,10 +116,9 @@ public class SessionManager {
try {
//Make request & handle response
Response response = client.newCall(request).execute();
Document document = ParseHelpers.parse(response.body().string());
Document document = Jsoup.parse(response.body().string());
if (validateRetrievedCookies())
{
if (validateRetrievedCookies()) {
Timber.i("Login successful!");
setPersistentCookieSession(); //Store cookies
@ -129,7 +132,10 @@ public class SessionManager {
if (avatar != null)
editor.putString(AVATAR_LINK, avatar);
editor.putBoolean(HAS_AVATAR, avatar != null);
editor.putString(LOGOUT_LINK, extractLogoutLink(document));
String sesc = extractSesc(document);
editor.putString(SESC, sesc);
editor.putString(LOGOUT_LINK, generateLogoutLink(sesc));
editor.putString(MARK_ALL_AS_READ_LINK, generateMarkAllAsReadLink(sesc));
editor.apply();
return SUCCESS;
@ -181,10 +187,10 @@ public class SessionManager {
* 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() {
void validateSession() {
Timber.i("Validating session...");
if (isLoggedIn()) {
Timber.i("Refreshing session...");
int loginResult = login();
if (loginResult != FAILURE)
return;
@ -204,21 +210,18 @@ public class SessionManager {
setLoginScreenAsDefault(false);
}
/**
* Logout function. Always call it in a separate thread.
*/
public int logout() {
Timber.i("Logging out...");
try {
Request request = new Request.Builder()
.url(sharedPrefs.getString(LOGOUT_LINK, "LogoutLink"))
.url(getLogoutLink())
.build();
try {
//Make request & handle response
Response response = client.newCall(request).execute();
Document document = ParseHelpers.parse(response.body().string());
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
@ -241,6 +244,15 @@ public class SessionManager {
guestLogin();
}
}
public void refreshSescFromUrl(String url){
String sesc = extractSescFromLink(url);
if(sesc!=null){
setSesc(sesc);
setLogoutLink(generateLogoutLink(sesc));
setMarkAsReadLink(sesc);
}
}
//--------------------------------------AUTH ENDS-----------------------------------------------
//---------------------------------------GETTERS------------------------------------------------
@ -258,14 +270,31 @@ public class SessionManager {
public Cookie getThmmyCookie() {
List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl);
for(Cookie cookie: cookieList)
{
for(Cookie cookie: cookieList) {
if(cookie.name().equals("THMMYgrC00ki3"))
return cookie;
}
return null;
}
public String getMarkAllAsReadLink() {
String markAsReadLink = sharedPrefs.getString(MARK_ALL_AS_READ_LINK, null);
if(markAsReadLink == null){ //For older versions, extract it from logout link (otherwise user would have to login again)
String sesc = extractSescFromLink(getLogoutLink());
if(sesc!=null) {
setSesc(sesc);
markAsReadLink = generateMarkAllAsReadLink(sesc);
setMarkAsReadLink(markAsReadLink);
return markAsReadLink;
}
}
return markAsReadLink; // Warning: it can be null
}
private String getLogoutLink() {
return sharedPrefs.getString(LOGOUT_LINK, null);
}
public boolean hasAvatar() {
return sharedPrefs.getBoolean(HAS_AVATAR, false);
}
@ -280,6 +309,27 @@ public class SessionManager {
//--------------------------------------GETTERS END---------------------------------------------
//---------------------------------------SETTERS------------------------------------------------
private void setSesc(String sesc){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(SESC, sesc);
editor.apply();
}
private void setMarkAsReadLink(String markAllAsReadLink){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(MARK_ALL_AS_READ_LINK, markAllAsReadLink);
editor.apply();
}
private void setLogoutLink(String logoutLink){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(LOGOUT_LINK, logoutLink);
editor.apply();
}
//--------------------------------------SETTERS END---------------------------------------------
//------------------------------------OTHER FUNCTIONS-------------------------------------------
private boolean validateRetrievedCookies() {
List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl);
@ -353,7 +403,6 @@ public class SessionManager {
return "User"; //return a default username
}
@NonNull
private int extractUserId(@NonNull Document doc) {
try{
Elements elements = doc.select("a:containsOwn(Εμφάνιση των μηνυμάτων σας), a:containsOwn(Show own posts)");
@ -383,17 +432,33 @@ public class SessionManager {
return null;
}
@NonNull
private String extractLogoutLink(@NonNull Document doc) {
private String extractSesc(@NonNull Document doc) {
Elements logoutLink = doc.select("a[href^=https://www.thmmy.gr/smf/index.php?action=logout;sesc=]");
if (!logoutLink.isEmpty()) {
String link = logoutLink.first().attr("href");
if (link != null && !link.isEmpty())
return link;
return extractSescFromLink(link);
}
Timber.e(new ParseException("Parsing failed(logoutLink extraction)"),"ParseException");
return "https://www.thmmy.gr/smf/index.php?action=logout"; //return a default link
Timber.e(new ParseException("Parsing failed(extractSesc)"),"ParseException");
return null;
}
private String extractSescFromLink(String link){
if (link != null){
Pattern pattern = Pattern.compile(".+;sesc=(\\w+)");
Matcher matcher = pattern.matcher(link);
if (matcher.find())
return matcher.group(1);
}
Timber.e(new ParseException("Parsing failed(extractSescFromLogoutLink)"),"ParseException");
return null;
}
private String generateLogoutLink(String sesc){
return baseLogoutLink + sesc;
}
private String generateMarkAllAsReadLink(String sesc){
return baseMarkAllAsReadLink + sesc;
}
//----------------------------------OTHER FUNCTIONS END-----------------------------------------

18
app/src/main/java/gr/thmmy/mthmmy/session/ValidateSessionTask.java

@ -0,0 +1,18 @@
package gr.thmmy.mthmmy.session;
import android.os.AsyncTask;
import gr.thmmy.mthmmy.base.BaseApplication;
public class ValidateSessionTask extends AsyncTask<String, Void, Void> {
@Override
protected Void doInBackground(String... params) {
BaseApplication.getInstance().getSessionManager().validateSession();
return null;
}
public boolean isRunning(){
return getStatus() == AsyncTask.Status.RUNNING;
}
}

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

@ -1,47 +0,0 @@
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();
// For GIF images
Bitmap.Config config = source.getConfig() != null ? source.getConfig() : Bitmap.Config.ARGB_8888;
Bitmap bitmap = Bitmap.createBitmap(size, size, config);
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";
}
}

73
app/src/main/java/gr/thmmy/mthmmy/utils/DateTimeUtils.java

@ -0,0 +1,73 @@
package gr.thmmy.mthmmy.utils;
import androidx.annotation.VisibleForTesting;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
public class DateTimeUtils {
private static final long MONTH_IN_MILLIS = 30*DAY_IN_MILLIS;
private static final long DECADE_IN_MILLIS = 10*YEAR_IN_MILLIS;
@VisibleForTesting
public static String getRelativeTimeSpanString(long time) {
long now = System.currentTimeMillis();
boolean past = (now >= time);
long duration = Math.abs(now - time);
String format;
long count, mod;
if(duration < 45*SECOND_IN_MILLIS)
return "just now";
else if (duration < 45*MINUTE_IN_MILLIS) {
count = duration/MINUTE_IN_MILLIS;
mod = duration % MINUTE_IN_MILLIS;
if(mod >= 30*SECOND_IN_MILLIS)
count += 1;
format = "%dm";
} else if (duration < 22*HOUR_IN_MILLIS) {
count = duration/HOUR_IN_MILLIS;
format = "%dh";
mod = (duration%HOUR_IN_MILLIS)/MINUTE_IN_MILLIS;
if(count<3 && mod>9 && mod<51){
if(count==0)
format = mod +"m";
else
format = format + " " + mod +"m";
}
else if(mod >= 30)
count += 1;
} else if (duration < 26*DAY_IN_MILLIS) {
count = duration/DAY_IN_MILLIS;
format = "%dd";
mod = duration % DAY_IN_MILLIS;
if(mod >= 12*HOUR_IN_MILLIS)
count += 1;
} else if (duration < 320*DAY_IN_MILLIS) {
count = duration/MONTH_IN_MILLIS;
format = "%d month";
mod = duration % MONTH_IN_MILLIS;
if(mod >= 15*DAY_IN_MILLIS)
count += 1;
if(count>1)
format = format + 's';
} else if (duration < DECADE_IN_MILLIS) {
count = duration/YEAR_IN_MILLIS;
format = "%d year";
mod = duration % YEAR_IN_MILLIS;
if(mod >= 183*DAY_IN_MILLIS)
count += 1;
if(count>1)
format = format + 's';
}
else
return past ? "a long time ago": "in the distant future";
format = past ? format : "in " + format;
return String.format(format, (int) count);
}
}

9
app/src/main/java/gr/thmmy/mthmmy/utils/NetworkTask.java

@ -10,16 +10,15 @@ import java.io.IOException;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.crashreporting.CrashReporter;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
public abstract class NetworkTask<T> extends ExternalAsyncTask<String, Parcel<T>> {
protected OnNetworkTaskFinishedListener<T> onNetworkTaskFinishedListener;
private OnNetworkTaskFinishedListener<T> onNetworkTaskFinishedListener;
public NetworkTask(OnTaskStartedListener onTaskStartedListener, OnTaskCancelledListener onTaskCancelledListener,
OnNetworkTaskFinishedListener<T> onNetworkTaskFinishedListener) {
@ -47,14 +46,14 @@ public abstract class NetworkTask<T> extends ExternalAsyncTask<String, Parcel<T>
try {
responseBodyString = response.body().string();
} catch (NullPointerException npe) {
Timber.wtf(npe, "Invalid response. Detatails: https://square.github.io/okhttp/3.x/okhttp/okhttp3/Response.html#body--");
Timber.wtf(npe, "Invalid response. Details: https://square.github.io/okhttp/3.x/okhttp/okhttp3/Response.html#body--");
return new Parcel<>(NetworkResultCodes.NETWORK_ERROR, null);
} catch (IOException e) {
Timber.e(e, "Error getting response body string");
return new Parcel<>(NetworkResultCodes.NETWORK_ERROR, null);
}
try {
T data = performTask(ParseHelpers.parse(responseBodyString), response);
T data = performTask(Jsoup.parse(responseBodyString), response);
int resultCode = getResultCode(response, data);
return new Parcel<>(resultCode, data);
} catch (ParseException pe) {

2
app/src/main/java/gr/thmmy/mthmmy/utils/CrashReporter.java → app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReporter.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.utils.crashreporting;
import com.crashlytics.android.Crashlytics;

2
app/src/main/java/gr/thmmy/mthmmy/utils/CrashReportingTree.java → app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReportingTree.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.utils.crashreporting;
import android.util.Log;

39
app/src/main/java/gr/thmmy/mthmmy/utils/io/AssetUtils.java

@ -0,0 +1,39 @@
package gr.thmmy.mthmmy.utils.io;
import android.content.Context;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import timber.log.Timber;
public class AssetUtils {
public static String readFileToText(Context context, String fileName) {
StringBuilder stringBuilder = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(context.getAssets().open(fileName)));
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
return stringBuilder.toString();
} catch (IOException e) {
Timber.e(e, "IO error reading file %s from assets.", fileName);
} catch (Exception e) {
Timber.e(e, "Error reading file %s from assets.", fileName);
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
Timber.e(e, "Error in AssetUtils (closing reader).");
}
}
return null;
}
}

54
app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java

@ -1,9 +1,7 @@
package gr.thmmy.mthmmy.utils.parsing;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import java.util.ArrayList;
@ -11,8 +9,6 @@ import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import timber.log.Timber;
/**
* 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
@ -188,56 +184,6 @@ public class ParseHelpers {
else return "";
}
/**
* Method that replaces CloudFlare-obfuscated emails with deobfuscated ones
* Replace Jsoup.parse with this wherever needed
*
* @param html html to parse
* @return a document with deobfuscated emails
*/
public static Document parse(String html) {
Document document = Jsoup.parse(html);
deobfuscateElements(document.select("span.__cf_email__,a.__cf_email__"), true);
return document;
}
/**
* Use this method instead of parse() if you are targeting specific elements
*/
public static void deobfuscateElements(Elements elements, boolean found) {
if (!found)
elements = elements.select("span.__cf_email__,a.__cf_email__");
for (Element obfuscatedElement : elements) {
String deobfuscatedEmail = deobfuscateEmail(obfuscatedElement.attr("data-cfemail"));
if (obfuscatedElement.is("span")) {
Element parent = obfuscatedElement.parent();
if (parent.is("a") && parent.attr("href").contains("email-protection"))
parent.attr("href", "mailto:" + deobfuscatedEmail);
} else if (obfuscatedElement.attr("href").contains("email-protection"))
obfuscatedElement.attr("href", "mailto:" + deobfuscatedEmail);
obfuscatedElement.replaceWith(new TextNode(deobfuscatedEmail, ""));
}
}
/**
* @param obfuscatedEmail CloudFlare-obfuscated email
* @return deobfuscated email
*/
private static String deobfuscateEmail(String obfuscatedEmail) {
//Deobfuscate
final StringBuilder stringBuilder = new StringBuilder();
final int r = Integer.parseInt(obfuscatedEmail.substring(0, 2), 16);
for (int n = 2; n < obfuscatedEmail.length(); n += 2) {
final int i = Integer.parseInt(obfuscatedEmail.substring(n, n + 2), 16) ^ r;
stringBuilder.append(Character.toString((char) i));
}
Timber.d("Email deobfuscated.");
return stringBuilder.toString();
}
public static String emojiTagToHtml(String emojiTagedString) {
HashMap<Pattern, String> tagToHtmlMap = new HashMap<>();

5
app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseTask.java

@ -3,6 +3,7 @@ package gr.thmmy.mthmmy.utils.parsing;
import android.os.AsyncTask;
import android.widget.Toast;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.IOException;
@ -20,7 +21,7 @@ import timber.log.Timber;
*/
public abstract class ParseTask extends AsyncTask<String, Void, ParseTask.ResultCode> {
protected String url;
protected enum ResultCode {
public enum ResultCode {
SUCCESS, PARSING_ERROR, NETWORK_ERROR, OTHER_ERROR
}
@ -41,7 +42,7 @@ public abstract class ParseTask extends AsyncTask<String, Void, ParseTask.Result
Request request = prepareRequest(params);
try {
Response response = BaseApplication.getInstance().getClient().newCall(request).execute();
Document document = ParseHelpers.parse(response.body().string());
Document document = Jsoup.parse(response.body().string());
parse(document);
postParsing();
return ResultCode.SUCCESS;

112
app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParser.java

@ -0,0 +1,112 @@
package gr.thmmy.mthmmy.utils.parsing;
import androidx.annotation.VisibleForTesting;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.joda.time.format.DateTimeParser;
import java.text.DateFormatSymbols;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.base.BaseApplication;
import timber.log.Timber;
public class ThmmyDateTimeParser {
private static final DateTimeParser[] parsers = {
DateTimeFormat.forPattern("hh:mm:ss a").getParser(),
DateTimeFormat.forPattern("HH:mm:ss").getParser(),
DateTimeFormat.forPattern("MMMM d, Y, hh:mm:ss a").getParser(),
DateTimeFormat.forPattern("MMMM d, Y, HH:mm:ss").getParser(),
DateTimeFormat.forPattern("d MMMM Y, HH:mm:ss").getParser(),
DateTimeFormat.forPattern("Y-M-d, HH:mm:ss").getParser(),
DateTimeFormat.forPattern("d-M-Y, HH:mm:ss").getParser()
};
private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.append(null, parsers)
.toFormatter();
//TODO: Replace with Locale.forLanguageTag() (with "el-GR","en-US") when KitKat support is dropped
private static final Locale greekLocale = new Locale("el", "GR");
private static final Locale englishLocale = new Locale("en", "US");
private static final Pattern pattern = Pattern.compile("\\s((1[3-9]|2[0-3]):)");
private ThmmyDateTimeParser(){}
public static String convertToTimestamp(String thmmyDateTime){
Timber.v("Will attempt to convert %s to timestamp.", thmmyDateTime);
String originalDateTime = thmmyDateTime;
DateTimeZone dtz = getDtz();
// Remove any unnecessary "Today at" strings
thmmyDateTime = purifyTodayDateTime(thmmyDateTime);
// Add today's date for the first two cases
if(thmmyDateTime.charAt(2)==':')
thmmyDateTime = (new DateTime()).toString("MMMM d, Y, ") + thmmyDateTime;
// Don't even ask
if(thmmyDateTime.contains("am"))
thmmyDateTime = thmmyDateTime.replaceAll("\\s00:"," 12:");
// For the stupid format 23:54:12 pm
Matcher matcher = pattern.matcher(thmmyDateTime);
if (matcher.find())
thmmyDateTime = thmmyDateTime.replaceAll("\\spm","");
DateTime dateTime;
try{
dateTime=formatter.withZone(dtz).withLocale(englishLocale).parseDateTime(thmmyDateTime);
}
catch (IllegalArgumentException e1){
Timber.v("Parsing DateTime %s using English Locale failed.", thmmyDateTime);
try{
DateFormatSymbols dfs = DateTimeUtils.getDateFormatSymbols(greekLocale);
thmmyDateTime = thmmyDateTime.replace("am",dfs.getAmPmStrings()[0]);
thmmyDateTime = thmmyDateTime.replace("pm",dfs.getAmPmStrings()[1]);
Timber.v("Attempting to parse DateTime %s using Greek Locale...", thmmyDateTime);
dateTime=formatter.withZone(dtz).withLocale(greekLocale).parseDateTime(thmmyDateTime);
}
catch (IllegalArgumentException e2){
Timber.d("Parsing DateTime %s using Greek Locale failed too.", thmmyDateTime);
Timber.e("Couldn't convert DateTime %s to timestamp!", originalDateTime);
return null;
}
}
String timestamp = Long.toString(dateTime.getMillis());
Timber.v("DateTime %s was converted to %s, or %s", originalDateTime, timestamp, dateTime.toString());
return timestamp;
}
public static String simplifyDateTime(String dateTime){
return removeSeconds(purifyTodayDateTime(dateTime));
}
// Converts e.g. Today at 12:16:48 -> 12:16:48, but October 03, 2019, 16:40:18 remains as is
@VisibleForTesting
static String purifyTodayDateTime(String dateTime){
return dateTime.replaceAll("(Today at |Σήμερα στις )(.+)", "$2");
}
// Converts e.g. 12:16:48 -> 12:16, October 03, 2019, 16:40:18 -> 12:16 October 03, 2019, 16:40
private static String removeSeconds(String dateTime){
return dateTime.replaceAll("(.*):\\d+($|\\s.*)", "$1$2");
}
@VisibleForTesting
private static DateTimeZone getDtz(){
if(!BaseApplication.getInstance().getSessionManager().isLoggedIn())
return DateTimeZone.forID("Europe/Athens");
else
return DateTimeZone.getDefault();
}
}

2
app/src/main/java/gr/thmmy/mthmmy/utils/CenterVerticalSpan.java → app/src/main/java/gr/thmmy/mthmmy/utils/ui/CenterVerticalSpan.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.utils.ui;
import android.graphics.Canvas;
import android.graphics.Paint;

65
app/src/main/java/gr/thmmy/mthmmy/utils/ui/ImageDownloadDialogBuilder.java

@ -0,0 +1,65 @@
package gr.thmmy.mthmmy.utils.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.ContextWrapper;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import java.net.MalformedURLException;
import java.net.URL;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.ThmmyFile;
import timber.log.Timber;
import static android.content.Context.CLIPBOARD_SERVICE;
public class ImageDownloadDialogBuilder extends AlertDialog.Builder{
private static final String[] colors = {"Copy image location", "Save Image"};
private Context context;
private String imageURL;
public ImageDownloadDialogBuilder(@NonNull Context context, String imageURL) {
super(context);
this.context = context;
this.imageURL = imageURL;
setItems(colors, (dialog, which) -> {
if(which == 0)
copyUrlToClipboard();
else {
try {
getBaseActivity().downloadFile(new ThmmyFile(new URL(imageURL)));
} catch (MalformedURLException e) {
Timber.e(e, "Exception downloading image (MalformedURLException)");
} catch (NullPointerException e) {
Timber.e(e, "Exception downloading image (NullPointerException)");
}
}
});
}
private void copyUrlToClipboard(){
ClipboardManager clipboard = (ClipboardManager) BaseApplication.getInstance().getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("ReactiveWebViewCopiedText", imageURL);
clipboard.setPrimaryClip(clip);
Toast.makeText(BaseApplication.getInstance().getApplicationContext(),context.getString(R.string.link_copied_msg),Toast.LENGTH_SHORT).show();
}
private BaseActivity getBaseActivity() {
Context baseActivityContext = context;
while (baseActivityContext instanceof ContextWrapper) {
if (context instanceof BaseActivity)
return (BaseActivity) context;
baseActivityContext = ((ContextWrapper)context).getBaseContext();
}
return null;
}
}

63
app/src/main/java/gr/thmmy/mthmmy/utils/ui/PhotoViewUtils.java

@ -0,0 +1,63 @@
package gr.thmmy.mthmmy.utils.ui;
import android.app.Dialog;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.view.ViewGroup;
import android.view.Window;
import androidx.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.github.chrisbanes.photoview.PhotoView;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
public class PhotoViewUtils {
private final static int screenWidth = BaseApplication.getInstance().getWidthInPixels();
private final static int screenHeight = BaseApplication.getInstance().getHeightInPixels();
public static void displayPhotoViewImage(Context context, String imageURL) {
Dialog builder = new Dialog(context);
builder.requestWindowFeature(Window.FEATURE_NO_TITLE);
builder.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
builder.getWindow().setBackgroundDrawable(
new ColorDrawable(android.graphics.Color.TRANSPARENT));
PhotoView photoView = new PhotoView(context);
photoView.setLayoutParams(new ViewGroup.LayoutParams(screenWidth, screenHeight));
Glide.with(context)
.load(imageURL)
.fitCenter()
.error(R.drawable.ic_file_not_found)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
photoView.setZoomable(false);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
photoView.setOnLongClickListener(v -> {
ImageDownloadDialogBuilder imageDownloadDialogBuilder = new ImageDownloadDialogBuilder(context, imageURL);
imageDownloadDialogBuilder.show();
return false;
});
return false;
}
})
.into(photoView);
builder.addContentView(photoView, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
builder.show();
}
}

2
app/src/main/java/gr/thmmy/mthmmy/utils/ScrollAwareFABBehavior.java → app/src/main/java/gr/thmmy/mthmmy/utils/ui/ScrollAwareFABBehavior.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.utils.ui;
import android.content.Context;
import android.util.AttributeSet;

2
app/src/main/java/gr/thmmy/mthmmy/utils/ScrollAwareLinearBehavior.java → app/src/main/java/gr/thmmy/mthmmy/utils/ui/ScrollAwareLinearBehavior.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.utils.ui;
import android.animation.Animator;
import android.content.Context;

2
app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java

@ -132,7 +132,7 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa
int pageRequested = pageIndicatorIndex.getValue() - 1;
if (pageRequested != currentPageIndex - 1) {
Timber.i("Changing to page " + pageRequested + 1);
loadUrl(ParseHelpers.getBaseURL(topicUrl) + "." + String.valueOf(pageRequested * 15));
loadUrl(ParseHelpers.getBaseURL(topicUrl) + "." + pageRequested * 15);
pageIndicatorIndex.setValue(pageRequested + 1);
} else {
stopLoading();

2
app/src/main/java/gr/thmmy/mthmmy/utils/AppCompatSpinnerWithoutDefault.java → app/src/main/java/gr/thmmy/mthmmy/views/AppCompatSpinnerWithoutDefault.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.views;
import android.annotation.SuppressLint;
import android.content.Context;

2
app/src/main/java/gr/thmmy/mthmmy/utils/CustomLinearLayoutManager.java → app/src/main/java/gr/thmmy/mthmmy/views/CustomLinearLayoutManager.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.views;
import android.content.Context;

2
app/src/main/java/gr/thmmy/mthmmy/utils/CustomRecyclerView.java → app/src/main/java/gr/thmmy/mthmmy/views/CustomRecyclerView.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.views;
import android.content.Context;
import android.util.AttributeSet;

93
app/src/main/java/gr/thmmy/mthmmy/views/ReactiveWebView.java

@ -0,0 +1,93 @@
package gr.thmmy.mthmmy.views;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.webkit.WebView;
import android.widget.Toast;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.ui.ImageDownloadDialogBuilder;
import static android.content.Context.CLIPBOARD_SERVICE;
import static gr.thmmy.mthmmy.utils.ui.PhotoViewUtils.displayPhotoViewImage;
public class ReactiveWebView extends WebView {
private final static long MAX_TOUCH_DURATION = 100;
private final Context context;
private long downTime;
public ReactiveWebView(Context context) {
super(context);
this.context = context;
init();
}
public ReactiveWebView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
public ReactiveWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
}
private void init(){
setOnLongClickListener();
this.setVerticalScrollBarEnabled(false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downTime = event.getEventTime();
break;
case MotionEvent.ACTION_UP:
if(event.getEventTime() - downTime <= MAX_TOUCH_DURATION)
performClick();
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean performClick() {
WebView.HitTestResult result = this.getHitTestResult();
if(result.getType() == WebView.HitTestResult.IMAGE_TYPE){
String imageURL = result.getExtra();
displayPhotoViewImage(context, imageURL);
}
return super.performClick();
}
private void setOnLongClickListener(){
this.setOnLongClickListener(v -> {
HitTestResult result = ReactiveWebView.this.getHitTestResult();
if(result.getType() == HitTestResult.SRC_ANCHOR_TYPE)
copyUrlToClipboard(result.getExtra());
else if(result.getType() == WebView.HitTestResult.IMAGE_TYPE) {
String imageURL = result.getExtra();
ImageDownloadDialogBuilder builder = new ImageDownloadDialogBuilder(context,imageURL);
builder.show();
}
return false;
});
}
private void copyUrlToClipboard(String urlToCopy){
ClipboardManager clipboard = (ClipboardManager) BaseApplication.getInstance().getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("ReactiveWebViewCopiedText", urlToCopy);
clipboard.setPrimaryClip(clip);
Toast.makeText(BaseApplication.getInstance().getApplicationContext(),context.getString(R.string.link_copied_msg),Toast.LENGTH_SHORT).show();
}
}

232
app/src/main/java/gr/thmmy/mthmmy/views/RelativeTimeTextView.java

@ -0,0 +1,232 @@
package gr.thmmy.mthmmy.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import gr.thmmy.mthmmy.R;
import static gr.thmmy.mthmmy.utils.DateTimeUtils.getRelativeTimeSpanString;
/**
* A modified version of https://github.com/curioustechizen/android-ago
*/
@SuppressLint("AppCompatCustomView")
public class RelativeTimeTextView extends TextView {
private static final long INITIAL_UPDATE_INTERVAL = DateUtils.MINUTE_IN_MILLIS;
private long mReferenceTime;
private Handler mHandler = new Handler();
private UpdateTimeRunnable mUpdateTimeTask;
private boolean isUpdateTaskRunning = false;
public RelativeTimeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RelativeTimeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.RelativeTimeTextView, 0, 0);
String referenceTimeText;
try {
referenceTimeText = a.getString(R.styleable.RelativeTimeTextView_reference_time);
} finally {
a.recycle();
}
try {
mReferenceTime = Long.valueOf(referenceTimeText);
} catch (NumberFormatException nfe) {
/*
* TODO: Better exception handling
*/
mReferenceTime = -1L;
}
setReferenceTime(mReferenceTime);
}
/**
* Sets the reference time for this view. At any moment, the view will render a relative time period relative to the time set here.
* <p/>
* This value can also be set with the XML attribute {@code reference_time}
* @param referenceTime The timestamp (in milliseconds since epoch) that will be the reference point for this view.
*/
public void setReferenceTime(long referenceTime) {
this.mReferenceTime = referenceTime;
/*
* Note that this method could be called when a row in a ListView is recycled.
* Hence, we need to first stop any currently running schedules (for example from the recycled view.
*/
stopTaskForPeriodicallyUpdatingRelativeTime();
/*
* Instantiate a new runnable with the new reference time
*/
initUpdateTimeTask();
/*
* Start a new schedule.
*/
startTaskForPeriodicallyUpdatingRelativeTime();
/*
* Finally, update the text display.
*/
updateTextDisplay();
}
private void updateTextDisplay() {
/*
* TODO: Validation, Better handling of negative cases
*/
if (this.mReferenceTime == -1L)
return;
setText(getRelativeTimeSpanString(mReferenceTime));
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startTaskForPeriodicallyUpdatingRelativeTime();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopTaskForPeriodicallyUpdatingRelativeTime();
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == GONE || visibility == INVISIBLE) {
stopTaskForPeriodicallyUpdatingRelativeTime();
} else {
startTaskForPeriodicallyUpdatingRelativeTime();
}
}
private void startTaskForPeriodicallyUpdatingRelativeTime() {
if(mUpdateTimeTask.isDetached()) initUpdateTimeTask();
mHandler.post(mUpdateTimeTask);
isUpdateTaskRunning = true;
}
private void initUpdateTimeTask() {
mUpdateTimeTask = new UpdateTimeRunnable(this, mReferenceTime);
}
private void stopTaskForPeriodicallyUpdatingRelativeTime() {
if(isUpdateTaskRunning) {
mUpdateTimeTask.detach();
mHandler.removeCallbacks(mUpdateTimeTask);
isUpdateTaskRunning = false;
}
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.referenceTime = mReferenceTime;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
mReferenceTime = ss.referenceTime;
super.onRestoreInstanceState(ss.getSuperState());
}
public static class SavedState extends BaseSavedState {
private long referenceTime;
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(referenceTime);
}
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
private SavedState(Parcel in) {
super(in);
referenceTime = in.readLong();
}
}
private static class UpdateTimeRunnable implements Runnable {
private long mRefTime;
private final WeakReference<RelativeTimeTextView> weakRefRttv;
UpdateTimeRunnable(RelativeTimeTextView rttv, long refTime) {
this.mRefTime = refTime;
weakRefRttv = new WeakReference<>(rttv);
}
boolean isDetached() {
return weakRefRttv.get() == null;
}
void detach() {
weakRefRttv.clear();
}
@Override
public void run() {
RelativeTimeTextView rttv = weakRefRttv.get();
if (rttv == null) return;
long difference = Math.abs(System.currentTimeMillis() - mRefTime);
long interval = INITIAL_UPDATE_INTERVAL;
if (difference > DateUtils.WEEK_IN_MILLIS) {
interval = DateUtils.WEEK_IN_MILLIS;
} else if (difference > DateUtils.DAY_IN_MILLIS) {
interval = DateUtils.DAY_IN_MILLIS;
} else if (difference > DateUtils.HOUR_IN_MILLIS) {
interval = DateUtils.HOUR_IN_MILLIS;
}
rttv.updateTextDisplay();
rttv.mHandler.postDelayed(this, interval);
}
}
}

2
app/src/main/java/gr/thmmy/mthmmy/utils/ToggledBackgroundButton.java → app/src/main/java/gr/thmmy/mthmmy/views/ToggledBackgroundButton.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils;
package gr.thmmy.mthmmy.views;
import android.content.Context;
import android.util.AttributeSet;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/EditorView.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/EditorView.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview;
package gr.thmmy.mthmmy.views.editorview;
import android.animation.Animator;
import android.annotation.SuppressLint;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/EmojiInputField.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiInputField.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview;
package gr.thmmy.mthmmy.views.editorview;
import android.view.inputmethod.InputConnection;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/EmojiKeyboard.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiKeyboard.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview;
package gr.thmmy.mthmmy.views.editorview;
import android.content.Context;
import android.os.Handler;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/EmojiKeyboardAdapter.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiKeyboardAdapter.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview;
package gr.thmmy.mthmmy.views.editorview;
import android.graphics.drawable.AnimationDrawable;
import android.view.LayoutInflater;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/FormatButtonsAdapter.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/FormatButtonsAdapter.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview;
package gr.thmmy.mthmmy.views.editorview;
import android.view.LayoutInflater;
import android.view.View;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/IEmojiKeyboard.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/IEmojiKeyboard.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview;
package gr.thmmy.mthmmy.views.editorview;
public interface IEmojiKeyboard {
/**

5
app/src/main/res/drawable/ic_file_not_found.xml

@ -0,0 +1,5 @@
<vector android:height="96dp" android:tint="#D926A69A"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z"/>
</vector>

26
app/src/main/res/drawable/ic_launcher_foreground.xml

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1039.6812"
android:viewportHeight="1039.6812">
<group android:translateX="161.25058"
android:translateY="161.15057">
<path
android:pathData="M0,358.69a358.58,358.69 0,1 0,717.16 0a358.58,358.69 0,1 0,-717.16 0z"
android:fillColor="#333"/>
<group>
<clip-path
android:pathData="M0,358.69a358.58,358.69 0,1 0,717.16 0a358.58,358.69 0,1 0,-717.16 0z"/>
<path
android:pathData="M358.66,367.61c1,0 26,-75 37,-108.48 1.54,-4.68 7,-5.52 8.66,-0.27L493.66,473.03a4.29,4.29 0,0 0,7.93 0l27.28,-66.77a4.1,4.1 0,0 1,4 -2.43c61.43,0.08 122.87,-0.08 184.31,0"
android:strokeWidth="28"
android:fillColor="#00000000"
android:strokeColor="#6bdad5"/>
<path
android:pathData="M358.66,367.61c-1,0 -26,-75 -37,-108.48 -1.54,-4.68 -7,-5.52 -8.66,-0.27L223.66,473.03a4.29,4.29 0,0 1,-7.93 0l-27.28,-66.77a4.1,4.1 0,0 0,-4 -2.43c-61.43,0.08 -122.87,-0.08 -184.31,0"
android:strokeWidth="28"
android:fillColor="#00000000"
android:strokeColor="#6bdad5"/>
</group>
</group>
</vector>

5
app/src/main/res/drawable/ic_mark_as_read.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM2,16h8v-2L2,14v2zM21.5,11.5L23,13l-6.99,7 -4.51,-4.5L13,14l3.01,3 5.49,-5.5z"/>
</vector>

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

@ -37,7 +37,7 @@
android:id="@+id/user_thumbnail"
android:layout_width="@dimen/profile_activity_avatar_size"
android:layout_height="@dimen/profile_activity_avatar_size"
android:layout_marginBottom="5dp"
android:layout_marginBottom="6dp"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
@ -50,6 +50,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="6dp"
android:paddingBottom="4dp"
android:textAlignment="center"
android:textColor="@color/primary_text"
android:visibility="gone"/>
</LinearLayout>
@ -113,7 +118,7 @@
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:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_pm_fab"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

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

@ -225,7 +225,7 @@
android:layout_height="match_parent"
android:layout_marginBottom="16dp">
<WebView
<gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/post"
android:layout_width="match_parent"
android:layout_height="wrap_content"

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

@ -110,30 +110,54 @@
<TextView
android:id="@+id/apache_libs"
android:tag="APACHE"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/libraries_text"
android:onClick="displayApacheLibraries"
android:onClick="displayLibraries"
android:text="@string/apache_v2_0_libraries"
android:textColor="@color/accent" />
<TextView
android:id="@+id/mit_libs"
android:tag="MIT"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/apache_libs"
android:onClick="displayMITLibraries"
android:onClick="displayLibraries"
android:text="@string/the_mit_libraries"
android:textColor="@color/accent" />
<TextView
android:id="@+id/privacy_policy_header"
android:id="@+id/epl_libs"
android:tag="EPL"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/mit_libs"
android:onClick="displayLibraries"
android:text="@string/epl_libraries"
android:textColor="@color/accent" />
<TextView
android:id="@+id/other_libs"
android:tag="OTHER"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/epl_libs"
android:onClick="displayLibraries"
android:text="@string/other_libraries"
android:textColor="@color/accent" />
<TextView
android:id="@+id/privacy_policy_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/other_libs"
android:layout_marginTop="20dp"
android:text="@string/privacy_policy"
android:textAppearance="?android:attr/textAppearanceMedium"

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

@ -62,6 +62,6 @@
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:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_add_fab" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

4
app/src/main/res/layout/activity_create_content.xml

@ -46,7 +46,7 @@
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
<gr.thmmy.mthmmy.editorview.EditorView
<gr.thmmy.mthmmy.views.editorview.EditorView
android:id="@+id/main_content_editorview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -55,7 +55,7 @@
android:layout_marginEnd="16dp"
app:hint="topic message"/>
<gr.thmmy.mthmmy.editorview.EmojiKeyboard
<gr.thmmy.mthmmy.views.editorview.EmojiKeyboard
android:id="@+id/emoji_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"

2
app/src/main/res/layout/activity_downloads.xml

@ -55,6 +55,6 @@
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/fab_margins"
android:layout_marginEnd="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior"
app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_file_upload_white_24dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

9
app/src/main/res/layout/activity_profile.xml

@ -37,7 +37,7 @@
android:id="@+id/user_thumbnail"
android:layout_width="@dimen/profile_activity_avatar_size"
android:layout_height="@dimen/profile_activity_avatar_size"
android:layout_marginBottom="5dp"
android:layout_marginBottom="6dp"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
@ -49,6 +49,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="6dp"
android:paddingBottom="4dp"
android:textAlignment="center"
android:textColor="@color/primary_text"
android:visibility="gone"/>
</LinearLayout>
@ -112,7 +117,7 @@
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:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_pm_fab"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

6
app/src/main/res/layout/activity_topic.xml

@ -51,7 +51,7 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="gr.thmmy.mthmmy.activities.topic.TopicActivity" />
<gr.thmmy.mthmmy.editorview.EmojiKeyboard
<gr.thmmy.mthmmy.views.editorview.EmojiKeyboard
android:id="@+id/emoji_keyboard"
android:layout_width="match_parent"
android:layout_height="240dp"
@ -78,7 +78,7 @@
android:layout_gravity="bottom|end"
android:background="@color/primary"
app:elevation="8dp"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareLinearBehavior">
app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareLinearBehavior">
<ImageButton
android:id="@+id/page_first_button"
@ -148,7 +148,7 @@
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margins"
android:layout_marginBottom="50dp"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior"
app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_reply" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

2
app/src/main/res/layout/activity_topic_edit_row.xml

@ -76,7 +76,7 @@
android:textSize="10sp"
tools:ignore="SmallSp" />
</RelativeLayout>
<gr.thmmy.mthmmy.editorview.EditorView
<gr.thmmy.mthmmy.views.editorview.EditorView
android:id="@+id/edit_editorview"
android:layout_width="match_parent"
android:layout_height="wrap_content"

2
app/src/main/res/layout/activity_topic_post_row.xml

@ -225,7 +225,7 @@
android:layout_height="match_parent"
android:layout_marginBottom="16dp">
<WebView
<gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/post"
android:layout_width="match_parent"
android:layout_height="wrap_content"

2
app/src/main/res/layout/activity_topic_quick_reply_row.xml

@ -76,7 +76,7 @@
android:textSize="10sp"
tools:ignore="SmallSp" />
</RelativeLayout>
<gr.thmmy.mthmmy.editorview.EditorView
<gr.thmmy.mthmmy.views.editorview.EditorView
android:id="@+id/reply_editorview"
android:layout_width="match_parent"
android:layout_height="wrap_content"

6
app/src/main/res/layout/activity_upload.xml

@ -49,7 +49,7 @@
android:background="@color/primary_light"
android:orientation="vertical">
<gr.thmmy.mthmmy.utils.AppCompatSpinnerWithoutDefault
<gr.thmmy.mthmmy.views.AppCompatSpinnerWithoutDefault
android:id="@+id/upload_spinner_category_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -118,7 +118,7 @@
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<gr.thmmy.mthmmy.utils.ToggledBackgroundButton
<gr.thmmy.mthmmy.views.ToggledBackgroundButton
android:id="@+id/upload_title_description_builder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -195,7 +195,7 @@
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margins"
android:layout_marginBottom="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior"
app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_file_upload_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

15
app/src/main/res/layout/fragment_bookmarks.xml

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_height="match_parent">
@ -22,4 +22,15 @@
android:divider="?android:listDivider"
android:dividerPadding="16dp"/>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>
<TextView
android:id="@+id/nothing_bookmarked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="invisible"
android:textIsSelectable="false"
android:text="@string/nothing_bookmarked_here"
android:textColor="@color/accent"
android:textSize="@dimen/medium_text"
/>
</FrameLayout>

2
app/src/main/res/layout/fragment_forum.xml

@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<gr.thmmy.mthmmy.utils.CustomRecyclerView
<gr.thmmy.mthmmy.views.CustomRecyclerView
android:id="@+id/list"
android:name="gr.thmmy.mthmmy.sections.forum.ForumFragment"
android:layout_width="match_parent"

2
app/src/main/res/layout/fragment_latest_posts_row.xml

@ -50,7 +50,7 @@
android:layout_alignParentStart="true"
android:layout_below="@+id/spacer_divider">
<WebView
<gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/post"
android:layout_width="match_parent"
android:layout_height="wrap_content"

2
app/src/main/res/layout/fragment_recent.xml

@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<gr.thmmy.mthmmy.utils.CustomRecyclerView
<gr.thmmy.mthmmy.views.CustomRecyclerView
android:id="@+id/list"
android:name="gr.thmmy.mthmmy.sections.recent.RecentFragment"
android:layout_width="match_parent"

2
app/src/main/res/layout/fragment_recent_row.xml

@ -32,7 +32,7 @@
android:layout_below="@+id/title"
android:layout_toEndOf="@+id/dateTime"/>
<TextView
<gr.thmmy.mthmmy.views.RelativeTimeTextView
android:id="@+id/dateTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

7
app/src/main/res/layout/fragment_shoutbox.xml

@ -16,23 +16,24 @@
app:mpb_indeterminateTint="@color/accent"
app:mpb_progressStyle="horizontal" />
<gr.thmmy.mthmmy.utils.CustomRecyclerView
<gr.thmmy.mthmmy.views.CustomRecyclerView
android:id="@+id/shoutbox_recyclerview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:listitem="@layout/fragment_shoutbox_shout_row" />
<gr.thmmy.mthmmy.editorview.EditorView
<gr.thmmy.mthmmy.views.editorview.EditorView
android:id="@+id/edior_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:paddingTop="8dp"
android:visibility="gone"/>
<gr.thmmy.mthmmy.editorview.EmojiKeyboard
<gr.thmmy.mthmmy.views.editorview.EmojiKeyboard
android:id="@+id/emoji_keyboard"
android:layout_width="match_parent"
android:layout_height="180dp"

2
app/src/main/res/layout/fragment_shoutbox_shout_row.xml

@ -50,7 +50,7 @@
android:layout_marginBottom="9dp"
android:background="@color/divider" />
<WebView
<gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/shout_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"

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

Loading…
Cancel
Save