From e630fb2141a257594a7d0f96b07b062f09773a5f Mon Sep 17 00:00:00 2001 From: Sereri Date: Sun, 17 Dec 2023 16:47:18 +0100 Subject: [PATCH 1/7] 3.9.0 ; Make imgur use user account --- Awful.apk/build.gradle | 58 +++---- Awful.apk/src/main/AndroidManifest.xml | 4 +- Awful.apk/src/main/assets/changelog.html | 7 + .../java/com/ferg/awfulapp/Authentication.kt | 2 +- .../com/ferg/awfulapp/AwfulApplication.java | 24 --- .../java/com/ferg/awfulapp/AwfulFragment.kt | 2 +- .../ferg/awfulapp/CrashlyticsReportingTree.kt | 17 -- .../ferg/awfulapp/ForumsIndexFragment.java | 15 +- .../com/ferg/awfulapp/PostReplyActivity.java | 10 +- .../com/ferg/awfulapp/PostReplyFragment.java | 13 +- .../announcements/AnnouncementsFragment.java | 47 +++--- .../awfulapp/forums/ForumListAdapter.java | 135 +++++++-------- .../ferg/awfulapp/network/NetworkUtils.java | 1 + .../awfulapp/popupmenu/BasePopupMenu.java | 17 +- .../awfulapp/popupmenu/UrlContextMenu.java | 8 +- .../ferg/awfulapp/provider/AwfulProvider.java | 16 +- .../ferg/awfulapp/provider/AwfulTheme.java | 2 +- .../ferg/awfulapp/provider/ColorProvider.java | 2 +- .../ferg/awfulapp/reply/ImgurInserter.java | 155 +++++++++--------- .../ferg/awfulapp/search/SearchFragment.kt | 12 +- .../com/ferg/awfulapp/task/AwfulRequest.kt | 9 - .../com/ferg/awfulapp/util/AwfulUtils.java | 6 - .../com/ferg/awfulapp/widget/PageBar.java | 104 ++++++------ .../ferg/awfulapp/widget/ProbationBar.java | 34 ++-- .../com/ferg/awfulapp/widget/StatusFrog.java | 10 +- .../awfulapp/widget/ThreadIconPicker.java | 22 ++- .../src/main/res/layout/forum_index_item.xml | 4 +- .../res/layout/private_message_activity.xml | 1 + Awful.apk/src/main/res/layout/status_frog.xml | 4 +- Awful.apk/src/main/res/layout/thread_item.xml | 4 +- README.md | 8 +- build.gradle | 19 +++ gradle.properties | 3 +- gradle/wrapper/gradle-wrapper.properties | 3 +- settings.gradle | 15 ++ 35 files changed, 356 insertions(+), 437 deletions(-) delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/CrashlyticsReportingTree.kt diff --git a/Awful.apk/build.gradle b/Awful.apk/build.gradle index f19987ae4..057f95ab0 100644 --- a/Awful.apk/build.gradle +++ b/Awful.apk/build.gradle @@ -1,40 +1,31 @@ buildscript { - ext.kotlin_version = '1.7.20' + ext.kotlin_version = '1.9.21' - repositories { - google() - jcenter() - maven { url 'https://maven.fabric.io/public' } - } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:8.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.14' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' - } } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply plugin: 'com.google.gms.google-services' -apply plugin: 'com.google.firebase.crashlytics' - - -repositories { - google() - jcenter() - maven { url 'https://jitpack.io' } +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'com.google.devtools.ksp' } android { - compileSdkVersion 33 + namespace "com.ferg.awfulapp" + compileSdk 34 + + buildFeatures { + viewBinding = true + buildConfig = true + } defaultConfig { applicationId = "com.ferg.awfulapp" - minSdkVersion 21 - targetSdkVersion 33 + minSdkVersion 24 + targetSdkVersion 34 resConfigs 'en' // Stops the Gradle plugin’s automatic rasterization of vectors @@ -76,9 +67,8 @@ android { } } - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 + kotlin { + jvmToolchain(17) } task copyThreadTags { @@ -116,9 +106,9 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.11.0' // these are all needed to override some old versions that are dependencies... somewhere - implementation 'androidx.media:media:1.6.0' + implementation 'androidx.media:media:1.7.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' // used to fix SSL issues on older devices @@ -126,7 +116,7 @@ dependencies { implementation 'com.android.volley:volley:1.2.1' - implementation 'com.google.code.gson:gson:2.8.9' + implementation 'com.google.code.gson:gson:2.10' implementation 'org.jsoup:jsoup:1.15.4' implementation 'com.jakewharton.threetenabp:threetenabp:1.2.4' @@ -142,16 +132,12 @@ dependencies { implementation 'com.ToxicBakery.viewpager.transforms:view-pager-transforms:2.0.24@aar' implementation 'com.github.orangegangsters:swipy:1.2.3@aar' implementation 'com.bignerdranch.android:expandablerecyclerview:2.1.1' - implementation 'com.jakewharton:butterknife:10.2.1' implementation 'com.jakewharton.timber:timber:4.7.1' - implementation 'com.github.bumptech.glide:glide:4.11.0' - kapt 'com.github.bumptech.glide:compiler:4.11.0' + implementation 'com.github.bumptech.glide:glide:4.16.0' + ksp 'com.github.bumptech.glide:ksp:4.16.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0' - kapt 'com.jakewharton:butterknife-compiler:10.2.1' - - implementation 'com.google.firebase:firebase-crashlytics:18.4.3' implementation 'com.github.rubensousa:BottomSheetBuilder:1.5.1' diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index 21a04ccaf..5af3bda84 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -7,8 +7,8 @@ -->
+
+

3.9.2

+
    +
  • Removed crash report library because Google considers it collecting user data. Now collecting user data via the base functionality again like in 2012. Remember to include your username in the crash reports. Or don't, that's ok too.
  • +
  • Removed some older libraries that are no longer maintained. Something might have broken? If you see something, say something
  • +
+

3.9.1

    diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/Authentication.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/Authentication.kt index 8befd7bba..572ab87dc 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/Authentication.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/Authentication.kt @@ -23,7 +23,7 @@ import com.ferg.awfulapp.preferences.AwfulPreferences class LogOutDialog : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = - AlertDialog.Builder(activity!!) + AlertDialog.Builder(requireActivity()) .setTitle(R.string.logout) .setMessage(R.string.logout_message) .setPositiveButton(R.string.logout, { _, _ -> diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulApplication.java b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulApplication.java index 7511a84f4..248e1ced1 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulApplication.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulApplication.java @@ -7,7 +7,6 @@ import android.os.StrictMode; import android.webkit.WebView; -import com.google.firebase.crashlytics.FirebaseCrashlytics; import com.ferg.awfulapp.announcements.AnnouncementsManager; import com.ferg.awfulapp.constants.Constants; import com.ferg.awfulapp.network.NetworkUtils; @@ -26,7 +25,6 @@ public class AwfulApplication extends Application { * Used for storing misc app data, separate from user preferences, so onPreferenceChange callbacks aren't triggered */ private static SharedPreferences appStatePrefs; - private static boolean crashlyticsEnabled = false; /** * Stores the user agent used by web views in this application, which is required to be @@ -71,21 +69,6 @@ public void onCreate() { long hoursSinceInstall = getHoursSinceInstall(); - // enable Crashlytics on non-debug builds, or debug builds that have been installed for a while - crashlyticsEnabled = !BuildConfig.DEBUG || hoursSinceInstall > 4; - - if (crashlyticsEnabled) { - FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true); - FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance(); - - if (mPref.sendUsernameInReport) - crashlytics.setUserId(mPref.username); - } else { - FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false); - } - - Timber.plant(crashlyticsEnabled ? new CrashlyticsReportingTree() : new Timber.DebugTree()); - Timber.i("App installed %d hours ago", hoursSinceInstall); if (Constants.DEBUG) { @@ -120,13 +103,6 @@ private long getHoursSinceInstall() { return hoursSinceInstall; } - /** - * Returns true if the Crashlytics singleton has been initialised and can be used. - */ - public static boolean crashlyticsEnabled() { - return crashlyticsEnabled; - } - /** * Get the SharedPreferences used for storing basic app state. *

    diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt index c8c5d0530..23fe9703f 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt @@ -57,7 +57,7 @@ abstract class AwfulFragment : Fragment(), AwfulPreferences.AwfulPreferenceUpdat AwfulRequest.ProgressListener, ForumsPagerPage, NavigationEventHandler { protected var TAG = "AwfulFragment" - protected val prefs: AwfulPreferences by lazy { AwfulPreferences.getInstance(context!!, this) } + protected val prefs: AwfulPreferences by lazy { AwfulPreferences.getInstance(requireContext(), this) } protected val handler: Handler by lazy { Handler() } protected val alertView: AlertView by lazy { AlertView(activity) } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/CrashlyticsReportingTree.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/CrashlyticsReportingTree.kt deleted file mode 100644 index 8d88757d7..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/CrashlyticsReportingTree.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ferg.awfulapp - -import com.google.firebase.crashlytics.FirebaseCrashlytics -import timber.log.Timber - - -class CrashlyticsReportingTree : Timber.Tree() { - override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) { - val crashlytics = FirebaseCrashlytics.getInstance() - - crashlytics.log("$priority/$tag:$message") - - if (throwable != null) { - crashlytics.log(throwable.localizedMessage) - } - } -} \ No newline at end of file diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexFragment.java index 309c731a0..4e1b5f9ca 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexFragment.java @@ -5,6 +5,8 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import com.ferg.awfulapp.databinding.ForumIndexFragmentBinding; import com.google.android.material.snackbar.Snackbar; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -27,9 +29,6 @@ import java.util.ArrayList; import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; - import static com.ferg.awfulapp.forums.ForumStructure.FLAT; import static com.ferg.awfulapp.forums.ForumStructure.TWO_LEVEL; @@ -55,11 +54,8 @@ public class ForumsIndexFragment extends AwfulFragment private static final String KEY_SHOW_FAVOURITES = "show_favourites"; - @BindView(R.id.forum_index_list) RecyclerView forumRecyclerView; - @BindView(R.id.view_switcher) ViewSwitcher forumsListSwitcher; - @BindView(R.id.status_frog) StatusFrog statusFrog; private ForumListAdapter forumListAdapter; @@ -91,8 +87,11 @@ public void onCreate(Bundle savedInstanceState) { @Override public View onCreateView(@NonNull LayoutInflater aInflater, ViewGroup aContainer, Bundle aSavedState) { - View view = inflateView(R.layout.forum_index_fragment, aContainer, aInflater); - ButterKnife.bind(this, view); + ForumIndexFragmentBinding binding = ForumIndexFragmentBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + forumRecyclerView = binding.forumIndexList; + forumsListSwitcher = binding.viewSwitcher; + statusFrog = binding.statusFrog; updateViewColours(); refreshProbationBar(); forumsListSwitcher.setInAnimation(AnimationUtils.makeInAnimation(getContext(), true)); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyActivity.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyActivity.java index d622f219c..e043bdcc6 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyActivity.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyActivity.java @@ -32,21 +32,21 @@ import androidx.appcompat.widget.Toolbar; import android.view.MenuItem; -import butterknife.BindView; -import butterknife.ButterKnife; +import com.ferg.awfulapp.databinding.PostReplyActivityBinding; public class PostReplyActivity extends AwfulActivity { - @BindView(R.id.toolbar) + Toolbar mToolbar; PostReplyFragment replyFragment; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.post_reply_activity); - ButterKnife.bind(this); + PostReplyActivityBinding binding = PostReplyActivityBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + mToolbar = binding.toolbar; setSupportActionBar(mToolbar); setUpActionBar(); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java index a1f3328dc..aa8cb900b 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java @@ -48,6 +48,8 @@ import android.provider.MediaStore; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import com.ferg.awfulapp.databinding.PostReplyActivityBinding; import com.google.android.material.snackbar.Snackbar; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentManager; @@ -92,11 +94,10 @@ import org.apache.commons.lang3.StringUtils; import org.threeten.bp.Duration; import org.threeten.bp.Instant; +import org.w3c.dom.Text; import java.io.File; -import butterknife.BindView; -import butterknife.ButterKnife; import timber.log.Timber; import static com.ferg.awfulapp.constants.Constants.ATTACHMENT_MAX_BYTES; @@ -118,8 +119,6 @@ public class PostReplyFragment extends AwfulFragment { private static final String TAG = "PostReplyFragment"; // UI components - @BindView(R.id.thread_title) - TextView threadTitleView = null; private MessageComposer messageComposer; @Nullable private ProgressDialog progressDialog; @@ -172,16 +171,15 @@ public void onCreate(Bundle savedInstanceState) { public View onCreateView(LayoutInflater aInflater, ViewGroup aContainer, Bundle aSavedState) { super.onCreateView(aInflater, aContainer, aSavedState); Timber.v("onCreateView"); - return inflateView(R.layout.post_reply, aContainer, aInflater); + View view = inflateView(R.layout.post_reply, aContainer, aInflater); + return view; } - @Override public void onActivityCreated(Bundle aSavedState) { super.onActivityCreated(aSavedState); Timber.v("onActivityCreated"); Activity activity = getActivity(); - ButterKnife.bind(this, activity); messageComposer = (MessageComposer) getChildFragmentManager().findFragmentById(R.id.message_composer_fragment); messageComposer.setBackgroundColor(ColorProvider.BACKGROUND.getColor()); @@ -1042,6 +1040,7 @@ private void dismissProgressDialog() { * Update the title view to show the current thread title, if we have it */ private void updateThreadTitle() { + TextView threadTitleView = getActivity().findViewById(R.id.thread_title); if (threadTitleView != null) { threadTitleView.setText(mThreadTitle == null ? "" : mThreadTitle); } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/announcements/AnnouncementsFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/announcements/AnnouncementsFragment.java index 68cf49cce..f98505411 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/announcements/AnnouncementsFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/announcements/AnnouncementsFragment.java @@ -16,6 +16,7 @@ import com.android.volley.VolleyError; import com.ferg.awfulapp.AwfulFragment; import com.ferg.awfulapp.R; +import com.ferg.awfulapp.databinding.AnnouncementsFragmentBinding; import com.ferg.awfulapp.preferences.AwfulPreferences; import com.ferg.awfulapp.provider.AwfulTheme; import com.ferg.awfulapp.task.AnnouncementsRequest; @@ -28,8 +29,6 @@ import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; import timber.log.Timber; /** @@ -47,23 +46,19 @@ public class AnnouncementsFragment extends AwfulFragment { - @BindView(R.id.announcements_webview) - AwfulWebView webView; - @BindView(R.id.status_frog) - StatusFrog statusFrog; + AnnouncementsFragmentBinding binding; @NonNull @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflateView(R.layout.announcements_fragment, container, inflater); - ButterKnife.bind(this, view); - + binding = AnnouncementsFragmentBinding.inflate(inflater, container, false); + View view = binding.getRoot(); initialiseWebView(); return view; } private void initialiseWebView() { - webView.setJavascriptHandler(new WebViewJsInterface() { + binding.announcementsWebview.setJavascriptHandler(new WebViewJsInterface() { @JavascriptInterface public String getCSS() { @@ -93,7 +88,7 @@ public void resumeSwipe() { } }); - webView.setWebViewClient(new WebViewClient() { + binding.announcementsWebview.setWebViewClient(new WebViewClient() { // this lets links open back in the main activity if we handle them (e.g. 'look at this thread'), // and opens them in a browser or whatever if we don't (e.g. 'click here to buy a thing on the site') @Override @@ -104,7 +99,7 @@ public boolean shouldOverrideUrlLoading(WebView aView, String url) { return true; } }); - webView.setContent(AwfulHtmlPage.getContainerHtml(getPrefs(), -1, false)); + binding.announcementsWebview.setContent(AwfulHtmlPage.getContainerHtml(getPrefs(), -1, false)); } @@ -120,7 +115,7 @@ public void onActivityCreated(Bundle aSavedState) { */ private void showAnnouncements() { Context context = getContext().getApplicationContext(); - statusFrog.setStatusText(R.string.announcements_status_fetching).showSpinner(true); + binding.statusFrog.setStatusText(R.string.announcements_status_fetching).showSpinner(true); queueRequest( new AnnouncementsRequest(context).build(this, new AwfulRequest.AwfulResultCallback>() { @Override @@ -128,22 +123,22 @@ public void success(List result) { AnnouncementsManager.getInstance().markAllRead(); // update the status frog if there are no announcements, otherwise hide it and display them if (result.size() < 1) { - statusFrog.setStatusText(R.string.announcements_status_none).showSpinner(false); + binding.statusFrog.setStatusText(R.string.announcements_status_none).showSpinner(false); } else { - webView.setVisibility(View.VISIBLE); + binding.announcementsWebview.setVisibility(View.VISIBLE); // these page params don't mean anything in the context of the announcement page // we just want it to a) display ok, and b) not let the user click anything bad String bodyHtml = AwfulHtmlPage.getThreadHtml(result, AwfulPreferences.getInstance(), 1, 1); - if (webView != null) { - webView.setBodyHtml(bodyHtml); + if (binding.announcementsWebview != null) { + binding.announcementsWebview.setBodyHtml(bodyHtml); } - statusFrog.setVisibility(View.INVISIBLE); + binding.statusFrog.setVisibility(View.INVISIBLE); } } @Override public void failure(VolleyError error) { - statusFrog.setStatusText(R.string.announcements_status_failed).showSpinner(false); + binding.statusFrog.setStatusText(R.string.announcements_status_failed).showSpinner(false); Timber.w("Announcement get failed!\n" + error.getMessage()); } }) @@ -159,28 +154,28 @@ public String getTitle() { @Override public void onPause() { - if (webView != null) { - webView.onPause(); + if (binding.announcementsWebview != null) { + binding.announcementsWebview.onPause(); } super.onPause(); } @Override public void onResume() { - if (webView != null) { - webView.onResume(); + if (binding.announcementsWebview != null) { + binding.announcementsWebview.onResume(); } super.onResume(); } @Override protected boolean doScroll(boolean down) { - if (webView == null) { + if (binding.announcementsWebview == null) { return false; } else if (down) { - webView.pageDown(false); + binding.announcementsWebview.pageDown(false); } else { - webView.pageUp(false); + binding.announcementsWebview.pageUp(false); } return true; } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumListAdapter.java b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumListAdapter.java index ff5bfebde..58bedfaf1 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumListAdapter.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumListAdapter.java @@ -17,16 +17,14 @@ import com.bignerdranch.expandablerecyclerview.ViewHolder.ChildViewHolder; import com.bignerdranch.expandablerecyclerview.ViewHolder.ParentViewHolder; import com.ferg.awfulapp.R; +import com.ferg.awfulapp.databinding.ForumIndexItemBinding; +import com.ferg.awfulapp.databinding.ForumIndexSubforumItemBinding; import com.ferg.awfulapp.preferences.AwfulPreferences; import com.ferg.awfulapp.provider.ColorProvider; import java.util.ArrayList; import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; - import static android.view.View.GONE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; @@ -239,28 +237,8 @@ class TopLevelForumHolder extends ParentViewHolder { // list item sections - overall view, left column (tags etc), right column (details) private final View itemView; - @BindView(R.id.tag_and_dropdown_arrow) - View tagArea; - @BindView(R.id.forum_details) - View detailsArea; - // right column (used for forums) - @BindView(R.id.forum_title) - TextView title; - @BindView(R.id.forum_subtitle) - TextView subtitle; - @BindView(R.id.forum_favourite_marker) - ImageView favouriteMarker; - // left column (used for forums) - @BindView(R.id.subforums_expand_arrow) - ImageView dropdownButton; - @BindView(R.id.forum_tag) - SquareForumTag forumTag; - // section title (used for section headers) - @BindView(R.id.section_title) - TextView sectionTitle; - // the divider line - @BindView(R.id.list_divider) - View listDivider; + private ForumIndexItemBinding binding; + private Forum forum; private boolean hasSubforums; @@ -269,8 +247,9 @@ class TopLevelForumHolder extends ParentViewHolder { TopLevelForumHolder(View itemView) { super(itemView); this.itemView = itemView; - ButterKnife.bind(this, itemView); - detailsArea.setOnCreateContextMenuListener((contextMenu, view, contextMenuInfo) -> eventListener.onContextMenuCreated(forum, contextMenu)); + binding = ForumIndexItemBinding.bind(itemView); + + binding.forumDetails.setOnCreateContextMenuListener((contextMenu, view, contextMenuInfo) -> eventListener.onContextMenuCreated(forum, contextMenu)); } @@ -281,20 +260,20 @@ void bind(final TopLevelForum forumItem) { /* section items hide everything but the section title, other forum types hide the section title and show the other components. Think of of them as two alternative layouts in the same Layout file */ - tagArea.setVisibility(forum.isType(SECTION) ? GONE : VISIBLE); - detailsArea.setVisibility(forum.isType(SECTION) ? GONE : VISIBLE); - sectionTitle.setVisibility(forum.isType(SECTION) ? VISIBLE : GONE); + binding.tagAndDropdownArrow.setVisibility(forum.isType(SECTION) ? GONE : VISIBLE); + binding.forumDetails.setVisibility(forum.isType(SECTION) ? GONE : VISIBLE); + binding.sectionTitle.setVisibility(forum.isType(SECTION) ? VISIBLE : GONE); // hide the list divider for section titles and expanded parent forums boolean hideDivider = forum.isType(SECTION) || forumItem.isInitiallyExpanded(); - listDivider.setVisibility(hideDivider ? INVISIBLE : VISIBLE); + binding.listDivider.setVisibility(hideDivider ? INVISIBLE : VISIBLE); // sectionTitle is basically a differently formatted version of the title - setText(forum, title, subtitle, sectionTitle); - setThemeColours(itemView, title, subtitle); - handleSubtitles(forum, subtitle); + setText(forum, binding.forumTitle, binding.forumSubtitle, binding.sectionTitle); + setThemeColours(itemView, binding.forumTitle, binding.forumSubtitle); + handleSubtitles(forum, binding.forumSubtitle); - favouriteMarker.setVisibility(forum.isFavourite() ? VISIBLE : GONE); + binding.forumFavouriteMarker.setVisibility(forum.isFavourite() ? VISIBLE : GONE); /* the left section (potentially) has a tag and a dropdown button, anything missing is set to GONE so whatever's there gets vertically centred, and the space remains */ @@ -302,33 +281,38 @@ void bind(final TopLevelForum forumItem) { // if there's a forum tag then display it, otherwise remove it boolean hasForumTag = forum.getTagUrl() != null; if (hasForumTag) { - TagProvider.setSquareForumTag(forumTag, forum); - forumTag.setVisibility(View.VISIBLE); + TagProvider.setSquareForumTag(binding.forumTag, forum); + binding.forumTag.setVisibility(View.VISIBLE); } else { - forumTag.setVisibility(View.GONE); + binding.forumTag.setVisibility(View.GONE); } // if this item has subforums, show the dropdown and make it work, otherwise remove it if (hasSubforums) { - rotateDropdown(dropdownButton, !isExpanded(), true); - dropdownButton.setVisibility(VISIBLE); + rotateDropdown(binding.subforumsExpandArrow, !isExpanded(), true); + binding.subforumsExpandArrow.setVisibility(VISIBLE); } else { - dropdownButton.setVisibility(GONE); - } - } - - - @OnClick(R.id.tag_and_dropdown_arrow) - void toggleExpanded() { - if (hasSubforums) { - onClick(null); + binding.subforumsExpandArrow.setVisibility(GONE); } - } - - - @OnClick(R.id.forum_details) - void selectForum() { - eventListener.onForumClicked(forum); + binding.tagAndDropdownArrow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (hasSubforums) { + if (isExpanded()) { + collapseView(); + } else { + expandView(); + } + } + } + }); + + binding.forumDetails.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + eventListener.onForumClicked(forum); + } + }); } @@ -341,46 +325,37 @@ public boolean shouldItemViewClickToggleExpansion() { @Override public void onExpansionToggled(boolean closing) { super.onExpansionToggled(closing); - rotateDropdown(dropdownButton, closing, false); - listDivider.setVisibility(closing ? VISIBLE : INVISIBLE); + rotateDropdown(binding.subforumsExpandArrow, closing, false); + binding.listDivider.setVisibility(closing ? VISIBLE : INVISIBLE); } } class SubforumHolder extends ChildViewHolder { Forum forum; + ForumIndexSubforumItemBinding binding; - @BindView(R.id.forum_details) - View detailsArea; - @BindView(R.id.forum_title) - TextView title; - @BindView(R.id.forum_subtitle) - TextView subtitle; - @BindView(R.id.forum_favourite_marker) - ImageView favouriteMarker; - @BindView(R.id.item_container) - View itemLayout; SubforumHolder(View itemView) { super(itemView); - ButterKnife.bind(this, itemView); - detailsArea.setOnCreateContextMenuListener((contextMenu, view, contextMenuInfo) -> eventListener.onContextMenuCreated(forum, contextMenu)); + binding = ForumIndexSubforumItemBinding.bind(itemView); + binding.forumDetails.setOnCreateContextMenuListener((contextMenu, view, contextMenuInfo) -> eventListener.onContextMenuCreated(forum, contextMenu)); } void bind(final Forum forumItem) { forum = forumItem; - setText(forum, title, subtitle, null); - setThemeColours(itemLayout, title, subtitle); - handleSubtitles(forum, subtitle); - favouriteMarker.setVisibility(forum.isFavourite() ? VISIBLE : GONE); - } - - - @OnClick(R.id.forum_details) - void selectForum() { - eventListener.onForumClicked(forum); + setText(forum, binding.forumTitle, binding.forumSubtitle, null); + setThemeColours(binding.getRoot(), binding.forumTitle, binding.forumSubtitle); + handleSubtitles(forum, binding.forumSubtitle); + binding.forumFavouriteMarker.setVisibility(forum.isFavourite() ? VISIBLE : GONE); + binding.forumDetails.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + eventListener.onForumClicked(forum); + } + }); } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/network/NetworkUtils.java b/Awful.apk/src/main/java/com/ferg/awfulapp/network/NetworkUtils.java index 3cabc4854..9c498f17c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/network/NetworkUtils.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/network/NetworkUtils.java @@ -53,6 +53,7 @@ import timber.log.Timber; +@SuppressWarnings({"unchecked", "unsafe"}) public class NetworkUtils { private static final String CHARSET = "windows-1252"; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/BasePopupMenu.java b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/BasePopupMenu.java index 04531a2b0..ad8699aa0 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/BasePopupMenu.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/BasePopupMenu.java @@ -15,12 +15,11 @@ import android.widget.TextView; import com.ferg.awfulapp.R; +import com.ferg.awfulapp.databinding.ActionItemBinding; import com.ferg.awfulapp.provider.ColorProvider; import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; /** * Created by baka kaba on 22/05/2017. @@ -72,7 +71,6 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View result = inflater.inflate(layoutResId, container, false); - ButterKnife.bind(this, result); TextView actionTitle = result.findViewById(R.id.actionTitle); actionTitle.setMovementMethod(new ScrollingMovementMethod()); @@ -135,14 +133,11 @@ String getMenuLabel(@NonNull T action) { class ActionHolder extends RecyclerView.ViewHolder { - @BindView(R.id.actionTag) - ImageView actionTag; - @BindView(R.id.actionTitle) - TextView actionText; + ActionItemBinding binding; ActionHolder(View view) { super(view); - ButterKnife.bind(this, view); + binding = ActionItemBinding.bind(view); } } @@ -158,9 +153,9 @@ public ActionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) @Override public void onBindViewHolder(@NonNull ActionHolder holder, final int position) { final T action = menuItems.get(position); - holder.actionText.setText(getMenuLabel(action)); - holder.actionText.setTextColor(ColorProvider.PRIMARY_TEXT.getColor()); - holder.actionTag.setImageResource(action.getIconId()); + holder.binding.actionTitle.setText(getMenuLabel(action)); + holder.binding.actionTitle.setTextColor(ColorProvider.PRIMARY_TEXT.getColor()); + holder.binding.actionTag.setImageResource(action.getIconId()); holder.itemView.setOnClickListener(v -> { onActionClicked(action); if (onActionClickedListener != null) { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/UrlContextMenu.java b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/UrlContextMenu.java index 3be33108d..07d486225 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/UrlContextMenu.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/UrlContextMenu.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.List; -import butterknife.BindView; import static android.view.View.GONE; import static android.view.View.VISIBLE; @@ -52,9 +51,8 @@ public class UrlContextMenu extends BasePopupMenu private boolean isImage; private boolean isGif; - @BindView(R.id.actionTitle) TextView titleText; - @BindView(R.id.title_subheading) + TextView subheading = null; @Nullable private String subheadingText = null; @@ -93,8 +91,10 @@ void init(@NonNull Bundle args) { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); + titleText = view.findViewById(R.id.actionTitle); + subheading = view.findViewById(R.id.title_subheading); // tiny title text for long URLs - need to reapply the colour set in the xml - titleText.setTextAppearance(getContext(), R.style.TextAppearance_AppCompat_Small); + titleText.setTextAppearance(getContext(), androidx.appcompat.R.style.TextAppearance_AppCompat_Small); titleText.setTextColor(ColorProvider.ACTION_BAR_TEXT.getColor()); setSubheading(subheadingText); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java index c34f96ed4..2d9908a73 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java @@ -50,7 +50,6 @@ import com.ferg.awfulapp.thread.AwfulMessage; import com.ferg.awfulapp.thread.AwfulPost; import com.ferg.awfulapp.thread.AwfulThread; -import com.google.firebase.crashlytics.FirebaseCrashlytics; import java.util.Arrays; import java.util.HashMap; @@ -454,13 +453,7 @@ public Cursor query(@NonNull Uri aUri, String[] aProjection, String aSelection, if (uriType == UriMatcher.NO_MATCH) { String msg = String.format("Unrecognised query Uri!\nUri: %s\nProjection: %s\nSelection: %s\nSelection args: %s\nSort order: %s", aUri, Arrays.toString(aProjection), aSelection, Arrays.toString(aSelectionArgs), aSortOrder); - if (AwfulApplication.crashlyticsEnabled()) { - FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance(); - - crashlytics.log("WARN/" + TAG+ ": "+msg); - } else { - Log.w(TAG, msg); - } + Log.w(TAG, msg); return null; } @@ -533,12 +526,7 @@ public Cursor query(@NonNull Uri aUri, String[] aProjection, String aSelection, return result; } catch (Exception e) { String msg = String.format("aUri:\n%s\nQuery tables string:\n%s", aUri, builder.getTables()); - if (AwfulApplication.crashlyticsEnabled()){ - FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance(); - crashlytics.log("WARN/" + TAG+ ": "+msg); - } else{ - Log.w(TAG, msg, e); - } + Log.w(TAG, msg, e); throw e; } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulTheme.java b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulTheme.java index 18e0e17e1..bca8d1b9d 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulTheme.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulTheme.java @@ -214,7 +214,7 @@ public boolean isDark() { // initialise the dark theme flag - we're storing this per enum value, so we can avoid heavy processing TypedValue isLight = new TypedValue(); // this should pull the correct attribute from the thene - it's specified in the base platform themes - getTheme(AwfulPreferences.getInstance()).resolveAttribute(R.attr.isLightTheme, isLight, true); + getTheme(AwfulPreferences.getInstance()).resolveAttribute(androidx.appcompat.R.attr.isLightTheme, isLight, true); int FALSE = 0; isDark = (isLight.data == FALSE); } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/ColorProvider.java b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/ColorProvider.java index 421804df7..9c8f685c2 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/ColorProvider.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/ColorProvider.java @@ -29,7 +29,7 @@ public enum ColorProvider { PRIMARY_TEXT(R.attr.primaryPostFontColor), ALT_TEXT(R.attr.secondaryPostFontColor), - BACKGROUND(R.attr.background), + BACKGROUND(androidx.appcompat.R.attr.background), UNREAD_BACKGROUND(R.attr.unreadColor), UNREAD_BACKGROUND_DIM(R.attr.unreadColorDim), UNREAD_TEXT(R.attr.unreadFontColor), diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/reply/ImgurInserter.java b/Awful.apk/src/main/java/com/ferg/awfulapp/reply/ImgurInserter.java index b0af2a28d..8da0f779f 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/reply/ImgurInserter.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/reply/ImgurInserter.java @@ -16,11 +16,15 @@ import androidx.annotation.Nullable; import com.ferg.awfulapp.AwfulApplication; +import com.ferg.awfulapp.databinding.InsertImgurDialogBinding; import com.google.android.material.textfield.TextInputLayout; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.appcompat.app.AlertDialog; + +import android.text.Editable; +import android.text.TextWatcher; import android.text.format.DateFormat; import android.text.format.Formatter; import android.util.Log; @@ -28,6 +32,7 @@ import android.util.Patterns; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; @@ -50,14 +55,8 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; -import butterknife.BindView; -import butterknife.OnClick; -import butterknife.OnItemSelected; -import butterknife.OnTextChanged; - import static android.view.View.GONE; import static android.view.View.VISIBLE; -import static butterknife.ButterKnife.bind; /** * Created by baka kaba on 31/05/2017. @@ -78,37 +77,7 @@ public class ImgurInserter extends DialogFragment { private java.text.DateFormat dateFormat; private java.text.DateFormat timeFormat; - @BindView(R.id.upload_type) - Spinner uploadTypeSelector; - - @BindView(R.id.upload_image_section) - ViewGroup uploadImageSection; - @BindView(R.id.image_preview) - ImageView imagePreview; - @BindView(R.id.image_name) - TextView imageNameLabel; - @BindView(R.id.image_details) - TextView imageDetailsLabel; - - @BindView(R.id.upload_url_text_input_layout) - TextInputLayout uploadUrlTextWrapper; - @BindView(R.id.upload_url_edittext) - EditText uploadUrlEditText; - - @BindView(R.id.use_thumbnail) - CheckBox thumbnailCheckbox; - @BindView(R.id.add_gifs_as_video) - CheckBox gifsAsVideoCheckbox; - - @BindView(R.id.upload_status) - TextView uploadStatus; - @BindView(R.id.upload_progress_bar) - ProgressBar uploadProgressBar; - @BindView(R.id.remaining_uploads) - TextView remainingUploads; - @BindView(R.id.credits_reset_time) - TextView creditsResetTime; - + private InsertImgurDialogBinding binding; private Button uploadButton; Uri imageFile = null; @@ -126,7 +95,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { timeFormat = DateFormat.getTimeFormat(activity); View layout = activity.getLayoutInflater().inflate(R.layout.insert_imgur_dialog, null); - bind(this, layout); + binding = InsertImgurDialogBinding.bind(layout); AlertDialog dialog = new AlertDialog.Builder(activity) .setTitle(R.string.imgur_uploader_dialog_title) .setView(layout) @@ -138,9 +107,36 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { uploadButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); uploadButton.setOnClickListener(view -> startUpload()); // TODO: 05/06/2017 is that method guaranteed to be fired when the system creates the spinner and sets the first item? - uploadTypeSelector.setSelection(AwfulApplication.getAppStatePrefs().getInt(KEY_IMGUR_LAST_CHOSEN_UPLOAD_OPTION, 0)); + binding.uploadType.setSelection(AwfulApplication.getAppStatePrefs().getInt(KEY_IMGUR_LAST_CHOSEN_UPLOAD_OPTION, 0)); updateUploadType(); updateRemainingUploads(); + binding.uploadType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + updateUploadType(); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + binding.uploadImageSection.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + launchImagePicker(); + } + }); + binding.uploadUrlEdittext.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + onUrlTextChanged(); + } + }); return dialog; } @@ -167,10 +163,9 @@ private void cancelUploadTask() { /** * Check the currently selected upload type, and update state as necessary. */ - @OnItemSelected(R.id.upload_type) void updateUploadType() { // this assumes the first entry in the spinner is URL, and the second is IMAGE - int position = uploadTypeSelector.getSelectedItemPosition(); + int position = binding.uploadType.getSelectedItemPosition(); uploadSourceIsUrl = (position == 0); setState(State.CHOOSING); } @@ -183,13 +178,13 @@ void updateRemainingUploads() { Pair uploadLimit = ImgurUploadRequest.getCurrentUploadLimit(); Integer remaining = uploadLimit.first; Long resetTime = uploadLimit.second; - creditsResetTime.setText(resetTime == null ? "" : getString(R.string.imgur_uploader_remaining_uploads_reset_time, timeFormat.format(resetTime), dateFormat.format(resetTime))); + binding.creditsResetTime.setText(resetTime == null ? "" : getString(R.string.imgur_uploader_remaining_uploads_reset_time, timeFormat.format(resetTime), dateFormat.format(resetTime))); if (remaining == null) { - remainingUploads.setText(R.string.imgur_uploader_remaining_uploads_unknown); - creditsResetTime.setText(""); + binding.remainingUploads.setText(R.string.imgur_uploader_remaining_uploads_unknown); + binding.creditsResetTime.setText(""); } else { - remainingUploads.setText(getResources().getQuantityString(R.plurals.imgur_uploader_remaining_uploads, remaining, remaining)); + binding.remainingUploads.setText(getResources().getQuantityString(R.plurals.imgur_uploader_remaining_uploads, remaining, remaining)); if (remaining < 1) { setState(State.NO_UPLOAD_CREDITS); } @@ -204,7 +199,7 @@ void updateRemainingUploads() { /** * Display an image chooser to pick a file to upload. */ - @OnClick(R.id.upload_image_section) + void launchImagePicker() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT) .addCategory(Intent.CATEGORY_OPENABLE) @@ -234,7 +229,7 @@ private void onImageSelected(@NonNull Uri imageUri) { // check if this image looks invalid - if so, complain instead of using it String invalidReason = reasonImageIsInvalid(imageUri); if (invalidReason != null) { - uploadStatus.setText(invalidReason); + binding.uploadStatus.setText(invalidReason); return; } @@ -304,13 +299,13 @@ private Pair getFileNameAndSize(@NonNull Uri fileUri) { private void displayImageDetails(@NonNull Uri imageUri) { Pair nameAndSize = getFileNameAndSize(imageUri); if (nameAndSize.first == null && nameAndSize.second == null) { - imageNameLabel.setText(""); - imageDetailsLabel.setText(R.string.imgur_uploader_no_file_details); + binding.imageName.setText(""); + binding.imageDetails.setText(R.string.imgur_uploader_no_file_details); } else { String name = (nameAndSize.first == null) ? getString(R.string.imgur_uploader_unknown_value) : nameAndSize.first; - imageNameLabel.setText(getString(R.string.imgur_uploader_file_name, name)); + binding.imageName.setText(getString(R.string.imgur_uploader_file_name, name)); String size = (nameAndSize.second == null) ? getString(R.string.imgur_uploader_unknown_value) : Formatter.formatShortFileSize(getContext(), nameAndSize.second); - imageDetailsLabel.setText(getString(R.string.imgur_uploader_file_size, size)); + binding.imageDetails.setText(getString(R.string.imgur_uploader_file_size, size)); } } @@ -329,7 +324,7 @@ private void displayImagePreview(@NonNull Uri imageUri) { options.inSampleSize = 4; inputStream = getActivity().getContentResolver().openInputStream(imageUri); previewBitmap = BitmapFactory.decodeStream(inputStream, null, options); - imagePreview.setImageDrawable(new BitmapDrawable(previewBitmap)); + binding.imagePreview.setImageDrawable(new BitmapDrawable(previewBitmap)); } catch (FileNotFoundException e) { e.printStackTrace(); // TODO: 05/06/2017 'no preview' or something? @@ -345,12 +340,12 @@ private void displayImagePreview(@NonNull Uri imageUri) { /** * Handle changes to the 'image source URL' field, updating state where necessary. */ - @OnTextChanged(R.id.upload_url_edittext) + void onUrlTextChanged() { // change state when (and only when) the url contents no longer match the current state // this also avoids a circular call when the url is reset in #setState (url becomes empty // but state is already set appropriately to CHOOSING) - boolean urlIsEmpty = uploadUrlEditText.length() == 0; + boolean urlIsEmpty = binding.uploadUrlEdittext.length() == 0; if (urlIsEmpty && state == State.READY_TO_UPLOAD) { setState(State.CHOOSING); } else if (!urlIsEmpty && state == State.CHOOSING) { @@ -362,7 +357,7 @@ void onUrlTextChanged() { return; } - String url = uploadUrlEditText.getText().toString().toLowerCase(); + String url = binding.uploadUrlEdittext.getText().toString().toLowerCase(); boolean looksLikeUrl = Patterns.WEB_URL.matcher(url).matches(); String warningMessage = null; if (!StringUtils.startsWithAny(url, "http://", "https://")) { @@ -371,7 +366,7 @@ void onUrlTextChanged() { } else if (!looksLikeUrl) { warningMessage = getString(R.string.imgur_uploader_url_validation_warning); } - uploadUrlTextWrapper.setError(warningMessage); + binding.uploadUrlTextInputLayout.setError(warningMessage); } @@ -387,13 +382,13 @@ void startUpload() { if (state != State.READY_TO_UPLOAD) { return; } - AwfulApplication.getAppStatePrefs().edit().putInt(KEY_IMGUR_LAST_CHOSEN_UPLOAD_OPTION, uploadTypeSelector.getSelectedItemPosition()).apply(); + AwfulApplication.getAppStatePrefs().edit().putInt(KEY_IMGUR_LAST_CHOSEN_UPLOAD_OPTION, binding.uploadType.getSelectedItemPosition()).apply(); setState(State.UPLOADING); cancelUploadTask(); // do a url if we have one if (uploadSourceIsUrl) { - uploadTask = new ImgurUploadRequest(uploadUrlEditText.getText().toString(), this::parseUploadResponse, this::handleUploadError); + uploadTask = new ImgurUploadRequest(binding.uploadUrlEdittext.getText().toString(), this::parseUploadResponse, this::handleUploadError); NetworkUtils.queueRequest(uploadTask); } else { ContentResolver contentResolver = getActivity().getContentResolver(); @@ -435,10 +430,10 @@ private void parseUploadResponse(JSONObject response) { JSONObject data = response.getJSONObject("data"); String videoUrl = StringUtils.defaultIfBlank(data.optString("gifv"), data.optString("mp4")); String imageUrl = data.getString("link"); - if (gifsAsVideoCheckbox.isChecked() && StringUtils.isNotBlank(videoUrl)) { + if (binding.addGifsAsVideo.isChecked() && StringUtils.isNotBlank(videoUrl)) { ((MessageComposer) getTargetFragment()).onHtml5VideoUploaded(videoUrl); } else { - ((MessageComposer) getTargetFragment()).onImageUploaded(imageUrl, thumbnailCheckbox.isChecked()); + ((MessageComposer) getTargetFragment()).onImageUploaded(imageUrl, binding.useThumbnail.isChecked()); } dismiss(); return; @@ -463,7 +458,7 @@ private void parseUploadResponse(JSONObject response) { private void onUploadError(@NonNull String errorMessage) { // revert back to pre-upload state setState(State.READY_TO_UPLOAD); - uploadStatus.setText(errorMessage); + binding.uploadStatus.setText(errorMessage); updateRemainingUploads(); } @@ -532,45 +527,45 @@ private void setState(State newState) { // this intentionally sets the appearing view to visible BEFORE removing the other // which avoids too much weirdness with the layout change animation if (uploadSourceIsUrl) { - uploadUrlTextWrapper.setVisibility(VISIBLE); - uploadImageSection.setVisibility(GONE); + binding.uploadUrlTextInputLayout.setVisibility(VISIBLE); + binding.uploadImageSection.setVisibility(GONE); } else { - uploadImageSection.setVisibility(VISIBLE); - uploadUrlTextWrapper.setVisibility(GONE); + binding.uploadImageSection.setVisibility(VISIBLE); + binding.uploadUrlTextInputLayout.setVisibility(GONE); } - imagePreview.setImageResource(R.drawable.ic_photo_dark); - imageNameLabel.setText(""); - imageDetailsLabel.setText(R.string.imgur_uploader_tap_to_choose_file); - uploadUrlEditText.setText(""); - uploadUrlTextWrapper.setError(null); + binding.imagePreview.setImageResource(R.drawable.ic_photo_dark); + binding.imageName.setText(""); + binding.imageDetails.setText(R.string.imgur_uploader_tap_to_choose_file); + binding.uploadUrlEdittext.setText(""); + binding.uploadUrlTextInputLayout.setError(null); uploadButton.setEnabled(false); - uploadStatus.setText(uploadSourceIsUrl ? getString(R.string.imgur_uploader_status_enter_image_url) : getString(R.string.imgur_uploader_status_choose_source_file)); - uploadProgressBar.setVisibility(GONE); + binding.uploadStatus.setText(uploadSourceIsUrl ? getString(R.string.imgur_uploader_status_enter_image_url) : getString(R.string.imgur_uploader_status_choose_source_file)); + binding.uploadProgressBar.setVisibility(GONE); break; // upload source selected (either a URL entered, or a source file chosen) case READY_TO_UPLOAD: uploadButton.setEnabled(true); - uploadStatus.setText(R.string.imgur_uploader_status_ready_to_upload); - uploadProgressBar.setVisibility(GONE); + binding.uploadStatus.setText(R.string.imgur_uploader_status_ready_to_upload); + binding.uploadProgressBar.setVisibility(GONE); break; // upload request in progress case UPLOADING: uploadButton.setEnabled(false); - uploadStatus.setText(R.string.imgur_uploader_status_upload_in_progress); - uploadProgressBar.setVisibility(VISIBLE); + binding.uploadStatus.setText(R.string.imgur_uploader_status_upload_in_progress); + binding.uploadProgressBar.setVisibility(VISIBLE); break; // error state for when we can't upload case NO_UPLOAD_CREDITS: // put on the brakes, hide everything and prevent uploads uploadButton.setEnabled(false); - uploadStatus.setText(R.string.imgur_uploader_status_no_remaining_uploads); - uploadImageSection.setVisibility(GONE); - uploadUrlTextWrapper.setVisibility(GONE); - uploadProgressBar.setVisibility(GONE); + binding.uploadStatus.setText(R.string.imgur_uploader_status_no_remaining_uploads); + binding.uploadImageSection.setVisibility(GONE); + binding.uploadUrlTextInputLayout.setVisibility(GONE); + binding.uploadProgressBar.setVisibility(GONE); break; } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt index 88ade42f0..1c59b9b4e 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt @@ -64,7 +64,7 @@ import java.util.* class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout.OnRefreshListener { - private val mSearchQuery by lazy { view!!.findViewById(R.id.search_query) as EditText } + private val mSearchQuery by lazy { requireView().findViewById(R.id.search_query) as EditText } private var mQueryId: Int = 0 private var mMaxPageQueried: Int = 0 @@ -74,7 +74,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl private var mDialog: ProgressDialog? = null private val mSearchResultList: RecyclerView by lazy { - (view!!.findViewById(R.id.search_results) as RecyclerView) + (requireView().findViewById(R.id.search_results) as RecyclerView) .apply { adapter = SearchResultAdapter() layoutManager = @@ -84,7 +84,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl private var mSearchResults: MutableList = mutableListOf() private val mSRL: SwipyRefreshLayout by lazy { - (view!!.findViewById(R.id.search_srl) as SwipyRefreshLayout) + (requireView().findViewById(R.id.search_srl) as SwipyRefreshLayout) .apply { setOnRefreshListener(this@SearchFragment) setColorSchemeResources(*ColorProvider.getSRLProgressColors(null)) @@ -136,7 +136,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl private fun search() { mDialog = ProgressDialog.show(activity, getString(R.string.search_forums_active_dialog_title), getString(R.string.search_forums_active_dialog_message), true, false) val searchForumsPrimitive = ArrayUtils.toPrimitive(searchForums.toTypedArray()) - NetworkUtils.queueRequest(SearchRequest(this.context!!, mSearchQuery.text.toString().toLowerCase(), searchForumsPrimitive) + NetworkUtils.queueRequest(SearchRequest(this.requireContext(), mSearchQuery.text.toString().toLowerCase(), searchForumsPrimitive) .build(null, object : AwfulRequest.AwfulResultCallback { override fun success(result: AwfulSearchResult) { removeLoadingDialog() @@ -189,7 +189,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl R.id.select_forums -> SearchForumsFragment(this) .apply { setStyle(DialogFragment.STYLE_NO_TITLE, 0) } - .show(fragmentManager!!, "searchforums") + .show(requireFragmentManager(), "searchforums") else -> return super.onOptionsItemSelected(item) } return true @@ -218,7 +218,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl override fun onRefresh(direction: SwipyRefreshLayoutDirection) { Timber.i("onRefresh: %s", mMaxPageQueried) val preItemCount = mSearchResultList.adapter?.itemCount ?: 0 - NetworkUtils.queueRequest(SearchResultPageRequest(this.context!!, mQueryId, mMaxPageQueried + 1).build(null, object : AwfulRequest.AwfulResultCallback> { + NetworkUtils.queueRequest(SearchResultPageRequest(this.requireContext(), mQueryId, mMaxPageQueried + 1).build(null, object : AwfulRequest.AwfulResultCallback> { // TODO: combine this with #search since they share functionality - maybe a SearchQuery object for the current query that holds this state we're changing override fun success(result: ArrayList) { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt index c2b393a5e..e26a13ecd 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt @@ -20,7 +20,6 @@ import com.ferg.awfulapp.preferences.AwfulPreferences import com.ferg.awfulapp.task.AwfulRequest.Parameters.GetParams import com.ferg.awfulapp.task.AwfulRequest.Parameters.PostParams import com.ferg.awfulapp.util.AwfulError -import com.google.firebase.crashlytics.FirebaseCrashlytics import org.apache.http.HttpEntity import org.apache.http.entity.ContentType import org.apache.http.entity.mime.MultipartEntityBuilder @@ -259,14 +258,6 @@ abstract class AwfulRequest(protected val context: Context, private val baseU return Response.success(result, HttpHeaderParser.parseCacheHeaders(response)) } catch (ae: AwfulError) { return Response.error(ae) - } catch (e: OutOfMemoryError) { - if (AwfulApplication.crashlyticsEnabled()) { - val crashlytics: FirebaseCrashlytics = FirebaseCrashlytics.getInstance() - - crashlytics.setCustomKey("Response URL", url) - crashlytics.setCustomKey("Response data size", response.data.size.toLong()) - } - throw e } catch (e: Exception) { // TODO: find out what else this is meant to be catching, because it's swallowing every exception Timber.e(e, "Failed parse: $url") diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/util/AwfulUtils.java b/Awful.apk/src/main/java/com/ferg/awfulapp/util/AwfulUtils.java index 8d0170f19..2338d8047 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/util/AwfulUtils.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/util/AwfulUtils.java @@ -25,14 +25,12 @@ import com.ToxicBakery.viewpager.transforms.ZoomInTransformer; import com.ToxicBakery.viewpager.transforms.ZoomOutSlideTransformer; import com.ToxicBakery.viewpager.transforms.ZoomOutTransformer; -import com.ferg.awfulapp.AwfulApplication; import com.ferg.awfulapp.constants.Constants; import com.ferg.awfulapp.preferences.AwfulPreferences; import com.ferg.awfulapp.provider.DatabaseHelper; import com.ferg.awfulapp.thread.AwfulEmote; import com.ferg.awfulapp.thread.AwfulPost; import com.ferg.awfulapp.thread.AwfulThread; -import com.google.firebase.crashlytics.FirebaseCrashlytics; import java.util.HashMap; @@ -165,10 +163,6 @@ public static boolean contains(int[] intArray, int value) { */ public static void failSilently(@NonNull Exception e) { Timber.e(e); - if (AwfulApplication.crashlyticsEnabled()) { - FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance(); - crashlytics.log(e.getMessage()); - } if (Constants.DEBUG) { throw new RuntimeException(e); } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/PageBar.java b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/PageBar.java index c2f8399f9..918c5bf72 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/PageBar.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/PageBar.java @@ -14,13 +14,10 @@ import android.widget.TextView; import com.ferg.awfulapp.R; +import com.ferg.awfulapp.databinding.PageBarBinding; import java.util.Locale; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; - /** * Created by baka kaba on 25/05/2016. *

    @@ -31,16 +28,7 @@ public class PageBar extends FrameLayout { public static final int FIRST_PAGE = 1; - @BindView(R.id.page_count_text) - TextView mPageCountText; - @BindView(R.id.next_page) - ImageButton nextPageButton; - @BindView(R.id.prev_page) - ImageButton prevPageButton; - @BindView(R.id.refresh) - ImageButton refreshButton; - @BindView(R.id.refresh_alt) - ImageButton altRefreshButton; + PageBarBinding binding; private PageBarCallbacks listener = null; @@ -67,9 +55,13 @@ public PageBar(Context context, AttributeSet attrs, int defStyleAttr, int defSty private void init() { - View pageBar = LayoutInflater.from(getContext()).inflate(R.layout.page_bar, this, true); - ButterKnife.bind(pageBar); + binding = PageBarBinding.inflate(LayoutInflater.from(getContext()), this, true); updatePagePosition(FIRST_PAGE, FIRST_PAGE); + onRefreshClicked(binding.refresh); + onRefreshClicked(binding.refreshAlt); + onNavButtonClicked(binding.nextPage); + onNavButtonClicked(binding.prevPage); + onPageNumberClicked(binding.pageCountText); } /** @@ -102,7 +94,7 @@ public void updatePagePosition(int currentPage, int lastPage) { private void updateDisplay(int currentPage, int lastPage, @NonNull PageType pageType, boolean hasPageCount) { String template = hasPageCount ? "%d / %d" : "%d"; - mPageCountText.setText(String.format(Locale.getDefault(), template, currentPage, lastPage)); + binding.pageCountText.setText(String.format(Locale.getDefault(), template, currentPage, lastPage)); /* hide and show the appropriate icons for each state: - don't show the prev/next arrow on the first/last page @@ -112,28 +104,28 @@ private void updateDisplay(int currentPage, int lastPage, @NonNull PageType page */ switch (pageType) { case SINGLE: - prevPageButton.setVisibility(GONE); - nextPageButton.setVisibility(GONE); - altRefreshButton.setVisibility(GONE); - refreshButton.setVisibility(VISIBLE); + binding.prevPage.setVisibility(GONE); + binding.nextPage.setVisibility(GONE); + binding.refreshAlt.setVisibility(GONE); + binding.refresh.setVisibility(VISIBLE); break; case FIRST_OF_MANY: - prevPageButton.setVisibility(GONE); - altRefreshButton.setVisibility(GONE); - refreshButton.setVisibility(VISIBLE); - nextPageButton.setVisibility(VISIBLE); + binding.prevPage.setVisibility(GONE); + binding.refreshAlt.setVisibility(GONE); + binding.refresh.setVisibility(VISIBLE); + binding.nextPage.setVisibility(VISIBLE); break; case ONE_OF_MANY: - altRefreshButton.setVisibility(GONE); - prevPageButton.setVisibility(VISIBLE); - refreshButton.setVisibility(VISIBLE); - nextPageButton.setVisibility(VISIBLE); + binding.refreshAlt.setVisibility(GONE); + binding.prevPage.setVisibility(VISIBLE); + binding.refresh.setVisibility(VISIBLE); + binding.nextPage.setVisibility(VISIBLE); break; case LAST_OF_MANY: - nextPageButton.setVisibility(GONE); - refreshButton.setVisibility(GONE); - prevPageButton.setVisibility(VISIBLE); - altRefreshButton.setVisibility(VISIBLE); + binding.nextPage.setVisibility(GONE); + binding.refresh.setVisibility(GONE); + binding.prevPage.setVisibility(VISIBLE); + binding.refreshAlt.setVisibility(VISIBLE); } } @@ -145,26 +137,38 @@ public void setListener(@Nullable PageBarCallbacks listener) { this.listener = listener; } - @OnClick({R.id.refresh, R.id.refresh_alt}) - public void onRefreshClicked() { - if (listener != null) { - listener.onRefreshClicked(); - } + public void onRefreshClicked(ImageButton button) { + button.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onRefreshClicked(); + } + } + }); } - @OnClick({R.id.next_page, R.id.prev_page}) - public void onNavButtonClicked(View view) { - if (listener != null) { - listener.onPageNavigation(view.getId() == R.id.next_page); - } + public void onNavButtonClicked(ImageButton button) { + button.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onPageNavigation(button.getId() == R.id.next_page); + } + } + }); } + public void onPageNumberClicked(TextView pageNumber) { + pageNumber.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onPageNumberClicked(); + } + } + }); - @OnClick({R.id.page_count_text}) - public void onPageNumberClicked() { - if (listener != null) { - listener.onPageNumberClicked(); - } } // TODO: probably best to add a setter for the stuff that uses this @@ -174,11 +178,11 @@ public void onPageNumberClicked() { */ @NonNull public View getTextView() { - return mPageCountText; + return binding.pageCountText; } public void setTextColour(@ColorInt int textColour) { - mPageCountText.setTextColor(textColour); + binding.pageCountText.setTextColor(textColour); } private enum PageType {SINGLE, FIRST_OF_MANY, LAST_OF_MANY, ONE_OF_MANY} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ProbationBar.java b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ProbationBar.java index 7e10e24bd..2de9ed31b 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ProbationBar.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ProbationBar.java @@ -11,14 +11,11 @@ import android.widget.TextView; import com.ferg.awfulapp.R; +import com.ferg.awfulapp.databinding.ProbationBarBinding; import java.text.DateFormat; import java.util.Date; -import butterknife.BindString; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; /** * Created by baka kaba on 25/05/2016. @@ -30,10 +27,7 @@ */ public class ProbationBar extends LinearLayout { - @BindView(R.id.probation_message) - TextView mProbationMessageView; - @BindString(R.string.probation_message) - String probationMessage; + ProbationBarBinding binding; @Nullable private Callbacks listener = null; @@ -60,8 +54,15 @@ public ProbationBar(Context context, AttributeSet attrs, int defStyleAttr, int d } private void init() { - View probationBar = LayoutInflater.from(getContext()).inflate(R.layout.probation_bar, this, true); - ButterKnife.bind(probationBar); + binding = ProbationBarBinding.inflate(LayoutInflater.from(getContext())); + binding.goToLC.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onProbationButtonClicked(); + } + } + }); } @@ -82,19 +83,12 @@ public void setListener(@Nullable Callbacks listener) { */ public void setProbation(@Nullable Long probationTime) { if (probationTime == null) { - setVisibility(View.GONE); + this.setVisibility(View.GONE); return; } - setVisibility(VISIBLE); + this.setVisibility(VISIBLE); String probeEnd = DateFormat.getDateTimeInstance().format(new Date(probationTime)); - mProbationMessageView.setText(String.format(probationMessage, probeEnd)); - } - - @OnClick(R.id.go_to_LC) - public void onProbationButtonClicked() { - if (listener != null) { - listener.onProbationButtonClicked(); - } + binding.probationMessage.setText(String.format(binding.getRoot().getResources().getString(R.string.probation_message), probeEnd)); } public interface Callbacks { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/StatusFrog.java b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/StatusFrog.java index c3aa7f0ac..576fdc907 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/StatusFrog.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/StatusFrog.java @@ -15,8 +15,6 @@ import com.ferg.awfulapp.R; -import butterknife.BindView; -import butterknife.ButterKnife; /** * Created by baka kaba on 14/08/2017. @@ -30,9 +28,9 @@ */ public class StatusFrog extends RelativeLayout { - @BindView(R.id.status_message) + TextView statusMessage; - @BindView(R.id.status_progress_bar) + ProgressBar progressBar; public StatusFrog(Context context) { @@ -59,7 +57,9 @@ public StatusFrog(Context context, AttributeSet attrs, int defStyleAttr, int def private void init(Context context, @Nullable AttributeSet attrs) { View view = LayoutInflater.from(context).inflate(R.layout.status_frog, this, true); - ButterKnife.bind(this, view); + statusMessage = view.findViewById(R.id.status_message); + progressBar = view.findViewById(R.id.status_progress_bar); + // handle any custom XML attributes if (attrs != null) { TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.StatusFrog, 0, 0); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ThreadIconPicker.java b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ThreadIconPicker.java index 38e552a8b..4089e3cf6 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ThreadIconPicker.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ThreadIconPicker.java @@ -31,10 +31,6 @@ import java.util.Iterator; import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.OnLongClick; import static com.ferg.awfulapp.constants.Constants.POST_ICON_REQUEST_TYPES.FORUM_POST; import static com.ferg.awfulapp.constants.Constants.POST_ICON_REQUEST_TYPES.PM; @@ -64,7 +60,6 @@ public class ThreadIconPicker extends Fragment { private static final int PM_FORUM_ID = -324546; private static final SparseArray> iconsCache = new SparseArray<>(); - @BindView(R.id.selected_icon) ImageView selectedIconView; private AwfulPostIcon currentIcon = BLANK_ICON; @Nullable @@ -75,7 +70,20 @@ public class ThreadIconPicker extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.icon_picker, container, true); - ButterKnife.bind(this, view); + selectedIconView = view.findViewById(R.id.selected_icon); + selectedIconView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showPicker(); + } + }); + selectedIconView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + secretForumCycler(); + return true; + } + }); return view; } @@ -118,7 +126,6 @@ public void usePrivateMessageIcons() { * If an icon source hasn't been selected through {@link #useForumIcons(int)} or * {@link #usePrivateMessageIcons()}, this will do nothing. */ - @OnClick(R.id.selected_icon) public void showPicker() { if (currentForumId == null) { Log.w(TAG, "The user tried to select an icon before a source forum was set!\nYou should prevent this or initialise with one"); @@ -227,7 +234,6 @@ private Menu generatePostIconMenu(@NonNull List postIcons) { Iterator allTheForums = null; - @OnLongClick(R.id.selected_icon) public boolean secretForumCycler() { if (allTheForums == null || !allTheForums.hasNext()) { ForumRepository repo = ForumRepository.getInstance(getContext()); diff --git a/Awful.apk/src/main/res/layout/forum_index_item.xml b/Awful.apk/src/main/res/layout/forum_index_item.xml index d4b9d9bb6..ca80f872d 100644 --- a/Awful.apk/src/main/res/layout/forum_index_item.xml +++ b/Awful.apk/src/main/res/layout/forum_index_item.xml @@ -44,10 +44,10 @@ android:scaleType="fitCenter" android:scaleX="1.3" android:scaleY="1.3" - android:tint="?primaryPostFontColor" android:visibility="visible" app:srcCompat="@drawable/ic_expand_more" - tools:ignore="MissingPrefix" /> + tools:ignore="MissingPrefix" + app:tint="?primaryPostFontColor" /> diff --git a/Awful.apk/src/main/res/layout/private_message_activity.xml b/Awful.apk/src/main/res/layout/private_message_activity.xml index 212431e94..0be382d84 100644 --- a/Awful.apk/src/main/res/layout/private_message_activity.xml +++ b/Awful.apk/src/main/res/layout/private_message_activity.xml @@ -1,6 +1,7 @@ + tools:ignore="ContentDescription" + app:tint="?attr/secondaryPostFontColor" /> + tools:visibility="visible" + app:tint="?attr/iconColorDark" /> diff --git a/README.md b/README.md index 3744736d7..77d15d19c 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,7 @@ Or take a look at [our issues][issues] and set up your own fork to quash some bu 2. [Fork Awful.apk on Github][github-fork-howto]. 3. Download your fork's Git repository and open the project in Android Studio. * Quick way: from the welcome screen, choose `Get from VCS` and enter the Github URL for your new fork. -4. Create your own `google-services.json`. - 1. [Create your own Firebase project][firebase-console] for Awful. - 2. Add an Android app to your Firebase project. - 3. For Android package name, enter `com.ferg.awfulapp.debug`. - 4. Download the `google-services.json` config file. - 5. Place `google-services.json` in the inner `/Awful.apk/` folder. -5. __(Optional)__ If you'd like to be able to upload images to Imgur: +4. __(Optional)__ If you'd like to be able to upload images to Imgur: 1. [Register a new application for the Imgur API][imgur-api-docs]. 2. Create the file [`secrets.xml`][secrets-example] in `/Awful.apk/src/main/res/values/`. 3. Place the client ID in `secrets.xml`, like so: diff --git a/build.gradle b/build.gradle index 495c5038e..589c9043f 100644 --- a/build.gradle +++ b/build.gradle @@ -1 +1,20 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext { + agp_version = '8.2.0' + } + repositories { + google() + jcenter() + maven { url 'https://jitpack.io' } + } + dependencies { + classpath "com.android.tools.build:gradle:$agp_version" + } +} +plugins { + id 'com.android.application' version '8.2.0' apply false + id 'com.android.library' version '8.2.0' apply false + id 'org.jetbrains.kotlin.android' version '1.9.21' apply false + id 'com.google.devtools.ksp' version '1.9.21-1.0.15' +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 72849b52a..f1d85932f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ org.gradle.jvmargs=-Xmx2048m org.gradle.caching=true android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true +android.nonFinalResIds = false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7e5645e7d..cadc0ed7c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun Dec 17 12:34:58 CET 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index f89cea603..2302aa9a5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + maven { url 'https://jitpack.io' } + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url 'https://jitpack.io' } + } +} include ':Awful.apk' From 64b62067e1dda944fb5a1d400677fc5ec717864d Mon Sep 17 00:00:00 2001 From: Sereri Date: Sat, 20 Apr 2024 17:42:28 +0200 Subject: [PATCH 2/7] Fix the fucking imgur upload. Fuck. --- .../main/java/com/ferg/awfulapp/task/ImgurUploadRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImgurUploadRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImgurUploadRequest.java index c339bc908..4f8ae1420 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImgurUploadRequest.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImgurUploadRequest.java @@ -146,7 +146,7 @@ public ImgurUploadRequest(@NonNull InputStream imageStream, @NonNull Response.Listener jsonResponseListener, @Nullable Response.ErrorListener errorListener) { this(true, jsonResponseListener, errorListener); - attachParams.addBinaryBody("image", imageStream); + attachParams.addBinaryBody("image", imageStream, ContentType.DEFAULT_BINARY, "Awful image"); httpEntity = attachParams.build(); } From 605cfc9915bf571d50d1b1f1807c95a6d54d3034 Mon Sep 17 00:00:00 2001 From: Sereri Date: Sat, 20 Apr 2024 17:56:54 +0200 Subject: [PATCH 3/7] 3.9.2 ; Make imgur use user account --- Awful.apk/src/main/assets/changelog.html | 1 + 1 file changed, 1 insertion(+) diff --git a/Awful.apk/src/main/assets/changelog.html b/Awful.apk/src/main/assets/changelog.html index e504b9c57..b2130e722 100644 --- a/Awful.apk/src/main/assets/changelog.html +++ b/Awful.apk/src/main/assets/changelog.html @@ -8,6 +8,7 @@

    3.9.2

      +
    • Made imgur upload work again. You can finally uninstall that awful app. No, I mean theirs. I should have written terrible app, that's clearer.
    • Removed crash report library because Google considers it collecting user data. Now collecting user data via the base functionality again like in 2012. Remember to include your username in the crash reports. Or don't, that's ok too.
    • Removed some older libraries that are no longer maintained. Something might have broken? If you see something, say something
    From a1ec0efb56f3f00f1352e5a388d2f936430d1e9b Mon Sep 17 00:00:00 2001 From: Sereri Date: Mon, 20 May 2024 13:20:53 +0200 Subject: [PATCH 4/7] 3.9.5 replace zoom thing --- Awful.apk/build.gradle | 5 - Awful.apk/src/main/AndroidManifest.xml | 4 +- Awful.apk/src/main/assets/changelog.html | 8 +- Awful.apk/src/main/assets/css/amberpos.css | 10 + Awful.apk/src/main/assets/css/general.css | 49 ++++ Awful.apk/src/main/assets/css/oled.css | 8 + Awful.apk/src/main/assets/css/yospos.css | 9 + .../src/main/assets/javascript/hammer.js | 7 + .../src/main/assets/javascript/thread.js | 226 ++++++++++++++---- .../com/ferg/awfulapp/ImageViewFragment.kt | 77 ------ .../ferg/awfulapp/ThreadDisplayFragment.java | 16 +- .../ferg/awfulapp/thread/AwfulHtmlPage.java | 3 +- .../main/res/layout/image_view_fragment.xml | 13 - 13 files changed, 285 insertions(+), 150 deletions(-) create mode 100644 Awful.apk/src/main/assets/javascript/hammer.js delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/ImageViewFragment.kt delete mode 100644 Awful.apk/src/main/res/layout/image_view_fragment.xml diff --git a/Awful.apk/build.gradle b/Awful.apk/build.gradle index 057f95ab0..1cb1c103c 100644 --- a/Awful.apk/build.gradle +++ b/Awful.apk/build.gradle @@ -134,11 +134,6 @@ dependencies { implementation 'com.bignerdranch.android:expandablerecyclerview:2.1.1' implementation 'com.jakewharton.timber:timber:4.7.1' - implementation 'com.github.bumptech.glide:glide:4.16.0' - ksp 'com.github.bumptech.glide:ksp:4.16.0' - - implementation 'com.github.chrisbanes:PhotoView:2.3.0' - implementation 'com.github.rubensousa:BottomSheetBuilder:1.5.1' testImplementation 'junit:junit:4.12' diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index 5af3bda84..038a52ddc 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -7,8 +7,8 @@ -->
    -

    3.9.2

    +

    3.9.5

    +
      +
    • Replaced the display image zoom thing so that it doesn't just enlarge the image but also actually zooms in. The wonders of modern technology.
    • +
    +
    +
    +

    3.9.4

    • Made imgur upload work again. You can finally uninstall that awful app. No, I mean theirs. I should have written terrible app, that's clearer.
    • Removed crash report library because Google considers it collecting user data. Now collecting user data via the base functionality again like in 2012. Remember to include your username in the crash reports. Or don't, that's ok too.
    • diff --git a/Awful.apk/src/main/assets/css/amberpos.css b/Awful.apk/src/main/assets/css/amberpos.css index e14a5cec9..dd52e92c6 100644 --- a/Awful.apk/src/main/assets/css/amberpos.css +++ b/Awful.apk/src/main/assets/css/amberpos.css @@ -80,4 +80,14 @@ body { */ .postmenu:after { content: "\e901"; +} + + +#zoom-close { + background: black; + border: 2px solid #eacf4c; +} + +#zoom-close:after { + color: #eacf4c; } \ No newline at end of file diff --git a/Awful.apk/src/main/assets/css/general.css b/Awful.apk/src/main/assets/css/general.css index 34343e3b4..5ab959448 100644 --- a/Awful.apk/src/main/assets/css/general.css +++ b/Awful.apk/src/main/assets/css/general.css @@ -317,4 +317,53 @@ video.playing ~ .video-link { .postcontent .signature { padding-top: 10px; +} + +#zoom { + display: none; +} + +#zoom img { + display: block; + max-width:100%; + max-height:100%; + cursor: move; + touch-action: none; + } + +#zoom.zoom-enabled { + z-index: 1; + background-color: black; + position: fixed; + overflow: hidden; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#zoom-close { + position: fixed; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + top: 20px; + right: 20px; + background: #00000099; + width: 50px; + height: 50px; + z-index: 2; +} + +#zoom-close:after { + display: block; + content: '\00d7'; + font-size: 50px; + color: white; } \ No newline at end of file diff --git a/Awful.apk/src/main/assets/css/oled.css b/Awful.apk/src/main/assets/css/oled.css index 207fa2df3..9dfc8c7b3 100644 --- a/Awful.apk/src/main/assets/css/oled.css +++ b/Awful.apk/src/main/assets/css/oled.css @@ -97,4 +97,12 @@ body { height: 1px; background-color: #e7e7e7; margin: 0px; +} + +#zoom-close { + background: black; +} + +#zoom-close:after { + color: #e7e7e7; } \ No newline at end of file diff --git a/Awful.apk/src/main/assets/css/yospos.css b/Awful.apk/src/main/assets/css/yospos.css index 2291f6d4b..af39594a5 100644 --- a/Awful.apk/src/main/assets/css/yospos.css +++ b/Awful.apk/src/main/assets/css/yospos.css @@ -83,4 +83,13 @@ body { */ .postmenu:after { content: "\e901"; +} + +#zoom-close { + background: black; + border: 2px solid #0F0; +} + +#zoom-close:after { + color: #0F0; } \ No newline at end of file diff --git a/Awful.apk/src/main/assets/javascript/hammer.js b/Awful.apk/src/main/assets/javascript/hammer.js new file mode 100644 index 000000000..edadee159 --- /dev/null +++ b/Awful.apk/src/main/assets/javascript/hammer.js @@ -0,0 +1,7 @@ +/*! Hammer.JS - v2.0.8 - 2016-04-23 + * http://hammerjs.github.io/ + * + * Copyright (c) 2016 Jorik Tangelder; + * Licensed under the MIT license */ +!function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(j(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",f=a.console&&(a.console.warn||a.console.log);return f&&f.call(a.console,e,d),b.apply(this,arguments)}}function i(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&la(d,c)}function j(a,b){return function(){return a.apply(b,arguments)}}function k(a,b){return typeof a==oa?a.apply(b?b[0]||d:d,b):a}function l(a,b){return a===d?b:a}function m(a,b,c){g(q(b),function(b){a.addEventListener(b,c,!1)})}function n(a,b,c){g(q(b),function(b){a.removeEventListener(b,c,!1)})}function o(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function p(a,b){return a.indexOf(b)>-1}function q(a){return a.trim().split(/\s+/g)}function r(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;dc[b]}):d.sort()),d}function u(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g1&&!c.firstMultiple?c.firstMultiple=D(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=E(d);b.timeStamp=ra(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=I(h,i),b.distance=H(h,i),B(c,b),b.offsetDirection=G(b.deltaX,b.deltaY);var j=F(b.deltaTime,b.deltaX,b.deltaY);b.overallVelocityX=j.x,b.overallVelocityY=j.y,b.overallVelocity=qa(j.x)>qa(j.y)?j.x:j.y,b.scale=g?K(g.pointers,d):1,b.rotation=g?J(g.pointers,d):0,b.maxPointers=c.prevInput?b.pointers.length>c.prevInput.maxPointers?b.pointers.length:c.prevInput.maxPointers:b.pointers.length,C(c,b);var k=a.element;o(b.srcEvent.target,k)&&(k=b.srcEvent.target),b.target=k}function B(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};b.eventType!==Ea&&f.eventType!==Ga||(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function C(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Ha&&(i>Da||h.velocity===d)){var j=b.deltaX-h.deltaX,k=b.deltaY-h.deltaY,l=F(i,j,k);e=l.x,f=l.y,c=qa(l.x)>qa(l.y)?l.x:l.y,g=G(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function D(a){for(var b=[],c=0;ce;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:pa(c/b),y:pa(d/b)}}function F(a,b,c){return{x:b/a||0,y:c/a||0}}function G(a,b){return a===b?Ia:qa(a)>=qa(b)?0>a?Ja:Ka:0>b?La:Ma}function H(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function I(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function J(a,b){return I(b[1],b[0],Ra)+I(a[1],a[0],Ra)}function K(a,b){return H(b[0],b[1],Ra)/H(a[0],a[1],Ra)}function L(){this.evEl=Ta,this.evWin=Ua,this.pressed=!1,x.apply(this,arguments)}function M(){this.evEl=Xa,this.evWin=Ya,x.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function N(){this.evTarget=$a,this.evWin=_a,this.started=!1,x.apply(this,arguments)}function O(a,b){var c=s(a.touches),d=s(a.changedTouches);return b&(Ga|Ha)&&(c=t(c.concat(d),"identifier",!0)),[c,d]}function P(){this.evTarget=bb,this.targetIds={},x.apply(this,arguments)}function Q(a,b){var c=s(a.touches),d=this.targetIds;if(b&(Ea|Fa)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=s(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return o(a.target,i)}),b===Ea)for(e=0;e-1&&d.splice(a,1)};setTimeout(e,cb)}}function U(a){for(var b=a.srcEvent.clientX,c=a.srcEvent.clientY,d=0;d=f&&db>=g)return!0}return!1}function V(a,b){this.manager=a,this.set(b)}function W(a){if(p(a,jb))return jb;var b=p(a,kb),c=p(a,lb);return b&&c?jb:b||c?b?kb:lb:p(a,ib)?ib:hb}function X(){if(!fb)return!1;var b={},c=a.CSS&&a.CSS.supports;return["auto","manipulation","pan-y","pan-x","pan-x pan-y","none"].forEach(function(d){b[d]=c?a.CSS.supports("touch-action",d):!0}),b}function Y(a){this.options=la({},this.defaults,a||{}),this.id=v(),this.manager=null,this.options.enable=l(this.options.enable,!0),this.state=nb,this.simultaneous={},this.requireFail=[]}function Z(a){return a&sb?"cancel":a&qb?"end":a&pb?"move":a&ob?"start":""}function $(a){return a==Ma?"down":a==La?"up":a==Ja?"left":a==Ka?"right":""}function _(a,b){var c=b.manager;return c?c.get(a):a}function aa(){Y.apply(this,arguments)}function ba(){aa.apply(this,arguments),this.pX=null,this.pY=null}function ca(){aa.apply(this,arguments)}function da(){Y.apply(this,arguments),this._timer=null,this._input=null}function ea(){aa.apply(this,arguments)}function fa(){aa.apply(this,arguments)}function ga(){Y.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}function ha(a,b){return b=b||{},b.recognizers=l(b.recognizers,ha.defaults.preset),new ia(a,b)}function ia(a,b){this.options=la({},ha.defaults,b||{}),this.options.inputTarget=this.options.inputTarget||a,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=a,this.input=y(this),this.touchAction=new V(this,this.options.touchAction),ja(this,!0),g(this.options.recognizers,function(a){var b=this.add(new a[0](a[1]));a[2]&&b.recognizeWith(a[2]),a[3]&&b.requireFailure(a[3])},this)}function ja(a,b){var c=a.element;if(c.style){var d;g(a.options.cssProps,function(e,f){d=u(c.style,f),b?(a.oldCssProps[d]=c.style[d],c.style[d]=e):c.style[d]=a.oldCssProps[d]||""}),b||(a.oldCssProps={})}}function ka(a,c){var d=b.createEvent("Event");d.initEvent(a,!0,!0),d.gesture=c,c.target.dispatchEvent(d)}var la,ma=["","webkit","Moz","MS","ms","o"],na=b.createElement("div"),oa="function",pa=Math.round,qa=Math.abs,ra=Date.now;la="function"!=typeof Object.assign?function(a){if(a===d||null===a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),c=1;ch&&(b.push(a),h=b.length-1):e&(Ga|Ha)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Za={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},$a="touchstart",_a="touchstart touchmove touchend touchcancel";i(N,x,{handler:function(a){var b=Za[a.type];if(b===Ea&&(this.started=!0),this.started){var c=O.call(this,a,b);b&(Ga|Ha)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}}});var ab={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},bb="touchstart touchmove touchend touchcancel";i(P,x,{handler:function(a){var b=ab[a.type],c=Q.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}});var cb=2500,db=25;i(R,x,{handler:function(a,b,c){var d=c.pointerType==za,e=c.pointerType==Ba;if(!(e&&c.sourceCapabilities&&c.sourceCapabilities.firesTouchEvents)){if(d)S.call(this,b,c);else if(e&&U.call(this,c))return;this.callback(a,b,c)}},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var eb=u(na.style,"touchAction"),fb=eb!==d,gb="compute",hb="auto",ib="manipulation",jb="none",kb="pan-x",lb="pan-y",mb=X();V.prototype={set:function(a){a==gb&&(a=this.compute()),fb&&this.manager.element.style&&mb[a]&&(this.manager.element.style[eb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){k(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),W(a.join(" "))},preventDefaults:function(a){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=p(d,jb)&&!mb[jb],f=p(d,lb)&&!mb[lb],g=p(d,kb)&&!mb[kb];if(e){var h=1===a.pointers.length,i=a.distance<2,j=a.deltaTime<250;if(h&&i&&j)return}return g&&f?void 0:e||f&&c&Na||g&&c&Oa?this.preventSrc(b):void 0},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var nb=1,ob=2,pb=4,qb=8,rb=qb,sb=16,tb=32;Y.prototype={defaults:{},set:function(a){return la(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=_(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=_(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=_(a,this),-1===r(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=_(a,this);var b=r(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(b,a)}var c=this,d=this.state;qb>d&&b(c.options.event+Z(d)),b(c.options.event),a.additionalEvent&&b(a.additionalEvent),d>=qb&&b(c.options.event+Z(d))},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=tb)},canEmit:function(){for(var a=0;af?Ja:Ka,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Ia:0>g?La:Ma,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return aa.prototype.attrTest.call(this,a)&&(this.state&ob||!(this.state&ob)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=$(a.direction);b&&(a.additionalEvent=this.options.event+b),this._super.emit.call(this,a)}}),i(ca,aa,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&ob)},emit:function(a){if(1!==a.scale){var b=a.scale<1?"in":"out";a.additionalEvent=this.options.event+b}this._super.emit.call(this,a)}}),i(da,Y,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[hb]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distanceb.time;if(this._input=a,!d||!c||a.eventType&(Ga|Ha)&&!f)this.reset();else if(a.eventType&Ea)this.reset(),this._timer=e(function(){this.state=rb,this.tryEmit()},b.time,this);else if(a.eventType&Ga)return rb;return tb},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===rb&&(a&&a.eventType&Ga?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=ra(),this.manager.emit(this.options.event,this._input)))}}),i(ea,aa,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&ob)}}),i(fa,aa,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Na|Oa,pointers:1},getTouchAction:function(){return ba.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Na|Oa)?b=a.overallVelocity:c&Na?b=a.overallVelocityX:c&Oa&&(b=a.overallVelocityY),this._super.attrTest.call(this,a)&&c&a.offsetDirection&&a.distance>this.options.threshold&&a.maxPointers==this.options.pointers&&qa(b)>this.options.velocity&&a.eventType&Ga},emit:function(a){var b=$(a.offsetDirection);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),i(ga,Y,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[ib]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance e.preventDefault(), false); + displayDefaultWidth = image.offsetWidth; + displayDefaultHeight = image.offsetHeight; + rangeX = Math.max(0, displayDefaultWidth - containerWidth); + rangeY = Math.max(0, displayDefaultHeight - containerHeight); + } + + + function updateImage(x, y, scale) { + const transform = 'translateX(' + x + 'px) translateY(' + y + 'px) translateZ(0px) scale(' + scale + ',' + scale + ')'; + image.style.transform = transform; + } + + function updateRange() { + rangeX = Math.max(0, Math.round(displayDefaultWidth * imageCurrentScale) - containerWidth); + rangeY = Math.max(0, Math.round(displayDefaultHeight * imageCurrentScale) - containerHeight); + + rangeMaxX = Math.round(rangeX / 2); + rangeMinX = 0 - rangeMaxX; + + rangeMaxY = Math.round(rangeY / 2); + rangeMinY = 0 - rangeMaxY; + } + + const hammertime = new Hammer(zoom,{ inputClass: Hammer.TouchMouseInput }); + + hammertime.get('pinch').set({ enable: true }); + hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL }); + + hammertime.on('pan', function(ev) { + imageCurrentX = clamp(imageX + ev.deltaX, rangeMinX, rangeMaxX); + imageCurrentY = clamp(imageY + ev.deltaY, rangeMinY, rangeMaxY); + updateImage(imageCurrentX, imageCurrentY, imageScale); + }); + + hammertime.on('pinch pinchmove',function (ev) { + imageCurrentScale = clampScale(ev.scale * imageScale); + updateRange(); + imageCurrentX = clamp(imageX + ev.deltaX, rangeMinX, rangeMaxX); + imageCurrentY = clamp(imageY + ev.deltaY, rangeMinY, rangeMaxY); + updateImage(imageCurrentX, imageCurrentY, imageCurrentScale); + }); + + hammertime.on('panend pancancel pinchend pinchcancel', function(){ + imageScale = imageCurrentScale; + imageX = imageCurrentX; + imageY = imageCurrentY; + }); + listener.setZoomEnabled(true); +} + +/** + * Exists the zoom overlay + */ +function exitImageZoom() { + if(!document.getElementById('zoom')){ return } + document.getElementById('zoom').remove(); + document.getElementById('zoom-close').remove(); + listener.resumeSwipe(); + listener.setZoomEnabled(false); +} + /** * Load an image url and replace links with the image. Handles paused gifs and basic text links. * @param {String} url The image URL @@ -392,13 +524,13 @@ function freezeGif(image) { * @param {Element} image Gif image to monitor */ function prepareFreezeGif(image) { - if (!image.complete) { - image.addEventListener('load', function freezeLoadHandler() { - freezeGif(image); - }); - } else { - freezeGif(image); - } + if (!image.complete) { + image.addEventListener('load', function freezeLoadHandler() { + freezeGif(image); + }); + } else { + freezeGif(image); + } } /** @@ -467,9 +599,9 @@ function handleQuoteLink(link, event) { * @param {Element} info The HTMLElement of the postinfo */ function toggleInfo(info) { - var posterTitle = info.querySelector('.postinfo-title'); - var posterRegDate = info.querySelector('.postinfo-regdate'); - if (!posterTitle) { return; } + var posterTitle = info.querySelector('.postinfo-title'); + var posterRegDate = info.querySelector('.postinfo-regdate'); + if (!posterTitle) { return; } if (posterTitle.classList.contains('extended')) { if (info.querySelector('.avatar') !== null) { @@ -484,9 +616,9 @@ function toggleInfo(info) { posterTitle.classList.remove('extended'); posterTitle.setAttribute('aria-hidden', 'true'); if (posterRegDate) { - posterRegDate.classList.remove('extended'); - posterRegDate.setAttribute('aria-hidden', 'true'); - } + posterRegDate.classList.remove('extended'); + posterRegDate.setAttribute('aria-hidden', 'true'); + } } else { if (info.querySelector('.avatar') !== null) { if (info.querySelector('canvas') !== null) { @@ -503,9 +635,9 @@ function toggleInfo(info) { posterTitle.classList.add('extended'); posterTitle.setAttribute('aria-hidden', 'false'); if (posterRegDate) { - posterRegDate.classList.add('extended'); - posterRegDate.setAttribute('aria-hidden', 'false'); - } + posterRegDate.classList.add('extended'); + posterRegDate.setAttribute('aria-hidden', 'false'); + } } } @@ -514,7 +646,7 @@ function toggleInfo(info) { * @param {Element} postMenu The HTMLElement of the postmenu */ function showPostMenu(postMenu) { -// temp hack to create the right menu for rap sheet entries without making its own CSS class etc + // temp hack to create the right menu for rap sheet entries without making its own CSS class etc if (postMenu.hasAttribute('badPostUrl')) { showPunishmentMenu(postMenu); return; @@ -716,16 +848,16 @@ function handleTouchLeave() { * Hides all instances of the given avatar on the page */ function hideAvatar(avatarUrl) { - document.querySelectorAll('[src="' + avatarUrl + '"]').forEach(function(avatarTag) { - avatarTag.classList.add('hide-avatar'); - }); + document.querySelectorAll('[src="' + avatarUrl + '"]').forEach(function (avatarTag) { + avatarTag.classList.add('hide-avatar'); + }); } /** * Shows all instances of the given avatar on the page */ function showAvatar(avatarUrl) { - document.querySelectorAll('[src="' + avatarUrl + '"]').forEach(function(avatarTag) { - avatarTag.classList.remove('hide-avatar'); - }); + document.querySelectorAll('[src="' + avatarUrl + '"]').forEach(function (avatarTag) { + avatarTag.classList.remove('hide-avatar'); + }); } \ No newline at end of file diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ImageViewFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/ImageViewFragment.kt deleted file mode 100644 index 3c6c52641..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ImageViewFragment.kt +++ /dev/null @@ -1,77 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2011, Scott Ferguson - * All rights reserved. - - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name of the software nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - - * THIS SOFTWARE IS PROVIDED BY SCOTT FERGUSON ''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 SCOTT FERGUSON 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. - */ - -package com.ferg.awfulapp - -import android.graphics.Bitmap -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import com.bumptech.glide.Glide - - - - - -/** - * Loads and displays an image in a zoomable view. - */ -class ImageViewFragment : AwfulFragment() { - - private var imageUrl = "No image url" - - companion object { - const val EXTRA_IMAGE_URL = "image url" - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflateView(R.layout.image_view_fragment, container, inflater) - } - - override fun onActivityCreated(aSavedState: Bundle?) { - super.onActivityCreated(aSavedState) - val mImageView = requireActivity().findViewById(R.id.iv_photo) as ImageView - val imageUrl = requireActivity().intent.getStringExtra(EXTRA_IMAGE_URL); - - Glide - .with(mImageView) - .load(imageUrl) - .into(mImageView); - - - - setActionBarTitle(imageUrl!!) - } - - override fun getTitle(): String = imageUrl -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java index e92b76d0c..9e75a622f 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java @@ -189,6 +189,8 @@ Potentially null views, if layout inflation failed (i.e. the WebView package is private final LinkedList backStack = new LinkedList<>(); private boolean bypassBackStack = false; + private boolean zoomEnabled = false; + private String mTitle = null; private String postJump = ""; private int savedScrollPosition = 0; @@ -1212,6 +1214,11 @@ public void resumeSwipe() { ((ForumsIndexActivity)mSelf.getAwfulActivity()).allowSwipe(); } + @JavascriptInterface + public void setZoomEnabled(boolean zoomOn) { + zoomEnabled = zoomOn; + } + @JavascriptInterface public void popupText(String text) { Toast.makeText(getActivity(), text, Toast.LENGTH_SHORT).show(); @@ -1327,13 +1334,10 @@ public void startUrlIntent(String url){ .setSubtitle("None of your apps want to open this " + intentUri.getScheme() + ":\\\\ link. Try installing an app that is less picky") .show(); } - } public void displayImage(String url){ - Intent intent = BasicActivity.Companion.intentFor(ImageViewFragment.class, getActivity(), ""); - intent.putExtra(ImageViewFragment.EXTRA_IMAGE_URL, url); - startActivity(intent); + mThreadView.runJavascript(String.format("showImageZoom('%s')", url)); } @Override @@ -1758,6 +1762,10 @@ private int backStackCount(){ @Override public boolean onBackPressed() { + if(zoomEnabled) { + mThreadView.runJavascript("exitImageZoom()"); + return true; + } if(backStackCount() > 0){ popThread(); return true; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java index d69ca5ead..75f1b66e3 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java @@ -60,7 +60,8 @@ public abstract class AwfulHtmlPage { "longtap.js", "jsonp.js", "embedding.js", - "thread.js" + "thread.js", + "hammer.js" }; /** diff --git a/Awful.apk/src/main/res/layout/image_view_fragment.xml b/Awful.apk/src/main/res/layout/image_view_fragment.xml deleted file mode 100644 index 9e386b987..000000000 --- a/Awful.apk/src/main/res/layout/image_view_fragment.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file From 4f7dced1f07c874f93dbb58c4f99ebc7b523cf5b Mon Sep 17 00:00:00 2001 From: Sereri Date: Thu, 23 May 2024 17:50:43 +0200 Subject: [PATCH 5/7] 3.9.6, first draft of the thing --- Awful.apk/src/main/AndroidManifest.xml | 9 +- Awful.apk/src/main/assets/changelog.html | 3 +- .../java/com/ferg/awfulapp/AwfulActivity.kt | 24 +- .../java/com/ferg/awfulapp/AwfulFragment.kt | 4 + .../ferg/awfulapp/ForumDisplayFragment.java | 40 + .../com/ferg/awfulapp/PostThreadActivity.java | 82 ++ .../com/ferg/awfulapp/PostThreadFragment.java | 1070 +++++++++++++++++ .../ferg/awfulapp/constants/Constants.java | 8 +- .../preferences/AwfulPreferences.java | 3 + .../com/ferg/awfulapp/preferences/Keys.java | 3 + .../ferg/awfulapp/provider/AwfulProvider.java | 38 +- .../awfulapp/provider/DatabaseHelper.java | 24 +- .../awfulapp/task/PreviewThreadRequest.kt | 50 + .../ferg/awfulapp/task/SendThreadRequest.kt | 40 + .../com/ferg/awfulapp/task/ThreadRequest.kt | 36 + .../ferg/awfulapp/thread/AwfulMessage.java | 7 + .../ferg/awfulapp/thread/AwfulPostIcon.java | 2 +- .../java/com/ferg/awfulapp/thread/Thread.java | 105 ++ .../awfulapp/widget/ThreadIconPicker.java | 4 + .../src/main/res/drawable/ic_just_post.xml | 5 + Awful.apk/src/main/res/layout/post_thread.xml | 14 + .../main/res/layout/post_thread_activity.xml | 75 ++ .../main/res/menu/forum_display_fragment.xml | 10 + Awful.apk/src/main/res/menu/post_thread.xml | 34 + .../src/main/res/values/preference_keys.xml | 1 + Awful.apk/src/main/res/values/strings.xml | 2 + 26 files changed, 1677 insertions(+), 16 deletions(-) create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadActivity.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewThreadRequest.kt create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SendThreadRequest.kt create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadRequest.kt create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/thread/Thread.java create mode 100644 Awful.apk/src/main/res/drawable/ic_just_post.xml create mode 100644 Awful.apk/src/main/res/layout/post_thread.xml create mode 100644 Awful.apk/src/main/res/layout/post_thread_activity.xml create mode 100644 Awful.apk/src/main/res/menu/forum_display_fragment.xml create mode 100644 Awful.apk/src/main/res/menu/post_thread.xml diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index 038a52ddc..f71c07183 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -7,8 +7,8 @@ --> + diff --git a/Awful.apk/src/main/assets/changelog.html b/Awful.apk/src/main/assets/changelog.html index 9d4c0b98a..067d1fbf7 100644 --- a/Awful.apk/src/main/assets/changelog.html +++ b/Awful.apk/src/main/assets/changelog.html @@ -6,9 +6,10 @@
      -

      3.9.5

      +

      3.9.6

      • Replaced the display image zoom thing so that it doesn't just enlarge the image but also actually zooms in. The wonders of modern technology.
      • +
      • Added function to increase the number of bad threads on the forums. May dog have mercy on our souls.
      diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt index 85d8861ab..a4f4a5463 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt @@ -250,11 +250,25 @@ abstract class AwfulActivity : AppCompatActivity(), AwfulPreferences.AwfulPrefer fun showPostComposer(threadId: Int, postType: Int, sourcePostId: Int) { // TODO: this should probably all be refactored into types like the NavigationEvents (maybe even rolled in with them) - discrete Posting events with the specific associated data for each startActivityForResult( - Intent(this, PostReplyActivity::class.java) - .putExtra(Constants.REPLY_THREAD_ID, threadId) - .putExtra(Constants.EDITING, postType) - .putExtra(Constants.REPLY_POST_ID, sourcePostId), - PostReplyFragment.REQUEST_POST + Intent(this, PostReplyActivity::class.java) + .putExtra(Constants.REPLY_THREAD_ID, threadId) + .putExtra(Constants.EDITING, postType) + .putExtra(Constants.REPLY_POST_ID, sourcePostId), + PostReplyFragment.REQUEST_POST + ) + } + + /** + * Display the thread composer. + * + * @param forumId the ID of the forum the thread is in + */ + fun showThreadComposer(forumId: Int) { + // TODO: this should probably all be refactored into types like the NavigationEvents (maybe even rolled in with them) - discrete Posting events with the specific associated data for each + startActivityForResult( + Intent(this, PostThreadActivity::class.java) + .putExtra(Constants.POST_FORUM_ID, forumId), + PostThreadFragment.REQUEST_THREAD ) } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt index 23fe9703f..9a06a75df 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt @@ -139,6 +139,10 @@ abstract class AwfulFragment : Fragment(), AwfulPreferences.AwfulPreferenceUpdat awfulActivity?.apply { runOnUiThread { showPostComposer(threadId, type, postId) } } } + fun displayPostThreadDialog(forumId: Int) { + awfulActivity?.apply { runOnUiThread { showThreadComposer(forumId) } } + } + protected fun setProgress(percent: Int) { progressPercent = percent if (progressPercent > 0) { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java index ce0b4a247..2bbf9f04c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java @@ -36,12 +36,15 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -53,6 +56,7 @@ import com.ferg.awfulapp.constants.Constants; import com.ferg.awfulapp.network.NetworkUtils; import com.ferg.awfulapp.preferences.AwfulPreferences; +import com.ferg.awfulapp.preferences.Keys; import com.ferg.awfulapp.provider.AwfulProvider; import com.ferg.awfulapp.provider.ColorProvider; import com.ferg.awfulapp.provider.DatabaseHelper; @@ -239,6 +243,42 @@ public void onStop() { // TODO: cancel network reqs? } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.forum_display_fragment, menu); + + MenuItem postThread = menu.findItem(R.id.post_thread); + postThread.setVisible(getForumId() != Constants.USERCP_ID); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.post_thread: + displayPostThreadDialog(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void displayPostThreadDialog() { + if(AwfulPreferences.getInstance().postWarningAccepted) { + displayPostThreadDialog(getForumId()); + return; + } + new AlertDialog.Builder(getAwfulActivity()) + .setIcon(R.drawable.ic_gavel_dark_24dp) + .setTitle("Warning") + .setMessage(R.string.post_warning) + .setPositiveButton("I accept", (dialog, which) -> { + displayPostThreadDialog(getForumId()); + AwfulPreferences.getInstance().setPreference(Keys.POST_WARNING_ACCEPTED, true); + }) + .setNegativeButton("Nope", (dialog, which) -> dialog.dismiss()) + .setCancelable(false) + .show(); + + } @Override public void onCreateContextMenu(ContextMenu aMenu, View aView, ContextMenuInfo aMenuInfo) { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadActivity.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadActivity.java new file mode 100644 index 000000000..e73c3e17a --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadActivity.java @@ -0,0 +1,82 @@ +/******************************************************************************** + * Copyright (c) 2011, Scott Ferguson + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of the software nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY SCOTT FERGUSON ''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 SCOTT FERGUSON 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. + *******************************************************************************/ + +package com.ferg.awfulapp; + +import android.os.Bundle; +import android.view.MenuItem; + +import com.ferg.awfulapp.databinding.PostThreadActivityBinding; + +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.FragmentManager; + + +public class PostThreadActivity extends AwfulActivity { + + + Toolbar mToolbar; + PostThreadFragment threadFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + PostThreadActivityBinding binding = PostThreadActivityBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + mToolbar = binding.toolbar; + + setSupportActionBar(mToolbar); + setUpActionBar(); + + FragmentManager fm = getSupportFragmentManager(); + threadFragment = (PostThreadFragment) fm.findFragmentById(R.id.thread_fragment); + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onLeaveActivity(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + onLeaveActivity(); + } + + private void onLeaveActivity() { + threadFragment.onNavigateBack(); + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java new file mode 100644 index 000000000..9bcb71e71 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java @@ -0,0 +1,1070 @@ +/** + * ***************************************************************************** + * Copyright (c) 2011, Scott Ferguson + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of the software nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY SCOTT FERGUSON ''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 SCOTT FERGUSON 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. + * ***************************************************************************** + */ + +package com.ferg.awfulapp; + +import android.Manifest; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.text.Html; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.Toast; + +import com.android.volley.VolleyError; +import com.ferg.awfulapp.constants.Constants; +import com.ferg.awfulapp.network.NetworkUtils; +import com.ferg.awfulapp.preferences.AwfulPreferences; +import com.ferg.awfulapp.provider.AwfulProvider; +import com.ferg.awfulapp.provider.ColorProvider; +import com.ferg.awfulapp.task.AwfulRequest; +import com.ferg.awfulapp.task.PreviewThreadRequest; +import com.ferg.awfulapp.task.SendThreadRequest; +import com.ferg.awfulapp.task.ThreadRequest; +import com.ferg.awfulapp.thread.AwfulForum; +import com.ferg.awfulapp.thread.AwfulMessage; +import com.ferg.awfulapp.thread.AwfulThread; +import com.ferg.awfulapp.reply.MessageComposer; +import com.ferg.awfulapp.util.AwfulUtils; +import com.ferg.awfulapp.widget.ThreadIconPicker; +import com.google.android.material.snackbar.Snackbar; + +import org.apache.commons.lang3.StringUtils; +import org.threeten.bp.Duration; +import org.threeten.bp.Instant; + +import java.io.File; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import timber.log.Timber; + +import static com.ferg.awfulapp.constants.Constants.ATTACHMENT_MAX_BYTES; +import static com.ferg.awfulapp.constants.Constants.ATTACHMENT_MAX_HEIGHT; +import static com.ferg.awfulapp.constants.Constants.ATTACHMENT_MAX_WIDTH; +import static com.ferg.awfulapp.thread.AwfulMessage.REPLY_DISABLE_SMILIES; +import static com.ferg.awfulapp.thread.AwfulMessage.REPLY_SIGNATURE; + +public class PostThreadFragment extends AwfulFragment { + + public static final int REQUEST_THREAD = 5; + public static final int RESULT_POSTED = 6; + public static final int RESULT_CANCELLED = 7; + public static final int ADD_ATTACHMENT = 9; + private static final String TAG = "PostThreadFragment"; + + // UI components + private MessageComposer messageComposer; + @Nullable + private ProgressDialog progressDialog; + + private ThreadIconPicker threadIconPicker; + + private EditText subject; + + // internal state + @Nullable + private SavedDraft savedDraft = null; + @Nullable + private ContentValues threadData = null; + private boolean saveRequired = true; + @Nullable + private Intent attachmentData; + + // async stuff + private ContentResolver mContentResolver; + @NonNull + private final DraftThreadLoaderCallback draftLoaderCallback = new DraftThreadLoaderCallback(); + @NonNull + private final ForumInfoCallback forumInfoCallback = new ForumInfoCallback(); + + // thread metadata + private int mForumId; + + // User's thread data + @Nullable + private String mFileAttachment; + private boolean disableEmotes = false; + private boolean postSignature = false; + + + /////////////////////////////////////////////////////////////////////////// + // Activity and fragment initialisation + /////////////////////////////////////////////////////////////////////////// + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Timber.v("onCreate"); + setHasOptionsMenu(true); + setRetainInstance(false); + } + + + @Override + public View onCreateView(LayoutInflater aInflater, ViewGroup aContainer, Bundle aSavedState) { + super.onCreateView(aInflater, aContainer, aSavedState); + Timber.v("onCreateView"); + View view = inflateView(R.layout.post_thread, aContainer, aInflater); + + return view; + } + + @Override + public void onActivityCreated(Bundle aSavedState) { + super.onActivityCreated(aSavedState); + Timber.v("onActivityCreated"); + Activity activity = getActivity(); + + messageComposer = (MessageComposer) getChildFragmentManager().findFragmentById(R.id.message_composer_fragment); + messageComposer.setBackgroundColor(ColorProvider.BACKGROUND.getColor()); + messageComposer.setTextColor(ColorProvider.PRIMARY_TEXT.getColor()); + + // grab all the important thread params + Intent intent = activity.getIntent(); + mForumId = intent.getIntExtra(Constants.POST_FORUM_ID, 0); + setActionBarTitle(getTitle()); + + threadIconPicker = (ThreadIconPicker) getFragmentManager().findFragmentById(R.id.thread_icon_picker); + threadIconPicker.useForumIcons(mForumId); + + subject = (EditText) activity.findViewById(R.id.thread_subject); + + // perform some sanity checking + boolean badRequest = false; + if (mForumId < 0 || mForumId == 0) { + // we always need a valid forum ID + badRequest = true; + } + if (badRequest) { + Toast.makeText(activity, "Can't create thread! Bad parameters", Toast.LENGTH_LONG).show(); + String template = "Failed to init thread activity%n Forum ID: %d"; + Timber.w(template, mForumId); + activity.finish(); + } + + mContentResolver = activity.getContentResolver(); + // load any related stored draft before starting the thread request + // TODO: 06/04/2017 probably better to handle this as two separate, completable requests - combine thread and draft data when they're both finished, instead of assuming the draft loader finishes first + getStoredDraft(); + refreshForumInfo(); + loadThread(mForumId); + } + + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (requestCode == ADD_ATTACHMENT) { + if (AwfulUtils.isMarshmallow23()) { + int permissionCheck = ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE); + if (permissionCheck != PackageManager.PERMISSION_GRANTED) { + this.attachmentData = data; + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, Constants.AWFUL_PERMISSION_READ_EXTERNAL_STORAGE); + } else { + addAttachment(data); + } + } else { + addAttachment(data); + } + } + } + } + + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case Constants.AWFUL_PERMISSION_READ_EXTERNAL_STORAGE: + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + addAttachment(); + } else { + Toast.makeText(getActivity(), R.string.no_file_permission_attachment, Toast.LENGTH_LONG).show(); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + + private void getStoredDraft() { + restartLoader(Constants.THREAD_DRAFT_LOADER_ID, null, draftLoaderCallback); + } + + private void refreshForumInfo() { + restartLoader(Constants.FORUM_LOADER_ID, null, forumInfoCallback); + } + + + /////////////////////////////////////////////////////////////////////////// + // Fetching data/drafts and populating editor + /////////////////////////////////////////////////////////////////////////// + + + /** + * Initiate a new thread by passing a request to the site and handling its response. + * + * @param mForumId The ID of the forum + */ + private void loadThread(int mForumId) { + progressDialog = ProgressDialog.show(getActivity(), "Loading", "Fetching Message...", true, true); + + // create a callback to handle the thread data from the site + AwfulRequest.AwfulResultCallback loadCallback = new AwfulRequest.AwfulResultCallback() { + @Override + public void success(ContentValues result) { + threadData = result; + // set any options and update the menu + postSignature = getCheckedAndRemove(REPLY_SIGNATURE, result); + disableEmotes = getCheckedAndRemove(REPLY_DISABLE_SMILIES, result); + invalidateOptionsMenu(); + dismissProgressDialog(); + handleDraft(); + } + + @Override + public void failure(VolleyError error) { + dismissProgressDialog(); + //allow time for the error to display, then close the window + getHandler().postDelayed(() -> leave(RESULT_CANCELLED), 3000); + } + }; + queueRequest(new ThreadRequest(getActivity(), mForumId).build(this, loadCallback)); + } + + /** + * Removes a key from a ContentValues, returning true if it was set to "checked" + */ + private boolean getCheckedAndRemove(@NonNull String key, @NonNull ContentValues values) { + if (!values.containsKey(key)) { + return false; + } + boolean checked = "checked".equals(values.getAsString(key)); + values.remove(key); + return checked; + } + + + /** + * Take care of any saved draft, allowing the user to use it if appropriate. + */ + private void handleDraft() { + // this implicitly relies on the Draft Thread Loader having already finished, assigning to savedDraft if it found any draft data + if (savedDraft == null) { + return; + } + /* + This is where we decide whether to load an existing draft, or ignore it. + The saved draft will end up getting replaced/deleted anyway (when the post is either posted or saved), + this just decides whether it's relevant to the current context, and the user needs to know about it. + + We basically ignore a draft if: + - we're currently editing a post, and the draft isn't an edit + - the draft is an edit, but not for this post + in both cases we need to avoid replacing the original post (that we're trying to edit) with some other post's draft + */ + + // got a useful draft, let the user decide what to do with it + displayDraftAlert(savedDraft); + } + + + /** + * Display a dialog allowing the user to use or discard an existing draft. + * + * @param draft a draft message relevant to this post + */ + private void displayDraftAlert(@NonNull SavedDraft draft) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + String template = "You have a %s:" + + "
      %s:

      " + + "%s" + + "

      " + + "Saved %s ago"; + + String type = "Saved Thread"; + + final int MAX_PREVIEW_LENGTH = 140; + String previewText = StringUtils.substring(draft.content, 0, MAX_PREVIEW_LENGTH).replaceAll("\\n", "
      "); + if (draft.content.length() > MAX_PREVIEW_LENGTH) { + previewText += "..."; + } + + String message = String.format(template, type, draft.subject , previewText, epochToSimpleDuration(draft.timestamp)); + new AlertDialog.Builder(activity) + .setIcon(R.drawable.ic_reply_dark) + .setTitle(type) + .setMessage(Html.fromHtml(message)) + .setPositiveButton("Use", (dialog, which) -> { + String newContent = draft.content; + // If we're quoting something, stick it after the draft thread (and add some whitespace too) + messageComposer.setText(newContent, true); + subject.setText(draft.subject); + if(draft.iconId != null && draft.iconUrl != null){ + threadIconPicker.useIcon(draft.iconId, draft.iconUrl); + } + }) + .setNegativeButton(R.string.discard, (dialog, which) -> deleteSavedThread()) + // avoid accidental draft losses by forcing a decision + .setCancelable(false) + .show(); + } + + + /////////////////////////////////////////////////////////////////////////// + // Send/preview posts + /////////////////////////////////////////////////////////////////////////// + + + /** + * Display a dialog allowing the user to submit or preview their post + */ + private void showSubmitDialog() { + new AlertDialog.Builder(getActivity()) + .setTitle("Confirm Post?") + .setPositiveButton(R.string.submit, + (dialog, button) -> { + if (progressDialog == null && getActivity() != null) { + progressDialog = ProgressDialog.show(getActivity(), "Posting", "Hopefully it didn't suck...", true, true); + } + saveThread(); + submitThread(); + }) + .setNeutralButton(R.string.preview, (dialog, button) -> previewPost()) + .setNegativeButton(R.string.cancel, (dialog, button) -> { + }) + .show(); + } + + + /** + * Actually submit the post/edit to the site. + */ + private void submitThread() { + ContentValues cv = prepareCV(); + if (cv == null) { + return; + } + AwfulRequest.AwfulResultCallback postCallback = new AwfulRequest.AwfulResultCallback() { + @Override + public void success(Void result) { + dismissProgressDialog(); + deleteSavedThread(); + saveRequired = false; + + Context context = getContext(); + if (context != null) { + Toast.makeText(context, context.getString(R.string.post_sent), Toast.LENGTH_LONG).show(); + } + mContentResolver.notifyChange(AwfulThread.CONTENT_URI, null); + leave(RESULT_POSTED); + } + + @Override + public void failure(VolleyError error) { + dismissProgressDialog(); + saveThread(); + } + }; + queueRequest(new SendThreadRequest(getActivity(), cv).build(this, postCallback)); + } + + + /** + * Request a preview of the current post from the site, and display it. + */ + private void previewPost() { + ContentValues cv = prepareCV(); + Activity activity = getActivity(); + FragmentManager fragmentManager = getFragmentManager(); + if (cv == null || activity == null || fragmentManager == null) { + return; + } + + final PreviewFragment previewFrag = new PreviewFragment(); + previewFrag.setStyle(DialogFragment.STYLE_NO_TITLE, 0); + previewFrag.show(fragmentManager, "Post Preview"); + + AwfulRequest.AwfulResultCallback previewCallback = new AwfulRequest.AwfulResultCallback() { + @Override + public void success(final String result) { + previewFrag.setContent(result); + } + + @Override + public void failure(VolleyError error) { + // love dialogs and callbacks very elegant + if (!previewFrag.isStateSaved() && previewFrag.getActivity() != null && !previewFrag.getActivity().isFinishing()) { + previewFrag.dismiss(); + } + if (getView() != null) { + Snackbar.make(getView(), "Preview failed.", Snackbar.LENGTH_LONG) + .setAction("Retry", v -> previewPost()).show(); + } + } + }; + + + queueRequest(new PreviewThreadRequest(getActivity(), cv).build(this, previewCallback)); + } + + + /** + * Create a ContentValues representing the current post and its options. + *

      + * Returns null if the data is invalid, e.g. an empty post + * + * @return The post data, or null if there was an error. + */ + @Nullable + private ContentValues prepareCV() { + if (threadData == null || threadData.getAsInteger(AwfulMessage.ID) == null) { + // TODO: if this ever happens, the ID never gets set (and causes an NPE in SendPostRequest) - handle this in a better way? + // Could use the mThreadId value, but that might be incorrect at this point and post to the wrong thread? Is null thread data an exceptional event? + Log.e(TAG, "No thread data in sendPost() - no thread ID to post to!"); + Activity activity = getActivity(); + if (activity != null) { + Toast.makeText(activity, "Unknown thread ID - can't post!", Toast.LENGTH_LONG).show(); + } + return null; + } + ContentValues cv = new ContentValues(threadData); + if (isOPEmpty()) { + dismissProgressDialog(); + getAlertView().setTitle(R.string.message_empty) + .setSubtitle(R.string.message_empty_subtext) + .show(); + return null; + } + if (!TextUtils.isEmpty(mFileAttachment)) { + cv.put(AwfulMessage.REPLY_ATTACHMENT, mFileAttachment); + } + if (postSignature) { + cv.put(REPLY_SIGNATURE, Constants.YES); + } + if (disableEmotes) { + cv.put(AwfulMessage.REPLY_DISABLE_SMILIES, Constants.YES); + } + + cv.put(AwfulMessage.POST_SUBJECT, subject.getText().toString()); + cv.put(AwfulMessage.POST_ICON_ID, threadIconPicker.getIcon().iconId); + cv.put(AwfulMessage.POST_ICON_URL, threadIconPicker.getIcon().iconUrl); + cv.put(AwfulMessage.POST_CONTENT, messageComposer.getText()); + return cv; + } + + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle/navigation stuff + /////////////////////////////////////////////////////////////////////////// + + + @Override + public void onResume() { + super.onResume(); + Timber.v("onResume"); + } + + @Override + public void onPause() { + super.onPause(); + Timber.v("onPause"); + cleanupTasks(); + } + + + @Override + public void onDestroyView() { + super.onDestroyView(); + Log.e(TAG, "onDestroyView"); + // final cleanup - some should have already been done in onPause (draft saving etc) + getLoaderManager().destroyLoader(Constants.THREAD_DRAFT_LOADER_ID); + getLoaderManager().destroyLoader(Constants.FORUM_LOADER_ID); + } + + /** + * Tasks to perform when the thread window moves from the foreground. + * Basically saves a draft if required, and hides elements like the keyboard + */ + private void cleanupTasks() { + autoSave(); + dismissProgressDialog(); + messageComposer.hideKeyboard(); + } + + + /** + * Finish the thread activity, performing cleanup and returning a result code to the activity that created it. + */ + private void leave(int activityResult) { + final AwfulActivity activity = getAwfulActivity(); + if (activity != null) { + activity.setResult(activityResult); + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null && getView() != null) { + imm.hideSoftInputFromWindow(getView().getApplicationWindowToken(), 0); + } + activity.finish(); + } + } + + + /** + * Call this when the user tries to leave the activity, so the Save/Discard dialog can be shown if necessary. + */ + void onNavigateBack() { + Activity activity = getActivity(); + if (activity == null) { + return; + } else if (isOPEmpty()) { + leave(RESULT_CANCELLED); + return; + } + new AlertDialog.Builder(activity) + .setIcon(R.drawable.ic_reply_dark) + .setMessage("Save this thread?") + .setPositiveButton(R.string.save, (dialog, button) -> { + // let #autoSave handle it on leaving + saveRequired = true; + leave(RESULT_CANCELLED); + }) + .setNegativeButton(R.string.discard, (dialog, which) -> { + deleteSavedThread(); + saveRequired = false; + leave(RESULT_CANCELLED); + }) + .setNeutralButton(R.string.cancel, (dialog, which) -> { + }) + .setCancelable(true) + .show(); + + } + + + /////////////////////////////////////////////////////////////////////////// + // Saving draft data + /////////////////////////////////////////////////////////////////////////// + + + /** + * Trigger a draft save, if required. + */ + private void autoSave() { + if (saveRequired && messageComposer != null) { + if (isOPEmpty()) { + Log.i(TAG, "Message unchanged, discarding."); + // TODO: 12/02/2017 does this actually need to check if it's unchanged? + deleteSavedThread();//if the thread is unchanged, throw it out. + messageComposer.setText(null, false); + } else { + Log.i(TAG, "Message Unsent, saving."); + saveThread(); + } + } + } + + + /** + * Delete any saved thread for the current thread + */ + private void deleteSavedThread() { + mContentResolver.delete(AwfulMessage.CONTENT_URI_THREAD, AwfulMessage.ID + "=?", AwfulProvider.int2StrArray(mForumId)); + } + + + /** + * Save a draft thread for the current thread. + */ + private void saveThread() { + if (getActivity() != null && mForumId > 0 && messageComposer != null) { + String content = messageComposer.getText(); + // don't save if the message is empty/whitespace + // not trimming the actual content, so we retain any whitespace e.g. blank lines after quotes + if (!content.trim().isEmpty()) { + Log.i(TAG, "Saving thread! " + content); + ContentValues post = (threadData == null) ? new ContentValues() : new ContentValues(threadData); + post.put(AwfulMessage.ID, mForumId); + post.put(AwfulMessage.POST_CONTENT, content); + post.put(AwfulMessage.EPOC_TIMESTAMP, System.currentTimeMillis()); + post.put(AwfulMessage.POST_SUBJECT, subject.getText().toString()); + post.put(AwfulMessage.POST_ICON_ID, threadIconPicker.getIcon().iconId); + post.put(AwfulMessage.POST_ICON_URL, threadIconPicker.getIcon().iconUrl); + if (mFileAttachment != null) { + post.put(AwfulMessage.REPLY_ATTACHMENT, mFileAttachment); + } + if (mContentResolver.update(ContentUris.withAppendedId(AwfulMessage.CONTENT_URI_THREAD, mForumId), post, null, null) < 1) { + mContentResolver.insert(AwfulMessage.CONTENT_URI_THREAD, post); + } + } + } + } + + + /////////////////////////////////////////////////////////////////////////// + // Menus + /////////////////////////////////////////////////////////////////////////// + + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + Timber.v("onCreateOptionsMenu"); + inflater.inflate(R.menu.post_thread, menu); + + MenuItem attach = menu.findItem(R.id.add_attachment); + if (attach != null && getPrefs() != null) { + attach.setEnabled(getPrefs().hasPlatinum); + attach.setVisible(getPrefs().hasPlatinum); + } + MenuItem remove = menu.findItem(R.id.remove_attachment); + if (remove != null && getPrefs() != null) { + remove.setEnabled((getPrefs().hasPlatinum && this.mFileAttachment != null)); + remove.setVisible(getPrefs().hasPlatinum && this.mFileAttachment != null); + } + MenuItem disableEmoticons = menu.findItem(R.id.disableEmots); + if (disableEmoticons != null) { + disableEmoticons.setChecked(disableEmotes); + } + MenuItem sig = menu.findItem(R.id.signature); + if (sig != null) { + sig.setChecked(postSignature); + } + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Timber.v("onOptionsItemSelected"); + switch (item.getItemId()) { + case R.id.submit_button: + showSubmitDialog(); + break; + case R.id.add_attachment: + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + startActivityForResult(Intent.createChooser(intent, + "Select Picture"), ADD_ATTACHMENT); + break; + case R.id.remove_attachment: + this.mFileAttachment = null; + Toast removeToast = Toast.makeText(getAwfulActivity(), getAwfulActivity().getResources().getText(R.string.file_removed), Toast.LENGTH_SHORT); + removeToast.show(); + invalidateOptionsMenu(); + break; + case R.id.signature: + item.setChecked(!item.isChecked()); + postSignature = item.isChecked(); + break; + case R.id.disableEmots: + item.setChecked(!item.isChecked()); + disableEmotes = item.isChecked(); + break; + default: + return super.onOptionsItemSelected(item); + } + + return true; + } + + + @Override + public void onPreferenceChange(AwfulPreferences prefs, String key) { + super.onPreferenceChange(prefs, key); + //refresh the menu to show/hide attach option (plat only) + invalidateOptionsMenu(); + } + + + /////////////////////////////////////////////////////////////////////////// + // Attachment handling + /////////////////////////////////////////////////////////////////////////// + + // TODO: 13/04/2017 make a separate attachment component and stick all this in there + + private void addAttachment() { + addAttachment(attachmentData); + attachmentData = null; + } + + private void addAttachment(Intent data) { + Uri selectedImageUri = data.getData(); + String path = getFilePath(selectedImageUri); + if (path == null) { + setAttachment(null, getString(R.string.file_error)); + return; + } + + File attachment = new File(path); + String filename = attachment.getName(); + if (!attachment.isFile() || !attachment.canRead()) { + setAttachment(null, String.format(getString(R.string.file_unreadable), filename)); + return; + } else if (!StringUtils.endsWithAny(filename.toLowerCase(), ".jpg", ".jpeg", ".png", ".gif")) { + setAttachment(null, String.format(getString(R.string.file_wrong_filetype), filename)); + return; + } else if (attachment.length() > ATTACHMENT_MAX_BYTES) { + setAttachment(null, String.format(getString(R.string.file_too_big), filename)); + return; + } + + // check the image size without creating a bitmap + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, options); + int height = options.outHeight; + int width = options.outWidth; + if (width > ATTACHMENT_MAX_WIDTH || height > ATTACHMENT_MAX_HEIGHT) { + setAttachment(null, String.format(getString(R.string.file_resolution_too_big), filename, width, height)); + return; + } + + setAttachment(path, String.format(getString(R.string.file_attached), filename)); + } + + + private void setAttachment(@Nullable String attachment, @NonNull String toastMessage) { + mFileAttachment = attachment; + Toast.makeText(getActivity(), toastMessage, Toast.LENGTH_LONG).show(); + invalidateOptionsMenu(); + } + + + private String getFilePath(final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(this.getActivity(), uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private String getDataColumn(Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String column = "_data"; + final String[] projection = { + column + }; + + try { + cursor = this.getActivity().getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + + /////////////////////////////////////////////////////////////////////////// + // Misc utility stuff + /////////////////////////////////////////////////////////////////////////// + + + /** + * Utility method to check if the composer contains an empty post + */ + private boolean isOPEmpty() { + return messageComposer.getText().trim().isEmpty(); + } + + /** + * Convert an epoch timestamp to a duration relative to now. + *

      + * Returns the duration in a "1d 4h 22m 30s" format, omitting units with zero values. + */ + private String epochToSimpleDuration(long epoch) { + Duration diff = Duration.between(Instant.ofEpochSecond((epoch / 1000)), Instant.now()).abs(); + String time = ""; + if (diff.toDays() > 0) { + time += " " + diff.toDays() + "d"; + diff = diff.minusDays(diff.toDays()); + } + if (diff.toHours() > 0) { + time += " " + diff.toHours() + "h"; + diff = diff.minusHours(diff.toHours()); + } + if (diff.toMinutes() > 0) { + time += " " + diff.toMinutes() + "m"; + diff = diff.minusMinutes(diff.toMinutes()); + } + + time += " " + diff.getSeconds() + "s"; + return time; + } + + + /////////////////////////////////////////////////////////////////////////// + // UI things + /////////////////////////////////////////////////////////////////////////// + + + /** + * Dismiss the progress dialog and set it to null, if it isn't already. + */ + private void dismissProgressDialog() { + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + progressDialog = null; + } + } + + @Override + public String getTitle() { + return "Post Thread"; + } + + + /////////////////////////////////////////////////////////////////////////// + // Async classes etc + /////////////////////////////////////////////////////////////////////////// + + + /** + * Provides a Loader that pulls draft data for the current thread from the DB. + */ + private class DraftThreadLoaderCallback implements LoaderManager.LoaderCallbacks { + + public Loader onCreateLoader(int aId, Bundle aArgs) { + Log.i(TAG, "Create Thread Cursor: " + mForumId); + return new CursorLoader(getActivity(), + ContentUris.withAppendedId(AwfulMessage.CONTENT_URI_THREAD, mForumId), + AwfulProvider.DraftThreadProjection, + null, + null, + null); + } + + public void onLoadFinished(Loader aLoader, Cursor aData) { + if (aData.isClosed() || !aData.moveToFirst()) { + // no draft saved for this thread + return; + } + // if there's some quote data, deserialise it into a SavedDraft + String quoteData = aData.getString(aData.getColumnIndex(AwfulMessage.POST_CONTENT)); + if (TextUtils.isEmpty(quoteData)) { + return; + } + String subject = aData.getString(aData.getColumnIndex(AwfulMessage.POST_SUBJECT)); + long draftTimestamp = aData.getLong(aData.getColumnIndex(AwfulMessage.EPOC_TIMESTAMP)); + String draftThread = NetworkUtils.unencodeHtml(quoteData); + + String draftIconId = aData.getString(aData.getColumnIndex(AwfulMessage.POST_ICON_ID)); + String draftIconUrl = aData.getString(aData.getColumnIndex(AwfulMessage.POST_ICON_URL)); + + savedDraft = new SavedDraft(draftThread, subject,draftIconId,draftIconUrl, draftTimestamp); + if (Constants.DEBUG) { + Log.i(TAG, "Saved thread message: " + draftThread); + } + } + + @Override + public void onLoaderReset(Loader aLoader) { + + } + } + + + /** + * Provides a Loader that gets metadata for the current thread, and dsiplays its title + */ + private class ForumInfoCallback implements LoaderManager.LoaderCallbacks { + + public Loader onCreateLoader(int aId, Bundle aArgs) { + return new CursorLoader(getActivity(), ContentUris.withAppendedId(AwfulForum.CONTENT_URI, mForumId), + AwfulProvider.ForumProjection, null, null, null); + } + + public void onLoadFinished(Loader aLoader, Cursor aData) { + Log.v(TAG, "Thread title finished, populating."); + } + + @Override + public void onLoaderReset(Loader aLoader) { + } + } + + + private static class SavedDraft { + @NonNull + private final String content; + private final String iconId; + private final String iconUrl; + private final String subject; + private final long timestamp; + + SavedDraft(@NonNull String content, String subject, String iconId, String iconUrl, long timestamp) { + this.content = content; + this.subject = subject; + this.iconId = iconId; + this.iconUrl = iconUrl; + this.timestamp = timestamp; + } + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java b/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java index 47169eef1..affbf4437 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java @@ -43,6 +43,7 @@ public class Constants { public static final String FUNCTION_USERCP = BASE_URL + "/usercp.php"; public static final String FUNCTION_FORUM = BASE_URL + "/forumdisplay.php"; public static final String FUNCTION_THREAD = BASE_URL + "/showthread.php"; + public static final String FUNCTION_POST_THREAD = BASE_URL + "/newthread.php"; public static final String FUNCTION_POST_REPLY = BASE_URL + "/newreply.php"; public static final String FUNCTION_EDIT_POST = BASE_URL + "/editpost.php"; public static final String FUNCTION_MEMBER = BASE_URL + "/member.php"; @@ -177,16 +178,19 @@ public class Constants { public static final int THREAD_INFO_LOADER_ID = 891; public static final int POST_LOADER_ID = 892; public static final int FORUM_INDEX_LOADER_ID = 893; + public static final int THREAD_DRAFT_LOADER_ID = 894; public static final String ACTION_DOSEND = "dosend"; public static final String DESTINATION_TOUSER = "touser"; public static final String PARAM_TITLE = "title"; - public static final String PARAM_MESSAGE = "message"; + public static final String PARAM_MESSAGE = "message"; + public static final String PARAM_SUBJECT = "subject"; public static final String EXTRA_BUNDLE = "extras"; public static final String SUBMIT_REPLY = "Submit Reply"; public static final String PREVIEW_REPLY = "Preview Reply"; + public static final String PREVIEW_POST = "Preview Post"; public static final String YES = "yes";//heh @@ -206,6 +210,8 @@ public class Constants { public static final String REPLY_POST_ID = "reply_post_id"; public static final String REPLY_THREAD_ID = "reply_thread_id"; + public static final String POST_FORUM_ID = "post_forum_id"; + public static final int AWFUL_THREAD_ID = 3571717; public static final int FORUM_ID_SHSC = 22; public static final int FORUM_ID_YOSPOS = 219; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/AwfulPreferences.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/AwfulPreferences.java index bfc13a7a6..3ab7bac1c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/AwfulPreferences.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/AwfulPreferences.java @@ -162,6 +162,8 @@ public class AwfulPreferences implements OnSharedPreferenceChangeListener { public boolean immersionMode; public String transformer; + public boolean postWarningAccepted; + // APP VERSION STUFF public int alertIDShown; public int lastVersionSeen; @@ -312,6 +314,7 @@ private void updateValues() { forumIndexShowSections = getPreference(Keys.FORUM_INDEX_SHOW_SECTIONS, true); forumIndexShowSubtitles = getPreference(Keys.FORUM_INDEX_SHOW_SUBTITLES, true); forumIndexHideSubforums = getPreference(Keys.FORUM_INDEX_HIDE_SUBFORUMS, true); + postWarningAccepted = getPreference(Keys.POST_WARNING_ACCEPTED, false); //I have never seen this before oh god } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/Keys.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/Keys.java index e3f32e6d0..ecb8b2048 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/Keys.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/Keys.java @@ -136,6 +136,7 @@ public abstract class Keys { FORUM_INDEX_SHOW_SECTIONS, FORUM_INDEX_SHOW_SUBTITLES, FORUM_INDEX_HIDE_SUBFORUMS, + POST_WARNING_ACCEPTED }) @Retention(RetentionPolicy.SOURCE) public @interface BooleanPreference { @@ -224,4 +225,6 @@ public abstract class Keys { public static final int MARKED_USERS = R.string.pref_key_marked_users; public static final int BLOCKED_AVATAR_URLS = R.string.pref_key_blocked_avatar_urls; + + public static final int POST_WARNING_ACCEPTED = R.string.pref_key_post_warning_accepted; } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java index 2d9908a73..7879b50ec 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java @@ -63,6 +63,7 @@ import static com.ferg.awfulapp.provider.DatabaseHelper.TABLE_PM; import static com.ferg.awfulapp.provider.DatabaseHelper.TABLE_POSTS; import static com.ferg.awfulapp.provider.DatabaseHelper.TABLE_THREADS; +import static com.ferg.awfulapp.provider.DatabaseHelper.TABLE_THREAD_DRAFTS; import static com.ferg.awfulapp.provider.DatabaseHelper.TABLE_UCP_THREADS; public class AwfulProvider extends ContentProvider { @@ -91,8 +92,10 @@ public class AwfulProvider extends ContentProvider { private static final int URI_DRAFT_ID = 11; private static final int URI_EMOTE = 12; private static final int URI_EMOTE_ID = 13; + private static final int URI_THREAD_DRAFT = 14; + private static final int URI_THREAD_DRAFT_ID = 15; /** This just holds the Uri types that directly refer to tables, not IDs */ - private static final Set TABLE_URIS = new HashSet<>(Arrays.asList(URI_FORUM, URI_POST, URI_THREAD, URI_UCP_THREAD, URI_PM, URI_DRAFT, URI_EMOTE)); + private static final Set TABLE_URIS = new HashSet<>(Arrays.asList(URI_FORUM, URI_POST, URI_THREAD, URI_UCP_THREAD, URI_PM, URI_DRAFT, URI_EMOTE, URI_THREAD_DRAFT)); private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { @@ -110,6 +113,8 @@ public class AwfulProvider extends ContentProvider { sUriMatcher.addURI(Constants.AUTHORITY, "draftreplies/#", URI_DRAFT_ID); sUriMatcher.addURI(Constants.AUTHORITY, "emote", URI_EMOTE); sUriMatcher.addURI(Constants.AUTHORITY, "emote/#", URI_EMOTE_ID); + sUriMatcher.addURI(Constants.AUTHORITY, "draftthreads", URI_THREAD_DRAFT); + sUriMatcher.addURI(Constants.AUTHORITY, "draftthreads/#", URI_THREAD_DRAFT_ID); } @@ -262,6 +267,22 @@ private static String[] arrayOfKeys(@NonNull Map map) { DatabaseHelper.UPDATED_TIMESTAMP }; + private static final HashMap sDraftThreadProjectionMap = new HashMap<>(); + static { + sDraftThreadProjectionMap.put(AwfulMessage.ID, AwfulMessage.ID); + sDraftThreadProjectionMap.put(AwfulPost.FORM_COOKIE, AwfulPost.FORM_COOKIE); + sDraftThreadProjectionMap.put(AwfulPost.FORM_KEY, AwfulPost.FORM_KEY); + sDraftThreadProjectionMap.put(AwfulMessage.POST_CONTENT, AwfulMessage.POST_CONTENT); + sDraftThreadProjectionMap.put(AwfulMessage.POST_SUBJECT, AwfulMessage.POST_SUBJECT); + sDraftThreadProjectionMap.put(AwfulMessage.POST_ICON_ID, AwfulMessage.POST_ICON_ID); + sDraftThreadProjectionMap.put(AwfulMessage.POST_ICON_URL, AwfulMessage.POST_ICON_URL); + sDraftThreadProjectionMap.put(AwfulMessage.REPLY_ATTACHMENT, AwfulMessage.REPLY_ATTACHMENT); + sDraftThreadProjectionMap.put(AwfulPost.FORM_BOOKMARK, AwfulPost.FORM_BOOKMARK); + sDraftThreadProjectionMap.put(AwfulMessage.EPOC_TIMESTAMP, AwfulMessage.EPOC_TIMESTAMP); + sDraftThreadProjectionMap.put(DatabaseHelper.UPDATED_TIMESTAMP, DatabaseHelper.UPDATED_TIMESTAMP); + } + public static final String[] DraftThreadProjection = arrayOfKeys(sDraftThreadProjectionMap); + // Private messages private static final HashMap sPMReplyProjectionMap = new HashMap<>(); static { @@ -502,6 +523,12 @@ public Cursor query(@NonNull Uri aUri, String[] aProjection, String aSelection, builder.setProjectionMap(sDraftProjectionMap); break; + case URI_THREAD_DRAFT_ID: + whereClause = AwfulMessage.ID; + case URI_THREAD_DRAFT: + builder.setProjectionMap(sDraftThreadProjectionMap); + break; + case URI_EMOTE_ID: whereClause = AwfulEmote.ID; case URI_EMOTE: @@ -562,9 +589,12 @@ private String getTableForUriType(int uriType) { case URI_PM_ID: case URI_PM: return TABLE_PM; - case URI_DRAFT_ID: - case URI_DRAFT: - return TABLE_DRAFTS; + case URI_DRAFT_ID: + case URI_DRAFT: + return TABLE_DRAFTS; + case URI_THREAD_DRAFT_ID: + case URI_THREAD_DRAFT: + return TABLE_THREAD_DRAFTS; case URI_EMOTE_ID: case URI_EMOTE: return TABLE_EMOTES; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/DatabaseHelper.java b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/DatabaseHelper.java index c24ed78d7..295d733b6 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/DatabaseHelper.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/DatabaseHelper.java @@ -20,7 +20,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "awful.db"; - private static final int DATABASE_VERSION = 36; + private static final int DATABASE_VERSION = 37; static final String TABLE_FORUM = "forum"; static final String TABLE_THREADS = "threads"; @@ -30,6 +30,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { static final String TABLE_EMOTES = "emotes"; static final String TABLE_PM = "private_messages"; static final String TABLE_DRAFTS = "draft_messages"; + static final String TABLE_THREAD_DRAFTS = "draft_threads"; public static final String UPDATED_TIMESTAMP = "timestamp_row_update"; @@ -46,6 +47,7 @@ public void onCreate(SQLiteDatabase aDb) { createEmoteTable(aDb); createPMTable(aDb); createDraftTable(aDb); + createThreadDraftTable(aDb); } @@ -158,6 +160,21 @@ private void createDraftTable(SQLiteDatabase aDb) { UPDATED_TIMESTAMP + " DATETIME);"); } + private void createThreadDraftTable(SQLiteDatabase aDb) { + aDb.execSQL("CREATE TABLE " + TABLE_THREAD_DRAFTS + " (" + + AwfulMessage.ID + " INTEGER UNIQUE," + + AwfulPost.FORM_KEY + " VARCHAR," + + AwfulPost.FORM_COOKIE + " VARCHAR," + + AwfulMessage.POST_CONTENT + " VARCHAR," + + AwfulMessage.POST_SUBJECT + " VARCHAR," + + AwfulMessage.POST_ICON_ID + " VARCHAR," + + AwfulMessage.POST_ICON_URL + " VARCHAR," + + AwfulPost.FORM_BOOKMARK + " VARCHAR," + + AwfulMessage.REPLY_ATTACHMENT + " VARCHAR," + + AwfulMessage.EPOC_TIMESTAMP + " INTEGER, " + + UPDATED_TIMESTAMP + " DATETIME);"); + } + @Override public void onUpgrade(SQLiteDatabase aDb, int aOldVersion, int aNewVersion) { @@ -188,6 +205,9 @@ public void onUpgrade(SQLiteDatabase aDb, int aOldVersion, int aNewVersion) { case 35: dropTables(aDb, TABLE_POSTS); createPostTable(aDb); + case 36: + dropTables(aDb, TABLE_THREAD_DRAFTS); + createThreadDraftTable(aDb); break;//make sure to keep this break statement on the last case of this switch default: wipeRecreateTables(aDb); @@ -209,7 +229,7 @@ private void dropTables(@NonNull SQLiteDatabase db, @NonNull String... tableName } private void wipeRecreateTables(SQLiteDatabase aDb) { - String[] allTables = {TABLE_FORUM, TABLE_THREADS, TABLE_POSTS, TABLE_EMOTES, TABLE_UCP_THREADS, TABLE_PM, TABLE_DRAFTS}; + String[] allTables = {TABLE_FORUM, TABLE_THREADS, TABLE_POSTS, TABLE_EMOTES, TABLE_UCP_THREADS, TABLE_PM, TABLE_DRAFTS, TABLE_THREAD_DRAFTS}; dropTables(aDb, allTables); onCreate(aDb); } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewThreadRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewThreadRequest.kt new file mode 100644 index 000000000..25d30b0f2 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewThreadRequest.kt @@ -0,0 +1,50 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.thread.AwfulMessage +import com.ferg.awfulapp.thread.AwfulPost +import com.ferg.awfulapp.thread.PostPreviewParseTask +import org.jsoup.nodes.Document + +/** + * A request that fetches a preview of a thread, providing the parsed HTML. + * + * This functions like the Preview button when composing a thread on the site, which + * takes the current content in the post window and renders it as HTML. You supply + * the data on the edit page via the ContentValues parameter, which is sent to + * the site to retrieve the preview. + */ +class PreviewThreadRequest (context: Context, reply: ContentValues) + : AwfulRequest(context, FUNCTION_POST_THREAD, isPostRequest = true) { +// TODO: 23/05/2024 this, PreviewPostRequest and PreviewEditRequest are almost identical, oh no it's getting worse + init { + with(parameters) { + val threadId = reply.getAsInteger(AwfulMessage.ID)?.toString() + ?: throw IllegalArgumentException("No forum ID included") + add(PARAM_ACTION, "postthread") + add(PARAM_FORUM_ID, Integer.toString(reply.getAsInteger(AwfulMessage.ID)!!)) + add(PARAM_FORMKEY, reply.getAsString(AwfulPost.FORM_KEY)) + add(PARAM_FORM_COOKIE, reply.getAsString(AwfulPost.FORM_COOKIE)) + add(PARAM_SUBJECT, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.POST_SUBJECT))) + add(PARAM_MESSAGE, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.POST_CONTENT))) + + add(PARAM_PARSEURL, YES) + // TODO: this bookmarks every thread you post in, unless you turn it off in a browser - seems bad? + if (reply.getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + add(PARAM_BOOKMARK, YES) + } + listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) + .forEach { if (reply.containsKey(it)) add(it, YES) } + + reply.getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> attachFile(PARAM_ATTACHMENT, filePath) } + add(PARAM_PREVIEW, PREVIEW_REPLY) + } + } + + + override fun handleResponse(doc: Document): String = PostPreviewParseTask(doc).call() + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendThreadRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendThreadRequest.kt new file mode 100644 index 000000000..5e1d9625b --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendThreadRequest.kt @@ -0,0 +1,40 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.thread.AwfulMessage +import com.ferg.awfulapp.thread.AwfulPost +import org.jsoup.nodes.Document + +/** + * Submit a thread described by a set of ContentValues + */ +class SendThreadRequest(context: Context, reply: ContentValues) + : AwfulRequest(context, FUNCTION_POST_THREAD, isPostRequest = true) { + + init { + with(parameters) { + add(PARAM_ACTION, "postthread") + add(PARAM_FORUM_ID, Integer.toString(reply.getAsInteger(AwfulMessage.ID)!!)) + add(PARAM_FORMKEY, reply.getAsString(AwfulPost.FORM_KEY)) + add(PARAM_FORM_COOKIE, reply.getAsString(AwfulPost.FORM_COOKIE)) + add(PARAM_SUBJECT, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.POST_SUBJECT))) + add(PARAM_MESSAGE, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.POST_CONTENT))) + add(PARAM_PARSEURL, YES) + add("iconid", reply.getAsString(AwfulMessage.REPLY_ICON)) + if (reply.getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + add(PARAM_BOOKMARK, YES) + } + listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) + .forEach { key -> if (reply.containsKey(key)) add(key, YES) } + reply.getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> + attachFile(PARAM_ATTACHMENT, filePath) + } + } + } + + override fun handleResponse(doc: Document): Void? = null + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadRequest.kt new file mode 100644 index 000000000..a4160e691 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadRequest.kt @@ -0,0 +1,36 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.provider.DatabaseHelper +import com.ferg.awfulapp.thread.Thread +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document +import java.sql.Timestamp + +/** + * Request the data you get when starting a new thread on the site. + * + * This provides you with any initial op contents, the form key + * and cookie (for authentication?) as well as any selected + * options (see [Thread.processReply]) and a current timestamp. + */ +class ThreadRequest(context: Context, private val forumId: Int) + : AwfulRequest(context, FUNCTION_POST_THREAD) { + + init { + with(parameters) { + add(PARAM_ACTION, "newthread") + add(PARAM_FORUM_ID, forumId.toString()) + } + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): ContentValues { + return Thread.processThread(doc, forumId).apply { + put(DatabaseHelper.UPDATED_TIMESTAMP, Timestamp(System.currentTimeMillis()).toString()) + } + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulMessage.java b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulMessage.java index 0b8292986..421bc359f 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulMessage.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulMessage.java @@ -59,6 +59,8 @@ public class AwfulMessage extends AwfulPagedItem { public static final Uri CONTENT_URI = Uri.parse("content://" + Constants.AUTHORITY + PATH); public static final String PATH_REPLY = "/draftreplies"; public static final Uri CONTENT_URI_REPLY = Uri.parse("content://" + Constants.AUTHORITY + PATH_REPLY); + public static final String PATH_THREAD = "/draftthreads"; + public static final Uri CONTENT_URI_THREAD = Uri.parse("content://" + Constants.AUTHORITY + PATH_THREAD); public static final String ID = "_id"; public static final String TITLE ="title"; @@ -76,6 +78,11 @@ public class AwfulMessage extends AwfulPagedItem { public static final String REPLY_ATTACHMENT = "attachment"; public static final String REPLY_SIGNATURE = "signature"; public static final String REPLY_DISABLE_SMILIES = "disablesmilies"; + + public static final String POST_CONTENT = "post_content"; + public static final String POST_SUBJECT = "post_subject"; + public static final String POST_ICON_ID = "post_icon_id"; + public static final String POST_ICON_URL = "post_icon_url"; public static final String FOLDER = "folder"; public static final int TYPE_PM = 1; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulPostIcon.java b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulPostIcon.java index 05d0757a5..0e80b6bc5 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulPostIcon.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulPostIcon.java @@ -49,7 +49,7 @@ public class AwfulPostIcon { public final int drawableId; public Drawable drawable = null; - private AwfulPostIcon(@NonNull String iconId, @NonNull String iconUrl, @NonNull Context context) { + public AwfulPostIcon(@NonNull String iconId, @NonNull String iconUrl, @NonNull Context context) { this.iconId = iconId; this.iconUrl = iconUrl; drawableId = getIconResId(iconUrl, context); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/Thread.java b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/Thread.java new file mode 100644 index 000000000..e0cd4010e --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/Thread.java @@ -0,0 +1,105 @@ +/******************************************************************************** + * Copyright (c) 2011, Scott Ferguson + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of the software nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY SCOTT FERGUSON ''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 SCOTT FERGUSON 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. + *******************************************************************************/ + +package com.ferg.awfulapp.thread; + +import android.content.ContentValues; + +import com.ferg.awfulapp.util.AwfulError; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +public class Thread { + private static final String TAG = "Reply"; + + private static final String FORMKEY = "//input[@name='formkey']"; + private static final String FORMCOOKIE = "//input[@name='form_cookie']"; + + private static final String PARAM_ACTION = "action"; + private static final String PARAM_THREADID = "threadid"; + private static final String PARAM_POSTID = "postid"; + private static final String PARAM_FORMKEY = "formkey"; + private static final String PARAM_FORM_COOKIE = "form_cookie"; + private static final String PARAM_BOOKMARK = "bookmark"; + private static final String PARAM_ATTACHMENT = "attachment"; + + private static final String VALUE_ACTION = "postthread"; + private static final String VALUE_POSTID = ""; + private static final String VALUE_FORM_COOKIE = "formcookie"; + + + public static final ContentValues processThread(Document page, int forumId) throws AwfulError { + ContentValues newThread = new ContentValues(); + newThread.put(AwfulMessage.ID, forumId); + getFormData(page, newThread); + newThread.put(AwfulPost.FORM_BOOKMARK, getBookmarkOption(page)); + newThread.put(AwfulPost.FORM_SIGNATURE, getSignatureOption(page)); + newThread.put(AwfulPost.FORM_DISABLE_SMILIES, getDisableEmotesOption(page)); + return newThread; + } + + public static final String getBookmarkOption(Document data){ + Element formBookmark = data.getElementsByAttributeValue("name", "bookmark").first(); + if(formBookmark.hasAttr("checked")){ + return "checked"; + }else{ + return ""; + } + } + + public static final String getDisableEmotesOption(Document data){ + Element formDisableEmotes = data.getElementsByAttributeValue("name", AwfulMessage.REPLY_DISABLE_SMILIES).first(); + if(formDisableEmotes.hasAttr("checked")){ + return "checked"; + }else{ + return ""; + } + } + + public static final String getSignatureOption(Document data){ + Element formSignature = data.getElementsByAttributeValue("name", AwfulMessage.REPLY_SIGNATURE).first(); + if(formSignature != null && formSignature.hasAttr("checked")){ + return "checked"; + }else{ + return ""; + } + } + + public static final ContentValues getFormData(Document data, ContentValues results) throws AwfulError { + try{ + Element formKey = data.getElementsByAttributeValue("name", "formkey").first(); + Element formCookie = data.getElementsByAttributeValue("name", "form_cookie").first(); + results.put(AwfulPost.FORM_KEY, formKey.val()); + results.put(AwfulPost.FORM_COOKIE, formCookie.val()); + }catch (Exception e){ + throw new AwfulError("Failed to load reply"); + } + return results; + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ThreadIconPicker.java b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ThreadIconPicker.java index 4089e3cf6..478285764 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ThreadIconPicker.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/widget/ThreadIconPicker.java @@ -166,6 +166,10 @@ private void useIcon(@NonNull AwfulPostIcon icon) { } } + public void useIcon(@NonNull String iconId, @NonNull String iconUrl) { + AwfulPostIcon newIcon = new AwfulPostIcon(iconId, iconUrl, getContext()); + useIcon(newIcon); + } /** * Get the currently selected icon. diff --git a/Awful.apk/src/main/res/drawable/ic_just_post.xml b/Awful.apk/src/main/res/drawable/ic_just_post.xml new file mode 100644 index 000000000..2a02e1219 --- /dev/null +++ b/Awful.apk/src/main/res/drawable/ic_just_post.xml @@ -0,0 +1,5 @@ + + + diff --git a/Awful.apk/src/main/res/layout/post_thread.xml b/Awful.apk/src/main/res/layout/post_thread.xml new file mode 100644 index 000000000..ae829256c --- /dev/null +++ b/Awful.apk/src/main/res/layout/post_thread.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/Awful.apk/src/main/res/layout/post_thread_activity.xml b/Awful.apk/src/main/res/layout/post_thread_activity.xml new file mode 100644 index 000000000..ca176fd16 --- /dev/null +++ b/Awful.apk/src/main/res/layout/post_thread_activity.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Awful.apk/src/main/res/menu/forum_display_fragment.xml b/Awful.apk/src/main/res/menu/forum_display_fragment.xml new file mode 100644 index 000000000..cd4ff9157 --- /dev/null +++ b/Awful.apk/src/main/res/menu/forum_display_fragment.xml @@ -0,0 +1,10 @@ + +

      + + \ No newline at end of file diff --git a/Awful.apk/src/main/res/menu/post_thread.xml b/Awful.apk/src/main/res/menu/post_thread.xml new file mode 100644 index 000000000..463424a24 --- /dev/null +++ b/Awful.apk/src/main/res/menu/post_thread.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Awful.apk/src/main/res/values/preference_keys.xml b/Awful.apk/src/main/res/values/preference_keys.xml index 12da51ff0..5716b04e6 100644 --- a/Awful.apk/src/main/res/values/preference_keys.xml +++ b/Awful.apk/src/main/res/values/preference_keys.xml @@ -73,6 +73,7 @@ marked_users favourite_forums recent_emotes + post_warning_accepted forum_index_show_subtitles forum_index_show_section_headers diff --git a/Awful.apk/src/main/res/values/strings.xml b/Awful.apk/src/main/res/values/strings.xml index b8688acb9..c27cd34bd 100644 --- a/Awful.apk/src/main/res/values/strings.xml +++ b/Awful.apk/src/main/res/values/strings.xml @@ -81,6 +81,7 @@ Thread rating This thread is a sticky This thread is locked + Post Thread Post reply @@ -516,6 +517,7 @@ Search in page Getting forums test + The functionality to create threads is fresh and may contain bugs that could potentially lead to auto-bans.\n\nThere are no safety nets.\n\nBy accepting this you acknowledge that you are voluntarily taking this risk and only have yourself to blame if you get banned, either due to an app-malfunction or (more likely) your dog-shit posting. I accept the terms and conditions of the forum rules From ce11462d7ecc346468e8f757304703e15ff24a47 Mon Sep 17 00:00:00 2001 From: Sereri Date: Sun, 2 Jun 2024 18:07:29 +0200 Subject: [PATCH 6/7] 3.9.8 fix thread posting fix attachments on API33+, fix P2R on zoom, fix disable swipe being disabled by code scroll --- Awful.apk/src/main/AndroidManifest.xml | 5 +++-- Awful.apk/src/main/assets/changelog.html | 3 ++- Awful.apk/src/main/assets/javascript/thread.js | 6 ++---- .../java/com/ferg/awfulapp/ForumsIndexActivity.kt | 4 +++- .../java/com/ferg/awfulapp/PostReplyFragment.java | 13 ++++++++----- .../java/com/ferg/awfulapp/PostThreadFragment.java | 2 +- .../com/ferg/awfulapp/ThreadDisplayFragment.java | 7 +++++++ .../java/com/ferg/awfulapp/constants/Constants.java | 1 + .../com/ferg/awfulapp/task/SendThreadRequest.kt | 2 +- 9 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index f71c07183..c8a648d0f 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -7,8 +7,8 @@ --> + diff --git a/Awful.apk/src/main/assets/changelog.html b/Awful.apk/src/main/assets/changelog.html index 067d1fbf7..7e91c60bd 100644 --- a/Awful.apk/src/main/assets/changelog.html +++ b/Awful.apk/src/main/assets/changelog.html @@ -6,10 +6,11 @@
      -

      3.9.6

      +

      3.9.8

      • Replaced the display image zoom thing so that it doesn't just enlarge the image but also actually zooms in. The wonders of modern technology.
      • Added function to increase the number of bad threads on the forums. May dog have mercy on our souls.
      • +
      • Fixed file attachments again (Android 13 and up this time).
      diff --git a/Awful.apk/src/main/assets/javascript/thread.js b/Awful.apk/src/main/assets/javascript/thread.js index c06beafa2..d25d9873d 100644 --- a/Awful.apk/src/main/assets/javascript/thread.js +++ b/Awful.apk/src/main/assets/javascript/thread.js @@ -310,7 +310,7 @@ function showReadPosts() { * @param {string} url url to zoom into */ function showImageZoom(url) { - listener.haltSwipe(); + listener.setZoomEnabled(true); var zoom = document.createElement('div'); zoom.setAttribute('id', 'zoom'); zoom.classList.add('zoom-enabled'); @@ -417,7 +417,6 @@ function showImageZoom(url) { imageX = imageCurrentX; imageY = imageCurrentY; }); - listener.setZoomEnabled(true); } /** @@ -427,7 +426,6 @@ function exitImageZoom() { if(!document.getElementById('zoom')){ return } document.getElementById('zoom').remove(); document.getElementById('zoom-close').remove(); - listener.resumeSwipe(); listener.setZoomEnabled(false); } @@ -496,7 +494,7 @@ function changeFontFace(font) { var styleElement = document.createElement('style'); styleElement.id = 'font-face'; styleElement.setAttribute('type', 'text/css'); - styleElement.textContent = '@font-face { font-family: userselected; src: url(\'content://com.ferg.awfulapp.webprovider/' + font + '\'); }'; + styleElement.textContent = '@font-face { font-family: userselected; src: url(\'file:///android_asset/' + font + '\'); }'; document.head.appendChild(styleElement); } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexActivity.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexActivity.kt index b783e23ca..0a8bf955d 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexActivity.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexActivity.kt @@ -275,7 +275,9 @@ class ForumsIndexActivity : } fun allowSwipe() { - runOnUiThread { forumsPager.setSwipeEnabled(true) } + if(!mPrefs.lockScrolling) { + runOnUiThread { forumsPager.setSwipeEnabled(true) } + } } override fun onWindowFocusChanged(hasFocus: Boolean) { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java index aa8cb900b..d8631941c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java @@ -226,13 +226,15 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { int permissionCheck = ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE); if (permissionCheck != PackageManager.PERMISSION_GRANTED) { this.attachmentData = data; - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, Constants.AWFUL_PERMISSION_READ_EXTERNAL_STORAGE); - } else { - addAttachment(data); + if (AwfulUtils.isTiramisu33()) { + requestPermissions(new String[]{Manifest.permission.READ_MEDIA_IMAGES}, Constants.AWFUL_PERMISSION_READ_MEDIA_IMAGES); + } else { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, Constants.AWFUL_PERMISSION_READ_EXTERNAL_STORAGE); + } + return; } - } else { - addAttachment(data); } + addAttachment(data); } } } @@ -242,6 +244,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case Constants.AWFUL_PERMISSION_READ_EXTERNAL_STORAGE: + case Constants.AWFUL_PERMISSION_READ_MEDIA_IMAGES: // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { addAttachment(); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java index 9bcb71e71..8ebc9cfd0 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java @@ -362,7 +362,7 @@ private void displayDraftAlert(@NonNull SavedDraft draft) { // If we're quoting something, stick it after the draft thread (and add some whitespace too) messageComposer.setText(newContent, true); subject.setText(draft.subject); - if(draft.iconId != null && draft.iconUrl != null){ + if(draft.iconId != null && draft.iconUrl != null && draft.iconUrl.length() > 0){ threadIconPicker.useIcon(draft.iconId, draft.iconUrl); } }) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java index 9e75a622f..a4f8a85be 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java @@ -1217,6 +1217,13 @@ public void resumeSwipe() { @JavascriptInterface public void setZoomEnabled(boolean zoomOn) { zoomEnabled = zoomOn; + if (zoomOn) { + haltSwipe(); + getSwipyLayout().setEnabled(false); + } else { + resumeSwipe(); + getSwipyLayout().setEnabled(!getPrefs().disablePullNext); + } } @JavascriptInterface diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java b/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java index affbf4437..3a8b2e2c4 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java @@ -227,6 +227,7 @@ public class Constants { public static final int AWFUL_PERMISSION_READ_EXTERNAL_STORAGE = 123; public static final int AWFUL_PERMISSION_WRITE_EXTERNAL_STORAGE = 124; + public static final int AWFUL_PERMISSION_READ_MEDIA_IMAGES = 125; public enum POST_ICON_REQUEST_TYPES { FORUM_POST, PM diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendThreadRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendThreadRequest.kt index 5e1d9625b..7439fcb95 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendThreadRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendThreadRequest.kt @@ -23,7 +23,7 @@ class SendThreadRequest(context: Context, reply: ContentValues) add(PARAM_SUBJECT, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.POST_SUBJECT))) add(PARAM_MESSAGE, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.POST_CONTENT))) add(PARAM_PARSEURL, YES) - add("iconid", reply.getAsString(AwfulMessage.REPLY_ICON)) + add("iconid", reply.getAsString(AwfulMessage.POST_ICON_ID)) if (reply.getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { add(PARAM_BOOKMARK, YES) } From ddede8ed34fd252a32b447efb943f0e1207c165b Mon Sep 17 00:00:00 2001 From: Sereri Date: Sun, 10 Nov 2024 22:10:58 +0100 Subject: [PATCH 7/7] 3.9.9 Fnots wrok agian, hwat a mses. OpenDislexic adedd. --- Awful.apk/build.gradle | 25 ++++---- Awful.apk/src/main/AndroidManifest.xml | 4 +- Awful.apk/src/main/assets/changelog.html | 4 +- .../main/assets/fonts/open_dyslexic.ttf.mp3 | Bin 0 -> 237868 bytes .../ferg/awfulapp/AwfulDialogFragment.java | 1 + .../java/com/ferg/awfulapp/AwfulFragment.kt | 7 ++- .../com/ferg/awfulapp/AwfulLoginActivity.java | 1 + .../java/com/ferg/awfulapp/EmoteFragment.kt | 34 +++++++---- .../java/com/ferg/awfulapp/FontManager.java | 31 +++++++++- .../ferg/awfulapp/ForumDisplayFragment.java | 1 + .../ferg/awfulapp/ForumsIndexFragment.java | 1 + .../com/ferg/awfulapp/MessageFragment.java | 8 ++- .../com/ferg/awfulapp/NavigationDrawer.kt | 11 ++++ .../com/ferg/awfulapp/PostReplyFragment.java | 27 +++++++-- .../com/ferg/awfulapp/PostThreadFragment.java | 30 +++++++-- .../com/ferg/awfulapp/PreviewFragment.java | 7 ++- .../awfulapp/PrivateMessageListFragment.java | 1 + .../ferg/awfulapp/ThreadDisplayFragment.java | 4 ++ .../announcements/AnnouncementsFragment.java | 1 + .../com/ferg/awfulapp/dialog/Changelog.kt | 13 +++- .../awfulapp/forums/ForumListAdapter.java | 5 ++ .../awfulapp/popupmenu/BasePopupMenu.java | 11 ++-- .../preferences/SettingsActivity.java | 56 ++++------------- .../fragments/AccountSettings.java | 4 +- .../fragments/ForumIndexSettings.java | 2 +- .../preferences/fragments/MiscSettings.java | 4 +- .../preferences/fragments/PostSettings.java | 2 +- .../preferences/fragments/RootSettings.java | 35 ++++++++++- .../fragments/SettingsFragment.java | 57 ++++++++++++++++-- .../preferences/fragments/ThemeSettings.java | 4 +- .../ferg/awfulapp/reply/MessageComposer.java | 6 ++ .../com/ferg/awfulapp/search/SearchFilter.kt | 7 ++- .../awfulapp/search/SearchForumsFragment.java | 3 + .../ferg/awfulapp/search/SearchFragment.kt | 35 ++++++++--- .../ferg/awfulapp/thread/AwfulHtmlPage.java | 2 +- .../ferg/awfulapp/webview/AwfulWebView.java | 8 +-- .../src/main/res/xml/accountsettings.xml | 7 ++- .../src/main/res/xml/forum_index_settings.xml | 8 ++- Awful.apk/src/main/res/xml/imagesettings.xml | 11 +++- Awful.apk/src/main/res/xml/miscsettings.xml | 20 +++++- .../main/res/xml/post_embedding_settings.xml | 14 ++++- .../res/xml/post_highlighting_settings.xml | 8 ++- Awful.apk/src/main/res/xml/postsettings.xml | 13 +++- Awful.apk/src/main/res/xml/rootsettings.xml | 18 +++++- Awful.apk/src/main/res/xml/themesettings.xml | 8 ++- .../src/main/res/xml/threadinfosettings.xml | 12 +++- build.gradle | 6 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 48 files changed, 437 insertions(+), 144 deletions(-) create mode 100644 Awful.apk/src/main/assets/fonts/open_dyslexic.ttf.mp3 diff --git a/Awful.apk/build.gradle b/Awful.apk/build.gradle index 1cb1c103c..e94ff7466 100644 --- a/Awful.apk/build.gradle +++ b/Awful.apk/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = '1.9.21' dependencies { - classpath 'com.android.tools.build:gradle:8.2.0' + classpath 'com.android.tools.build:gradle:8.7.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,8 +25,8 @@ android { defaultConfig { applicationId = "com.ferg.awfulapp" minSdkVersion 24 - targetSdkVersion 34 - resConfigs 'en' + targetSdkVersion 35 + resourceConfigurations += ['en'] // Stops the Gradle plugin’s automatic rasterization of vectors vectorDrawables.useSupportLibrary = true @@ -66,9 +66,9 @@ android { //proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.cfg' } } - - kotlin { - jvmToolchain(17) + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 } task copyThreadTags { @@ -105,8 +105,8 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' // these are all needed to override some old versions that are dependencies... somewhere implementation 'androidx.media:media:1.7.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' @@ -116,9 +116,9 @@ dependencies { implementation 'com.android.volley:volley:1.2.1' - implementation 'com.google.code.gson:gson:2.10' + implementation 'com.google.code.gson:gson:2.11.0' - implementation 'org.jsoup:jsoup:1.15.4' + implementation 'org.jsoup:jsoup:1.18.1' implementation 'com.jakewharton.threetenabp:threetenabp:1.2.4' implementation 'com.samskivert:jmustache:1.15' @@ -135,10 +135,11 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.github.rubensousa:BottomSheetBuilder:1.5.1' + implementation 'androidx.preference:preference:1.2.1' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:hamcrest-library:1.3' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' // updating this might cause the status and navigation bar to become blue, test for this implementation 'androidx.core:core-splashscreen:1.0.0-alpha02' diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index c8a648d0f..bc1a41d0a 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -7,8 +7,8 @@ -->
      -

      3.9.8

      +

      3.9.9

      • Replaced the display image zoom thing so that it doesn't just enlarge the image but also actually zooms in. The wonders of modern technology.
      • Added function to increase the number of bad threads on the forums. May dog have mercy on our souls.
      • Fixed file attachments again (Android 13 and up this time).
      • +
      • Fixed the theme font functionality. You can now make (most of) the app look like a 2009 Samsung phone again.
      • +
      • Added OpenDislexic font. Gee Whiz, what a coincidence.
      diff --git a/Awful.apk/src/main/assets/fonts/open_dyslexic.ttf.mp3 b/Awful.apk/src/main/assets/fonts/open_dyslexic.ttf.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0ff4c0b58dffcb3302aae160e88baefd8b73ce33 GIT binary patch literal 237868 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 literal 0 HcmV?d00001 diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulDialogFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulDialogFragment.java index dcb790d24..e743d6645 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulDialogFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulDialogFragment.java @@ -82,6 +82,7 @@ protected View inflateView(int resId, ViewGroup container, LayoutInflater inflat if(progressBar instanceof AwfulProgressBar){ this.progressBar = (AwfulProgressBar) progressBar; } + getAwfulActivity().setPreferredFont(v); return v; } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt index 9a06a75df..1e7b004ce 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt @@ -32,14 +32,14 @@ import android.content.ClipboardManager import android.content.Context import android.os.Bundle import android.os.Handler -import androidx.annotation.StringRes -import androidx.fragment.app.Fragment -import androidx.loader.app.LoaderManager import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import androidx.loader.app.LoaderManager import com.android.volley.Request import com.android.volley.VolleyError import com.ferg.awfulapp.network.NetworkUtils @@ -105,6 +105,7 @@ abstract class AwfulFragment : Fragment(), AwfulPreferences.AwfulPreferenceUpdat progressBar = v.findViewById(R.id.progress_bar) probationBar = v.findViewById(R.id.probation_bar) probationBar?.setListener { navigate(NavigationEvent.LepersColony(prefs.userId)) } + awfulActivity!!.setPreferredFont(v) return v } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulLoginActivity.java b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulLoginActivity.java index 66c393e24..2cc5bf996 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulLoginActivity.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulLoginActivity.java @@ -163,6 +163,7 @@ private void loginClick() { final String password = NetworkUtils.encodeHtml(mPassword.getText().toString()); mDialog = ProgressDialog.show(AwfulLoginActivity.this, "Logging In", "Hold on...", true); + setPreferredFont(mDialog.findViewById(android.R.id.title)); final AwfulLoginActivity self = this; NetworkUtils.queueRequest(new LoginRequest(this, username, password).build(null, new AwfulRequest.AwfulResultCallback() { @Override diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt index ce7f4d884..220725302 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt @@ -2,21 +2,22 @@ package com.ferg.awfulapp import android.database.Cursor import android.os.Bundle -import com.google.android.material.tabs.TabLayout -import androidx.core.app.* -import androidx.loader.content.CursorLoader -import androidx.loader.content.Loader -import androidx.viewpager.widget.ViewPager import android.text.Editable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* -import androidx.fragment.app.DialogFragment +import android.widget.EditText +import android.widget.GridView +import android.widget.ImageButton +import android.widget.TextView +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import androidx.loader.app.LoaderManager +import androidx.loader.content.CursorLoader +import androidx.loader.content.Loader +import androidx.viewpager.widget.ViewPager import com.android.volley.VolleyError import com.ferg.awfulapp.constants.Constants import com.ferg.awfulapp.preferences.AwfulPreferences @@ -29,6 +30,7 @@ import com.ferg.awfulapp.task.EmoteRequest import com.ferg.awfulapp.thread.AwfulEmote import com.ferg.awfulapp.util.PassiveTextWatcher import com.ferg.awfulapp.util.bind +import com.google.android.material.tabs.TabLayout import timber.log.Timber /** @@ -85,14 +87,16 @@ private object EmoteHistory { * * This must be created by a parent fragment that implements [EmotePickerListener] */ -class EmotePicker : DialogFragment() { +class EmotePicker : AwfulDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - return inflater.inflate(R.layout.emote_picker_container_fragment, container, false) + val view = inflater.inflate(R.layout.emote_picker_container_fragment, container, false); + awfulActivity.setPreferredFont(view) + return view; } override fun onActivityCreated(aSavedState: Bundle?) { @@ -110,6 +114,10 @@ class EmotePicker : DialogFragment() { } } + override fun getTitle(): String { + return "Emote" + } + fun onEmoteChosen(emoteCode: String) { Toast.makeText(activity, emoteCode, Toast.LENGTH_SHORT).show() (parentFragment as EmotePickerListener).onEmoteChosen(emoteCode) @@ -205,8 +213,12 @@ abstract class EmoteGridFragment : AwfulFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? = - inflater.inflate(layoutId, container, false) + ): View? { + val view = inflater.inflate(layoutId, container, false); + awfulActivity?.setPreferredFont(view) + return view; + } + /** diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/FontManager.java b/Awful.apk/src/main/java/com/ferg/awfulapp/FontManager.java index 5b8c29485..6f871c55f 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/FontManager.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/FontManager.java @@ -4,11 +4,16 @@ import android.graphics.Typeface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import android.text.SpannableStringBuilder; +import android.text.style.TypefaceSpan; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.ferg.awfulapp.preferences.AwfulPreferences; +import com.google.android.material.textfield.TextInputLayout; import org.apache.commons.lang3.text.WordUtils; @@ -113,9 +118,11 @@ public void setCurrentFont(String fontName) { * {@link Typeface#ITALIC}, or {@link Typeface#BOLD_ITALIC}, */ public void setTypefaceToCurrentFont(View view, int flags) { - if (view instanceof TextView) + if (view instanceof TextView) { setTextViewTypefaceToCurrentFont((TextView) view, flags); - else if (view instanceof ViewGroup) { + } else if(view instanceof TextInputLayout){ + setTextViewTypefaceToCurrentFont((TextInputLayout) view); + } else if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) @@ -174,6 +181,26 @@ private void setTextViewTypefaceToCurrentFont(TextView textView, int textStyle) Timber.w("Couldn't set typeface as currentFont is null"); } + /** + * Set a TextView's typeface to the current font. + * + * @param textLayout TextView to set + */ + private void setTextViewTypefaceToCurrentFont(TextInputLayout textLayout) { + + if (currentFont != null) + textLayout.setTypeface(currentFont); + else + Timber.w("Couldn't set typeface as currentFont is null"); + } + + public void setMenuItemFont(MenuItem item) { + SpannableStringBuilder title = new SpannableStringBuilder(item.getTitle()); + TypefaceSpan face = new TypefaceSpan(currentFont); + title.setSpan(face, 0, title.length(), 0); + item.setTitle(title); + } + /** * Check if the passed text style is valid. * diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java index 2bbf9f04c..20b115ab0 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java @@ -161,6 +161,7 @@ public void onPageNumberClicked() { refreshProbationBar(); + getAwfulActivity().setPreferredFont(result); return result; } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexFragment.java index 4e1b5f9ca..155438d5c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumsIndexFragment.java @@ -95,6 +95,7 @@ public View onCreateView(@NonNull LayoutInflater aInflater, ViewGroup aContainer updateViewColours(); refreshProbationBar(); forumsListSwitcher.setInAnimation(AnimationUtils.makeInAnimation(getContext(), true)); + getAwfulActivity().setPreferredFont(view); return view; } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/MessageFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/MessageFragment.java index 4cd24ea95..18f01ebae 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/MessageFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/MessageFragment.java @@ -135,6 +135,8 @@ public View onCreateView(LayoutInflater aInflater, ViewGroup aContainer, Bundle }else{ syncPM(); } + + getAwfulActivity().setPreferredFont(result); return result; } @@ -224,7 +226,8 @@ private void showSubmitDialog() { (dialog, button) -> { if (mDialog == null && getActivity() != null) { mDialog = ProgressDialog.show(getActivity(), "Sending", "Hopefully it didn't suck...", true, true); - } + getAwfulActivity().setPreferredFont(mDialog.findViewById(android.R.id.title)); + } saveReply(); sendPM(); }) @@ -399,6 +402,7 @@ public void onLoadFinished(Loader aLoader, Cursor aData) { }else{ mSubject.setText(title); } + getAwfulActivity().setPreferredFont(mSubject); String author = aData.getString(aData.getColumnIndex(AwfulMessage.AUTHOR)); mUsername.setText("Sender: " + author); String recip = aData.getString(aData.getColumnIndex(AwfulMessage.RECIPIENT)); @@ -407,11 +411,13 @@ public void onLoadFinished(Loader aLoader, Cursor aData) { }else{ mRecipient.setText(author); } + }else{ if(recipient != null){ mRecipient.setText(recipient); } } + getAwfulActivity().setPreferredFont(mRecipient); } @Override diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationDrawer.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationDrawer.kt index 3997ada86..bc4d9bed2 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationDrawer.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationDrawer.kt @@ -11,6 +11,7 @@ import androidx.appcompat.widget.Toolbar import android.view.MenuItem import android.widget.ImageView import android.widget.TextView +import androidx.core.view.forEach import com.android.volley.VolleyError import com.android.volley.toolbox.ImageLoader import com.ferg.awfulapp.ForumDisplayFragment.NULL_FORUM_ID @@ -84,6 +85,10 @@ class NavigationDrawer(val activity: AwfulActivity, toolbar: Toolbar, val prefs: prefs.registerCallback { _, _ -> refresh() } AnnouncementsManager.getInstance().registerListener { _, _, _, _ -> refresh() } refresh() + activity.setPreferredFont(navigationMenu) + navigationMenu.menu.forEach { + FontManager.getInstance().setMenuItemFont(it) + } } private fun menuItem(resId: Int) = navigationMenu.menu.findItem(resId) @@ -151,6 +156,9 @@ class NavigationDrawer(val activity: AwfulActivity, toolbar: Toolbar, val prefs: val unread = AnnouncementsManager.getInstance().unreadCount announcementsItem.title = getString(R.string.announcements) + if (unread == 0) "" else " ($unread)" + + activity.setPreferredFont(navigationMenu) + FontManager.getInstance().setMenuItemFont(announcementsItem) } fun setCurrentForumAndThread(forumId: Int?, threadId: Int?) { @@ -195,6 +203,7 @@ private class HierarchyTextUpdater( private val appContext = context.applicationContext private val forumMenuItem = WeakReference(forumItem) private val threadMenuItem = WeakReference(threadItem) + private val fm = FontManager.getInstance() override fun doInBackground(vararg params: Void?): Void? { threadName = StringProvider.getThreadName(appContext, threadId) @@ -205,6 +214,8 @@ private class HierarchyTextUpdater( override fun onPostExecute(aVoid: Void?) { forumMenuItem.get()?.title = forumName ?: "" threadMenuItem.get()?.title = threadName ?: "" + fm.setMenuItemFont(forumMenuItem.get()) + fm.setMenuItemFont(threadMenuItem.get()) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java index d8631941c..017c85dc9 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PostReplyFragment.java @@ -172,6 +172,7 @@ public View onCreateView(LayoutInflater aInflater, ViewGroup aContainer, Bundle super.onCreateView(aInflater, aContainer, aSavedState); Timber.v("onCreateView"); View view = inflateView(R.layout.post_reply, aContainer, aInflater); + getAwfulActivity().setPreferredFont(view); return view; } @@ -281,7 +282,7 @@ private void refreshThreadInfo() { */ private void loadReply(int mReplyType, int mThreadId, int mPostId) { progressDialog = ProgressDialog.show(getActivity(), "Loading", "Fetching Message...", true, true); - + getAwfulActivity().setPreferredFont(progressDialog.findViewById(android.R.id.title)); // create a callback to handle the reply data from the site AwfulRequest.AwfulResultCallback loadCallback = new AwfulRequest.AwfulResultCallback() { @Override @@ -411,7 +412,7 @@ private void displayDraftAlert(@NonNull SavedDraft draft) { String message = String.format(template, type.toLowerCase(), previewText, epochToSimpleDuration(draft.timestamp)); String positiveLabel = (mReplyType == TYPE_QUOTE) ? "Add" : "Use"; - new AlertDialog.Builder(activity) + AlertDialog use = new AlertDialog.Builder(activity) .setIcon(R.drawable.ic_reply_dark) .setTitle(type) .setMessage(Html.fromHtml(message)) @@ -427,6 +428,11 @@ private void displayDraftAlert(@NonNull SavedDraft draft) { // avoid accidental draft losses by forcing a decision .setCancelable(false) .show(); + + getAwfulActivity().setPreferredFont(use.findViewById(androidx.appcompat.R.id.alertTitle)); + getAwfulActivity().setPreferredFont(use.findViewById(android.R.id.message)); + getAwfulActivity().setPreferredFont(use.findViewById(android.R.id.button1)); + getAwfulActivity().setPreferredFont(use.findViewById(android.R.id.button2)); } @@ -439,12 +445,13 @@ private void displayDraftAlert(@NonNull SavedDraft draft) { * Display a dialog allowing the user to submit or preview their post */ private void showSubmitDialog() { - new AlertDialog.Builder(getActivity()) + AlertDialog submit = new AlertDialog.Builder(getActivity()) .setTitle(String.format("Confirm %s?", mReplyType == TYPE_EDIT ? "Edit" : "Post")) .setPositiveButton(R.string.submit, (dialog, button) -> { if (progressDialog == null && getActivity() != null) { progressDialog = ProgressDialog.show(getActivity(), "Posting", "Hopefully it didn't suck...", true, true); + getAwfulActivity().setPreferredFont(progressDialog.findViewById(android.R.id.title)); } saveReply(); submitPost(); @@ -453,6 +460,12 @@ private void showSubmitDialog() { .setNegativeButton(R.string.cancel, (dialog, button) -> { }) .show(); + + getAwfulActivity().setPreferredFont(submit.findViewById(androidx.appcompat.R.id.alertTitle)); + getAwfulActivity().setPreferredFont(submit.findViewById(android.R.id.message)); + getAwfulActivity().setPreferredFont(submit.findViewById(android.R.id.button1)); + getAwfulActivity().setPreferredFont(submit.findViewById(android.R.id.button2)); + getAwfulActivity().setPreferredFont(submit.findViewById(android.R.id.button3)); } @@ -647,7 +660,7 @@ void onNavigateBack() { leave(RESULT_CANCELLED); return; } - new AlertDialog.Builder(activity) + AlertDialog save = new AlertDialog.Builder(activity) .setIcon(R.drawable.ic_reply_dark) .setMessage(String.format("Save this %s?", mReplyType == TYPE_EDIT ? "edit" : "post")) .setPositiveButton(R.string.save, (dialog, button) -> { @@ -665,6 +678,11 @@ void onNavigateBack() { .setCancelable(true) .show(); + getAwfulActivity().setPreferredFont(save.findViewById(androidx.appcompat.R.id.alertTitle)); + getAwfulActivity().setPreferredFont(save.findViewById(android.R.id.message)); + getAwfulActivity().setPreferredFont(save.findViewById(android.R.id.button1)); + getAwfulActivity().setPreferredFont(save.findViewById(android.R.id.button2)); + getAwfulActivity().setPreferredFont(save.findViewById(android.R.id.button3)); } @@ -1047,6 +1065,7 @@ private void updateThreadTitle() { if (threadTitleView != null) { threadTitleView.setText(mThreadTitle == null ? "" : mThreadTitle); } + getAwfulActivity().setPreferredFont(threadTitleView); } @Override diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java index 8ebc9cfd0..554225d25 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PostThreadFragment.java @@ -162,6 +162,7 @@ public View onCreateView(LayoutInflater aInflater, ViewGroup aContainer, Bundle super.onCreateView(aInflater, aContainer, aSavedState); Timber.v("onCreateView"); View view = inflateView(R.layout.post_thread, aContainer, aInflater); + getAwfulActivity().setPreferredFont(view); return view; } @@ -266,6 +267,7 @@ private void refreshForumInfo() { */ private void loadThread(int mForumId) { progressDialog = ProgressDialog.show(getActivity(), "Loading", "Fetching Message...", true, true); + getAwfulActivity().setPreferredFont(progressDialog.findViewById(android.R.id.title)); // create a callback to handle the thread data from the site AwfulRequest.AwfulResultCallback loadCallback = new AwfulRequest.AwfulResultCallback() { @@ -353,7 +355,7 @@ private void displayDraftAlert(@NonNull SavedDraft draft) { } String message = String.format(template, type, draft.subject , previewText, epochToSimpleDuration(draft.timestamp)); - new AlertDialog.Builder(activity) + AlertDialog use = new AlertDialog.Builder(activity) .setIcon(R.drawable.ic_reply_dark) .setTitle(type) .setMessage(Html.fromHtml(message)) @@ -370,6 +372,11 @@ private void displayDraftAlert(@NonNull SavedDraft draft) { // avoid accidental draft losses by forcing a decision .setCancelable(false) .show(); + + getAwfulActivity().setPreferredFont(use.findViewById(androidx.appcompat.R.id.alertTitle)); + getAwfulActivity().setPreferredFont(use.findViewById(android.R.id.message)); + getAwfulActivity().setPreferredFont(use.findViewById(android.R.id.button1)); + getAwfulActivity().setPreferredFont(use.findViewById(android.R.id.button2)); } @@ -382,20 +389,27 @@ private void displayDraftAlert(@NonNull SavedDraft draft) { * Display a dialog allowing the user to submit or preview their post */ private void showSubmitDialog() { - new AlertDialog.Builder(getActivity()) + AlertDialog submit = new AlertDialog.Builder(getActivity()) .setTitle("Confirm Post?") .setPositiveButton(R.string.submit, (dialog, button) -> { if (progressDialog == null && getActivity() != null) { progressDialog = ProgressDialog.show(getActivity(), "Posting", "Hopefully it didn't suck...", true, true); + getAwfulActivity().setPreferredFont(progressDialog.findViewById(android.R.id.title)); } saveThread(); submitThread(); }) .setNeutralButton(R.string.preview, (dialog, button) -> previewPost()) .setNegativeButton(R.string.cancel, (dialog, button) -> { - }) - .show(); + }).show(); + + getAwfulActivity().setPreferredFont(submit.findViewById(androidx.appcompat.R.id.alertTitle)); + getAwfulActivity().setPreferredFont(submit.findViewById(android.R.id.message)); + getAwfulActivity().setPreferredFont(submit.findViewById(android.R.id.button1)); + getAwfulActivity().setPreferredFont(submit.findViewById(android.R.id.button2)); + getAwfulActivity().setPreferredFont(submit.findViewById(android.R.id.button3)); + } @@ -582,7 +596,7 @@ void onNavigateBack() { leave(RESULT_CANCELLED); return; } - new AlertDialog.Builder(activity) + AlertDialog save = new AlertDialog.Builder(activity) .setIcon(R.drawable.ic_reply_dark) .setMessage("Save this thread?") .setPositiveButton(R.string.save, (dialog, button) -> { @@ -600,6 +614,12 @@ void onNavigateBack() { .setCancelable(true) .show(); + + getAwfulActivity().setPreferredFont(save.findViewById(androidx.appcompat.R.id.alertTitle)); + getAwfulActivity().setPreferredFont(save.findViewById(android.R.id.message)); + getAwfulActivity().setPreferredFont(save.findViewById(android.R.id.button1)); + getAwfulActivity().setPreferredFont(save.findViewById(android.R.id.button2)); + getAwfulActivity().setPreferredFont(save.findViewById(android.R.id.button3)); } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PreviewFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PreviewFragment.java index 06b61153e..edd5f855e 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PreviewFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PreviewFragment.java @@ -45,7 +45,7 @@ import java.util.HashMap; -public class PreviewFragment extends DialogFragment { +public class PreviewFragment extends AwfulDialogFragment { private AwfulWebView postPreView; private ProgressBar progressBar; @@ -64,6 +64,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, getDialog().setCanceledOnTouchOutside(true); postPreView.setContent(getBlankPage()); + getAwfulActivity().setPreferredFont(dialogView); return dialogView; } @@ -100,4 +101,8 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest wrr) { postPreView.setJavascriptHandler(jsInterface); } + @Override + public String getTitle() { + return "Preview"; + } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PrivateMessageListFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PrivateMessageListFragment.java index cc12568a4..7d41a3777 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PrivateMessageListFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PrivateMessageListFragment.java @@ -114,6 +114,7 @@ public View onCreateView(LayoutInflater aInflater, ViewGroup aContainer, Bundle mFAB.setOnClickListener(onButtonClick); mFAB.setVisibility((getPrefs().noFAB ? View.GONE : View.VISIBLE)); + getAwfulActivity().setPreferredFont(result); return result; } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java index a4f8a85be..ca6a6bbff 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java @@ -540,6 +540,10 @@ public void onPrepareOptionsMenu(Menu menu) { if(yospos != null){ yospos.setVisible(mParentForumId == Constants.FORUM_ID_YOSPOS); } + FontManager fm = FontManager.getInstance(); + for (int i = 0; i < menu.size(); i++) { + fm.setMenuItemFont(menu.getItem(i)); + } } @Override diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/announcements/AnnouncementsFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/announcements/AnnouncementsFragment.java index f98505411..6ecffc769 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/announcements/AnnouncementsFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/announcements/AnnouncementsFragment.java @@ -54,6 +54,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c binding = AnnouncementsFragmentBinding.inflate(inflater, container, false); View view = binding.getRoot(); initialiseWebView(); + getAwfulActivity().setPreferredFont(view); return view; } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/dialog/Changelog.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/dialog/Changelog.kt index c9e986147..9a0df2703 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/dialog/Changelog.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/dialog/Changelog.kt @@ -1,8 +1,9 @@ package com.ferg.awfulapp.dialog import android.content.Context -import androidx.appcompat.app.AlertDialog import android.text.Html +import androidx.appcompat.app.AlertDialog +import com.ferg.awfulapp.AwfulActivity import com.ferg.awfulapp.R import org.jsoup.Jsoup import org.jsoup.nodes.Document @@ -32,10 +33,16 @@ object Changelog { } // Build a basic dialog with the changelog html - tag handler required pre-Nougat - AlertDialog.Builder(context) + val changelogAlert = AlertDialog.Builder(context) .setTitle(context.getString(R.string.changelog_dialog_title)) .setMessage(Html.fromHtml(changelogText, null, listTagHandler)) - .setPositiveButton(context.getString(R.string.alert_ok)) { dialog, _ -> dialog.dismiss() }.show() + .setPositiveButton(context.getString(R.string.alert_ok)) { dialog, _ -> dialog.dismiss() }.create() + + changelogAlert.show(); + val activity = context as AwfulActivity; + activity.setPreferredFont(changelogAlert.findViewById(androidx.appcompat.R.id.alertTitle)) + activity.setPreferredFont(changelogAlert.findViewById(android.R.id.message)) + activity.setPreferredFont(changelogAlert.findViewById(android.R.id.button1)) } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumListAdapter.java b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumListAdapter.java index 58bedfaf1..94ffb7de3 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumListAdapter.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumListAdapter.java @@ -16,6 +16,7 @@ import com.bignerdranch.expandablerecyclerview.Model.ParentListItem; import com.bignerdranch.expandablerecyclerview.ViewHolder.ChildViewHolder; import com.bignerdranch.expandablerecyclerview.ViewHolder.ParentViewHolder; +import com.ferg.awfulapp.AwfulActivity; import com.ferg.awfulapp.R; import com.ferg.awfulapp.databinding.ForumIndexItemBinding; import com.ferg.awfulapp.databinding.ForumIndexSubforumItemBinding; @@ -37,6 +38,7 @@ */ public class ForumListAdapter extends ExpandableRecyclerAdapter { + private final AwfulActivity parent; private final AwfulPreferences awfulPrefs; @NonNull private final EventListener eventListener; @@ -54,6 +56,7 @@ private ForumListAdapter(@NonNull Context context, @NonNull EventListener listener, @Nullable AwfulPreferences awfulPreferences) { super(topLevelForums); + parent = (AwfulActivity) context; eventListener = listener; awfulPrefs = awfulPreferences; inflater = LayoutInflater.from(context); @@ -273,6 +276,8 @@ void bind(final TopLevelForum forumItem) { setThemeColours(itemView, binding.forumTitle, binding.forumSubtitle); handleSubtitles(forum, binding.forumSubtitle); + parent.setPreferredFont(itemView); + binding.forumFavouriteMarker.setVisibility(forum.isFavourite() ? VISIBLE : GONE); /* the left section (potentially) has a tag and a dropdown button, anything missing diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/BasePopupMenu.java b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/BasePopupMenu.java index ad8699aa0..9097ef28d 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/BasePopupMenu.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/BasePopupMenu.java @@ -14,6 +14,7 @@ import android.widget.ImageView; import android.widget.TextView; +import com.ferg.awfulapp.AwfulDialogFragment; import com.ferg.awfulapp.R; import com.ferg.awfulapp.databinding.ActionItemBinding; import com.ferg.awfulapp.provider.ColorProvider; @@ -31,7 +32,7 @@ * the menu items as an enum, add whichever items you need, and switch on the enum cases to handle * the user's selection. Just pass the enum as the type parameter for the class. */ -public abstract class BasePopupMenu extends DialogFragment { +public abstract class BasePopupMenu extends AwfulDialogFragment { /** * Can be used to set a callback that is called when an action is clicked. @@ -81,6 +82,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, actionsView.setLayoutManager(new LinearLayoutManager(getContext())); getDialog().setCanceledOnTouchOutside(true); + getAwfulActivity().setPreferredFont(result); return result; } @@ -104,12 +106,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @NonNull abstract List generateMenuItems(); - /** - * The title to display on the dialog. - */ - @NonNull - abstract String getTitle(); - /** * Called when the user selects one of your menu items. * @@ -147,6 +143,7 @@ private class ActionHolderAdapter extends RecyclerView.Adapter { @Override public ActionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.action_item, parent, false); + getAwfulActivity().setPreferredFont(view); return new ActionHolder(view); } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/SettingsActivity.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/SettingsActivity.java index bf4b4b117..0df815cd3 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/SettingsActivity.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/SettingsActivity.java @@ -1,18 +1,15 @@ package com.ferg.awfulapp.preferences; import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; @@ -103,6 +100,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.settings); View leftPane = findViewById(R.id.root_fragment_container); + View mainPane = findViewById(R.id.main_fragment_container); if (leftPane != null && leftPane.getVisibility() == View.VISIBLE) { isDualPane = true; } @@ -116,7 +114,7 @@ protected void onCreate(Bundle savedInstanceState) { startFragment = new RootSettings(); } - FragmentManager fm = getFragmentManager(); + FragmentManager fm = getSupportFragmentManager(); // if there's no previous fragment history being restored, initialise! // we need to start with the root fragment, so it's always under the backstack if (savedInstanceState == null) { @@ -141,6 +139,8 @@ protected void onCreate(Bundle savedInstanceState) { setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); updateTitleBar(); + setPreferredFont(leftPane); + setPreferredFont(mainPane); } @@ -151,7 +151,7 @@ protected void onCreate(Bundle savedInstanceState) { */ @Override public void onBackPressed() { - FragmentManager fm = getFragmentManager(); + FragmentManager fm = getSupportFragmentManager(); int backStackCount = fm.getBackStackEntryCount(); // don't pop off the first entry in dual-pane mode, it will leave the second pane blank - just exit if (backStackCount == 0 || isDualPane && backStackCount == 1) { @@ -207,7 +207,7 @@ private void displayFragment(SettingsFragment fragment, boolean addedFromRoot) { */ // if we're opening a submenu and there's already one open, wipe it from the back stack - FragmentManager fm = getFragmentManager(); + FragmentManager fm = getSupportFragmentManager(); if (addedFromRoot) { // when a root submenu is clicked, we need a new submenu backstack clearBackStack(fm); @@ -239,7 +239,7 @@ private void updateTitleBar() { if (actionBar == null) { return; } - FragmentManager fm = getFragmentManager(); + FragmentManager fm = getSupportFragmentManager(); // make sure fragment transactions are finished before we poke around in there fm.executePendingTransactions(); // if there's a submenu fragment present, get the title from that @@ -257,7 +257,7 @@ private void updateTitleBar() { public void onPreferenceChange(AwfulPreferences preferences, String key) { // update the summaries on any loaded fragments for (String tag : new String[]{ROOT_FRAGMENT_TAG, SUBMENU_FRAGMENT_TAG}) { - SettingsFragment fragment = (SettingsFragment) getFragmentManager().findFragmentByTag(tag); + SettingsFragment fragment = (SettingsFragment) getSupportFragmentManager().findFragmentByTag(tag); if (fragment != null) { fragment.setSummaries(); } @@ -300,34 +300,4 @@ private void exportSettings(Intent data) { boolean success = (settingsUri != null && AwfulPreferences.getInstance(this).exportSettings(settingsUri)); Toast.makeText(this, (success ? "Settings exported!" : "Failed to export"), Toast.LENGTH_SHORT).show(); } - - @Override - protected Dialog onCreateDialog(int dialogId) { - switch (dialogId) { - case DIALOG_ABOUT: - CharSequence app_version = getText(R.string.app_name); - try { - app_version = app_version + " " + - getPackageManager().getPackageInfo(getPackageName(), 0) - .versionName; - } catch (PackageManager.NameNotFoundException e) { - // rather unlikely, just show app_name without version - } - // Build the text for the About dialog - Resources res = getResources(); - String aboutText = getString(R.string.about_contributors_title) + "\n\n"; - aboutText += StringUtils.join(res.getStringArray(R.array.about_contributors_array), '\n'); - aboutText += "\n\n" + getString(R.string.about_libraries_title) + "\n\n"; - aboutText += StringUtils.join(res.getStringArray(R.array.about_libraries_array), '\n'); - - return new AlertDialog.Builder(this) - .setTitle(app_version) - .setMessage(aboutText) - .setNeutralButton(android.R.string.ok, (dialog, which) -> { - }) - .create(); - default: - return super.onCreateDialog(dialogId); - } - } } \ No newline at end of file diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/AccountSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/AccountSettings.java index 4519082e7..46decfbef 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/AccountSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/AccountSettings.java @@ -4,13 +4,14 @@ import android.app.ProgressDialog; import android.content.Intent; import android.net.Uri; -import android.preference.Preference; +import androidx.preference.Preference; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import android.text.TextUtils; import android.widget.Toast; import com.android.volley.VolleyError; +import com.ferg.awfulapp.AwfulActivity; import com.ferg.awfulapp.R; import com.ferg.awfulapp.network.NetworkUtils; import com.ferg.awfulapp.preferences.Keys; @@ -66,6 +67,7 @@ private class FeaturesListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(Preference preference) { final Dialog dialog = ProgressDialog.show(getActivity(), "Loading", "Fetching Account Features", true); + ((AwfulActivity)getActivity()).setPreferredFont(dialog.findViewById(android.R.id.title)); NetworkUtils.queueRequest(new FeatureRequest(getActivity()) .build(null, new AwfulRequest.AwfulResultCallback() { @Override diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ForumIndexSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ForumIndexSettings.java index 11778faae..927ccd72c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ForumIndexSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ForumIndexSettings.java @@ -1,6 +1,6 @@ package com.ferg.awfulapp.preferences.fragments; -import android.preference.Preference; +import androidx.preference.Preference; import androidx.annotation.NonNull; import androidx.annotation.UiThread; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/MiscSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/MiscSettings.java index 76500d9a4..4a5e95176 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/MiscSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/MiscSettings.java @@ -1,8 +1,8 @@ package com.ferg.awfulapp.preferences.fragments; import android.app.Dialog; -import android.preference.ListPreference; -import android.preference.Preference; +import androidx.preference.ListPreference; +import androidx.preference.Preference; import androidx.annotation.NonNull; import android.view.View; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/PostSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/PostSettings.java index 4042d0022..251b34838 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/PostSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/PostSettings.java @@ -3,7 +3,7 @@ import android.app.Dialog; import android.content.DialogInterface; import android.graphics.Typeface; -import android.preference.Preference; +import androidx.preference.Preference; import androidx.annotation.NonNull; import android.util.Log; import android.util.TypedValue; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/RootSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/RootSettings.java index ebb09426e..870e3f5e0 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/RootSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/RootSettings.java @@ -1,18 +1,24 @@ package com.ferg.awfulapp.preferences.fragments; import android.app.Activity; +import androidx.appcompat.app.AlertDialog; +import android.app.Dialog; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.preference.Preference; +import android.content.res.Resources; +import androidx.preference.Preference; import androidx.annotation.NonNull; +import com.ferg.awfulapp.AwfulActivity; import com.ferg.awfulapp.NavigationEvent; import com.ferg.awfulapp.R; import com.ferg.awfulapp.constants.Constants; import com.ferg.awfulapp.dialog.Changelog; import com.ferg.awfulapp.preferences.SettingsActivity; +import org.apache.commons.lang3.StringUtils; + import java.util.Calendar; import java.util.Locale; @@ -66,7 +72,32 @@ public String getTitle() { private class AboutListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(Preference preference) { - getActivity().showDialog(SettingsActivity.DIALOG_ABOUT); + CharSequence app_version = getText(R.string.app_name); + try { + app_version = app_version + " " + + getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0) + .versionName; + } catch (PackageManager.NameNotFoundException e) { + // rather unlikely, just show app_name without version + } + // Build the text for the About dialog + Resources res = getResources(); + String aboutText = getString(R.string.about_contributors_title) + "\n\n"; + aboutText += StringUtils.join(res.getStringArray(R.array.about_contributors_array), '\n'); + aboutText += "\n\n" + getString(R.string.about_libraries_title) + "\n\n"; + aboutText += StringUtils.join(res.getStringArray(R.array.about_libraries_array), '\n'); + Dialog about = new AlertDialog.Builder(getActivity()) + .setTitle(app_version) + .setMessage(aboutText) + .setNeutralButton(android.R.string.ok, (dialog, which) -> { + }) + .show(); + + AwfulActivity activity = (AwfulActivity) getActivity(); + activity.setPreferredFont(about.findViewById(androidx.appcompat.R.id.alertTitle)); + activity.setPreferredFont(about.findViewById(android.R.id.message)); + activity.setPreferredFont(about.findViewById(android.R.id.button3)); + return true; } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/SettingsFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/SettingsFragment.java index cc204e820..e984ac074 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/SettingsFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/SettingsFragment.java @@ -1,21 +1,32 @@ package com.ferg.awfulapp.preferences.fragments; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.res.Resources; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceFragment; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.collection.ArrayMap; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceGroupAdapter; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; +import androidx.recyclerview.widget.RecyclerView; + import android.util.Log; import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; import android.widget.ListView; +import com.ferg.awfulapp.AwfulActivity; import com.ferg.awfulapp.NavigationEvent; import com.ferg.awfulapp.NavigationEventHandler; import com.ferg.awfulapp.R; @@ -41,7 +52,7 @@ * {@link SettingsActivity#PREFERENCE_XML_FILES} so it can be * automatically checked for defaults!

      */ -public abstract class SettingsFragment extends PreferenceFragment implements NavigationEventHandler { +public abstract class SettingsFragment extends PreferenceFragmentCompat implements NavigationEventHandler { public static final String TAG = "SettingsFragment"; private volatile boolean isInflated; @@ -113,6 +124,7 @@ public abstract class SettingsFragment extends PreferenceFragment implements Nav @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mPrefs = ((SettingsActivity) getActivity()).prefs; try { @@ -132,14 +144,43 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // for some reason, if you theme android:listDivider it won't show up in the preference list // so doing this directly seems to be the only way to theme it? Can't just get() it either - ListView listview = (ListView) getView().findViewById(android.R.id.list); Drawable divider = getResources().getDrawable(R.drawable.list_divider); TypedValue colour = new TypedValue(); getActivity().getTheme().resolveAttribute(android.R.attr.listDivider, colour, true); divider.setColorFilter(colour.data, PorterDuff.Mode.SRC_IN); - listview.setDivider(divider); + setDivider(divider); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View result = super.onCreateView(inflater, container, savedInstanceState); + + ((AwfulActivity) getActivity()).setPreferredFont(result); + + return result; + } + + @SuppressLint("RestrictedApi") + class AwfulPreferenceAdapter extends PreferenceGroupAdapter { + String TAG = "MyAdapter"; + public AwfulPreferenceAdapter(PreferenceGroup preferenceGroup) { + super(preferenceGroup); + } + + @NonNull + @Override + public PreferenceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + PreferenceViewHolder holder = super.onCreateViewHolder(parent, viewType); + ((AwfulActivity)getActivity()).setPreferredFont(holder.itemView); + return holder; + } } + @NonNull + @Override + protected RecyclerView.Adapter onCreateAdapter(@NonNull PreferenceScreen preferenceScreen) { + return new AwfulPreferenceAdapter(preferenceScreen); + } // // Navigation @@ -305,4 +346,8 @@ public boolean onPreferenceClick(Preference preference) { } } + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + return; + } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ThemeSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ThemeSettings.java index 4df60014e..bfcc03794 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ThemeSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ThemeSettings.java @@ -5,8 +5,8 @@ import android.content.ComponentName; import android.content.pm.PackageManager; import android.os.Build; -import android.preference.ListPreference; -import android.preference.Preference; +import androidx.preference.ListPreference; +import androidx.preference.Preference; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/reply/MessageComposer.java b/Awful.apk/src/main/java/com/ferg/awfulapp/reply/MessageComposer.java index ccac3db05..8f2c4ab5b 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/reply/MessageComposer.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/reply/MessageComposer.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import com.ferg.awfulapp.AwfulActivity; +import com.ferg.awfulapp.FontManager; import com.ferg.awfulapp.NavigationEvent; import com.ferg.awfulapp.preferences.AwfulPreferences; import com.google.android.material.bottomsheet.BottomSheetBehavior; @@ -67,6 +68,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa View result = inflater.inflate(R.layout.message_composer, container, true); messageBox = result.findViewById(R.id.message_edit_text); addBbCodeToSelectionMenu(messageBox); + ((AwfulActivity) getActivity()).setPreferredFont(result); return result; } @@ -88,6 +90,10 @@ public void onDestroy() { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.message_composer, menu); + FontManager fm = FontManager.getInstance(); + for (int i = 0; i < menu.size(); i++) { + fm.setMenuItemFont(menu.getItem(i)); + } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFilter.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFilter.kt index d47b1b897..a9057ee50 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFilter.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFilter.kt @@ -2,9 +2,9 @@ package com.ferg.awfulapp.search import android.os.Parcel import android.os.Parcelable -import androidx.appcompat.app.AlertDialog import android.view.View import android.widget.EditText +import androidx.appcompat.app.AlertDialog import com.ferg.awfulapp.R /** @@ -45,13 +45,16 @@ class SearchFilter(val type: FilterType, val param: String) : Parcelable { val layout = layoutInflater.inflate(R.layout.insert_text_dialog, null) val textField = layout.findViewById(R.id.text_field) as EditText textField.hint = description ?: label - AlertDialog.Builder(this) + val add = AlertDialog.Builder(this) .setTitle("Add search filter") .setView(layout) .setPositiveButton("Add filter", { _, _ -> searchFragment.addFilter(SearchFilter(this@FilterType, textField.text.toString())) }) .show() + searchFragment.awfulActivity?.setPreferredFont(add.findViewById(androidx.appcompat.R.id.alertTitle)) + searchFragment.awfulActivity?.setPreferredFont(add.findViewById(android.R.id.button1)) + searchFragment.awfulActivity?.setPreferredFont(layout) } } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchForumsFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchForumsFragment.java index 1c99dc73a..1cd58040a 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchForumsFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchForumsFragment.java @@ -83,6 +83,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, public SearchHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.search_forum_item, parent, false); + + getAwfulActivity().setPreferredFont(view); return new SearchHolder(view); } @@ -120,6 +122,7 @@ public void onBindViewHolder(SearchHolder holder, final int position) { self.notifyDataSetChanged(); } }); + getAwfulActivity().setPreferredFont(holder.itemView); } @Override diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt index 1c59b9b4e..08fd4ccf6 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt @@ -33,16 +33,22 @@ package com.ferg.awfulapp.search import android.app.ProgressDialog import android.os.Bundle -import com.google.android.material.snackbar.Snackbar -import androidx.fragment.app.DialogFragment -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import android.text.Html -import android.view.* +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.EditText import android.widget.TextView +import androidx.core.view.forEach +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.android.volley.VolleyError import com.ferg.awfulapp.AwfulFragment +import com.ferg.awfulapp.FontManager import com.ferg.awfulapp.NavigationEvent import com.ferg.awfulapp.NavigationEvent.Companion.parse import com.ferg.awfulapp.R @@ -57,10 +63,10 @@ import com.ferg.awfulapp.thread.AwfulSearch import com.ferg.awfulapp.thread.AwfulSearchResult import com.ferg.awfulapp.thread.AwfulURL import com.ferg.awfulapp.widget.SwipyRefreshLayout +import com.google.android.material.snackbar.Snackbar import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection import org.apache.commons.lang3.ArrayUtils import timber.log.Timber -import java.util.* class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout.OnRefreshListener { @@ -103,7 +109,10 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl override fun onCreateView(aInflater: LayoutInflater, aContainer: ViewGroup?, aSavedState: Bundle?): View? { super.onCreateView(aInflater, aContainer, aSavedState) Timber.v("onCreateView") - return inflateView(R.layout.search, aContainer, aInflater) + val result = inflateView(R.layout.search, aContainer, aInflater) + + awfulActivity?.setPreferredFont(result) + return result } @@ -171,8 +180,10 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater!!.inflate(R.menu.search, menu) + val fm = FontManager.getInstance() val filterMenu = menu?.findItem(R.id.search_terms)!!.subMenu SearchFilter.FilterType.values().forEach { filterMenu?.add(it.label) } + filterMenu?.forEach { fm.setMenuItemFont(it) } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -195,6 +206,15 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl return true } + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + + val fm = FontManager.getInstance() + for (i in 0 until menu.size()) { + fm.setMenuItemFont(menu.getItem(i)) + } + } + /** * Add a filter to the current set. @@ -266,6 +286,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl self.setOnClickListener { AwfulURL.parse(Constants.BASE_URL + threadLink).let(NavigationEvent::Url).let(::navigate) } + awfulActivity?.setPreferredFont(holder.itemView) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java index 75f1b66e3..68493f352 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java @@ -118,7 +118,7 @@ public static String getContainerHtml(AwfulPreferences aPrefs, @Nullable Integer if (!aPrefs.preferredFont.contains("default")) { - buffer.append("\n"); + buffer.append("\n"); } for (String scriptName : JS_FILES) { buffer.append("