diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eef46aad..cf3aaef0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,11 +17,18 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> - - - - - + + + + + \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java index 85cb4a9c..f8d87263 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java @@ -19,6 +19,7 @@ import androidx.preference.PreferenceManager; import androidx.viewpager.widget.ViewPager; import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.activities.LoginActivity; +import gr.thmmy.mthmmy.activities.TestActivity; import gr.thmmy.mthmmy.activities.board.BoardActivity; import gr.thmmy.mthmmy.activities.downloads.DownloadsActivity; import gr.thmmy.mthmmy.activities.main.forum.ForumFragment; @@ -132,6 +133,8 @@ public class MainActivity extends BaseActivity implements RecentFragment.RecentF , Toast.LENGTH_SHORT).show(); } mBackPressed = System.currentTimeMillis(); + + startActivity(new Intent(this, TestActivity.class)); } @Override diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java index 5a1fdc5a..f5bcb761 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java @@ -65,6 +65,7 @@ import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.TopicItem; import gr.thmmy.mthmmy.utils.CircleTransform; +import gr.thmmy.mthmmy.utils.parsing.ThmmyParser; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import timber.log.Timber; @@ -165,9 +166,34 @@ class TopicAdapter extends RecyclerView.Adapter { Poll poll = (Poll) topicItems.get(position); Poll.Entry[] entries = poll.getEntries(); PollViewHolder holder = (PollViewHolder) currentHolder; + + boolean pollSupported = true; + for (Poll.Entry entry : entries) { + if (ThmmyParser.containsHtml(entry.getEntryName())) pollSupported = false; + break; + } + if (ThmmyParser.containsHtml(poll.getQuestion())) pollSupported = false; + if (!pollSupported) { + holder.optionsLayout.setVisibility(View.GONE); + holder.voteChart.setVisibility(View.GONE); + holder.removeVotesButton.setVisibility(View.GONE); + holder.showPollResultsButton.setVisibility(View.GONE); + holder.hidePollResultsButton.setVisibility(View.GONE); + // use the submit vote button to open poll on browser + holder.submitButton.setText("Open in browser"); + holder.submitButton.setOnClickListener(v -> { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(viewModel.getTopicUrl())); + context.startActivity(browserIntent); + }); + holder.submitButton.setVisibility(View.VISIBLE); + // put a warning instead of a question + holder.question.setText("This topic contains a poll that is not supported in mthmmy"); + return; + } + holder.question.setText(poll.getQuestion()); holder.optionsLayout.removeAllViews(); - holder.errorTooManySelected.setVisibility(View.GONE); + holder.errorTextview.setVisibility(View.GONE); if (poll.getAvailableVoteCount() > 1) { for (Poll.Entry entry : entries) { LinearLayout container = new LinearLayout(context); @@ -182,6 +208,7 @@ class TopicAdapter extends RecyclerView.Adapter { //noinspection deprecation label.setText(Html.fromHtml(entry.getEntryName())); } + label.setText(ThmmyParser.html2span(context, entry.getEntryName())); checkBox.setTextColor(context.getResources().getColor(R.color.primary_text)); container.addView(checkBox); container.addView(label); @@ -201,6 +228,7 @@ class TopicAdapter extends RecyclerView.Adapter { //noinspection deprecation radioButton.setText(Html.fromHtml(entries[i].getEntryName())); } + radioButton.setText(ThmmyParser.html2span(context, entries[i].getEntryName())); radioButton.setTextColor(context.getResources().getColor(R.color.primary_text)); radioGroup.addView(radioButton); } @@ -260,10 +288,10 @@ class TopicAdapter extends RecyclerView.Adapter { if (poll.getPollFormUrl() != null) { holder.submitButton.setOnClickListener(v -> { if (!viewModel.submitVote(holder.optionsLayout)) { - holder.errorTooManySelected.setText(context.getResources() + holder.errorTextview.setText(context.getResources() .getQuantityString(R.plurals.error_too_many_checked, poll.getAvailableVoteCount(), poll.getAvailableVoteCount())); - holder.errorTooManySelected.setVisibility(View.VISIBLE); + holder.errorTextview.setVisibility(View.VISIBLE); } }); holder.submitButton.setVisibility(View.VISIBLE); @@ -765,7 +793,7 @@ class TopicAdapter extends RecyclerView.Adapter { } static class PollViewHolder extends RecyclerView.ViewHolder { - final TextView question, errorTooManySelected; + final TextView question, errorTextview; final LinearLayout optionsLayout; final AppCompatButton submitButton; final AppCompatButton removeVotesButton, showPollResultsButton, hidePollResultsButton; @@ -780,7 +808,7 @@ class TopicAdapter extends RecyclerView.Adapter { removeVotesButton = itemView.findViewById(R.id.remove_vote_button); showPollResultsButton = itemView.findViewById(R.id.show_poll_results_button); hidePollResultsButton = itemView.findViewById(R.id.show_poll_options_button); - errorTooManySelected = itemView.findViewById(R.id.error_too_many_checked); + errorTextview = itemView.findViewById(R.id.error_too_many_checked); voteChart = itemView.findViewById(R.id.vote_chart); } } diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java index 5a411e12..4ebbba5a 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java @@ -156,9 +156,9 @@ public class TopicParser { ArrayList parsedPostsList = new ArrayList<>(); -// Poll poll = findPoll(topic); -// if (poll != null) -// parsedPostsList.add(poll); + Poll poll = findPoll(topic); + if (poll != null) + parsedPostsList.add(poll); Elements postRows; diff --git a/app/src/main/java/gr/thmmy/mthmmy/model/BBTag.java b/app/src/main/java/gr/thmmy/mthmmy/model/BBTag.java new file mode 100644 index 00000000..77504761 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/model/BBTag.java @@ -0,0 +1,53 @@ +package gr.thmmy.mthmmy.model; + +import androidx.annotation.NonNull; + +public class BBTag { + private int start, end; + private String name, attribute; + + public BBTag(int start, String name) { + this.start = start; + this.name = name; + } + + public BBTag(int start, String name, String attribute) { + this.start = start; + this.name = name; + this.attribute = attribute; + } + + @NonNull + @Override + public String toString() { + return "start:" + start + ",end:" + end + ",name:" + name; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getEnd() { + return end; + } + + public void setEnd(int end) { + this.end = end; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAttribute() { + return attribute; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/model/HtmlTag.java b/app/src/main/java/gr/thmmy/mthmmy/model/HtmlTag.java new file mode 100644 index 00000000..b7e13021 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/model/HtmlTag.java @@ -0,0 +1,58 @@ +package gr.thmmy.mthmmy.model; + +import androidx.annotation.NonNull; + +public class HtmlTag { + private int start, end; + private String name, attributeKey, attributeValue; + + public HtmlTag(int start, String name) { + this.start = start; + this.name = name; + } + + public HtmlTag(int start, String name, String attributeKey, String attributeValue) { + this.start = start; + this.name = name; + this.attributeKey = attributeKey; + this.attributeValue = attributeValue; + } + + @NonNull + @Override + public String toString() { + return "start:" + start + ",end:" + end + ",name:" + name; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getEnd() { + return end; + } + + public void setEnd(int end) { + this.end = end; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAttributeKey() { + return attributeKey; + } + + public String getAttributeValue() { + return attributeValue; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java b/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java index 2713a2a8..8ca4ede6 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java @@ -1,6 +1,7 @@ package gr.thmmy.mthmmy.utils; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; @@ -12,6 +13,7 @@ import android.text.style.URLSpan; import android.view.View; import gr.thmmy.mthmmy.activities.board.BoardActivity; +import gr.thmmy.mthmmy.activities.main.MainActivity; import gr.thmmy.mthmmy.activities.profile.ProfileActivity; import gr.thmmy.mthmmy.model.ThmmyPage; @@ -41,7 +43,7 @@ public class HTMLUtils { return strBuilder; } - private static void makeLinkClickable(Activity activity, SpannableStringBuilder strBuilder, final URLSpan span) { + public static void makeLinkClickable(Context context, SpannableStringBuilder strBuilder, final URLSpan span) { int start = strBuilder.getSpanStart(span); int end = strBuilder.getSpanEnd(span); int flags = strBuilder.getSpanFlags(span); @@ -50,24 +52,27 @@ public class HTMLUtils { public void onClick(View view) { ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(Uri.parse(span.getURL())); if (target.is(ThmmyPage.PageCategory.BOARD)) { - Intent intent = new Intent(activity.getApplicationContext(), BoardActivity.class); + Intent intent = new Intent(context, BoardActivity.class); Bundle extras = new Bundle(); extras.putString(BUNDLE_BOARD_URL, span.getURL()); extras.putString(BUNDLE_BOARD_TITLE, ""); intent.putExtras(extras); intent.setFlags(FLAG_ACTIVITY_NEW_TASK); - activity.getApplicationContext().startActivity(intent); + context.startActivity(intent); } else if (target.is(ThmmyPage.PageCategory.PROFILE)) { - Intent intent = new Intent(activity.getApplicationContext(), ProfileActivity.class); + Intent intent = new Intent(context, ProfileActivity.class); Bundle extras = new Bundle(); extras.putString(BUNDLE_PROFILE_URL, span.getURL()); extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, ""); extras.putString(BUNDLE_PROFILE_USERNAME, ""); intent.putExtras(extras); intent.setFlags(FLAG_ACTIVITY_NEW_TASK); - activity.getApplicationContext().startActivity(intent); - } else if (target.is(ThmmyPage.PageCategory.INDEX)) - activity.finish(); + context.startActivity(intent); + } else if (target.is(ThmmyPage.PageCategory.INDEX)) { + Intent intent = new Intent(context, MainActivity.class); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } } }; strBuilder.setSpan(clickable, start, end, flags); diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyParser.java b/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyParser.java new file mode 100644 index 00000000..f900290f --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyParser.java @@ -0,0 +1,221 @@ +package gr.thmmy.mthmmy.utils.parsing; + +import android.content.Context; +import android.graphics.Typeface; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; + +import java.nio.charset.UnsupportedCharsetException; +import java.util.LinkedList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import gr.thmmy.mthmmy.model.BBTag; +import gr.thmmy.mthmmy.model.HtmlTag; +import gr.thmmy.mthmmy.utils.HTMLUtils; + +public class ThmmyParser { + private static final String[] ALL_BB_TAGS = {"b", "i", "u", "s", "glow", "shadow", "move", "pre", "lefter", + "center", "right", "hr", "size", "font", "color", "youtube", "flash", "img", "url" + , "email", "ftp", "table", "tr", "td", "sup", "sub", "tt", "code", "quote", "tex", "list", "li"}; + private static final String[] ALL_HTML_TAGS = {"b", "br", "span", "i", "div", "del", "marquee", "pre", + "hr", "embed", "noembed", "a", "img", "table", "tr", "td", "sup", "sub", "tt", "pre", "ul", "li"}; + + public static SpannableStringBuilder bb2span(String bb) { + SpannableStringBuilder builder = new SpannableStringBuilder(bb); + // store the original indices of the string + LinkedList stringIndices = new LinkedList<>(); + for (int i = 0; i < builder.length(); i++) { + stringIndices.add(i); + } + + BBTag[] tags = getBBTags(bb); + for (BBTag tag : tags) { + int start = stringIndices.indexOf(tag.getStart()); + int end = stringIndices.indexOf(tag.getEnd()); + int startTagLength = tag.getName().length() + 2; + int endTagLength = tag.getName().length() + 3; + switch (tag.getName()) { + case "b": + builder.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case "i": + builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case "u": + builder.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case "s": + builder.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + default: + throw new UnsupportedCharsetException("Tag not supported"); + } + //remove starting and ending tag and and do the same changes in the list + builder.delete(start, start + startTagLength); + for (int i = start; i < start + startTagLength; i++) { + stringIndices.remove(start); + } + builder.delete(end - startTagLength, end - startTagLength + endTagLength); + for (int i = end - startTagLength; i < end - startTagLength + endTagLength; i++) { + stringIndices.remove(end - startTagLength); + } + } + return builder; + } + + public static SpannableStringBuilder html2span(Context context, String html) { + SpannableStringBuilder builder = new SpannableStringBuilder(html); + // store the original indices of the string + LinkedList stringIndices = new LinkedList<>(); + for (int i = 0; i < builder.length(); i++) { + stringIndices.add(i); + } + + HtmlTag[] tags = getHtmlTags(html); + for (HtmlTag tag : tags) { + int start = stringIndices.indexOf(tag.getStart()); + int end = stringIndices.indexOf(tag.getEnd()); + int startTagLength = tag.getName().length() + 2; + if (tag.getAttributeKey() != null) { + startTagLength += tag.getAttributeKey().length() + tag.getAttributeValue().length() + 4; + } + int endTagLength = tag.getName().length() + 3; + + if (isHtmlTagSupported(tag.getName(), tag.getAttributeKey(), tag.getAttributeValue())) { + switch (tag.getName()) { + case "b": + builder.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case "i": + builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case "span": + if (tag.getAttributeKey().equals("style") && tag.getAttributeValue().equals("text-decoration: underline;")) { + builder.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + break; + case "del": + builder.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case "a": + URLSpan urlSpan = new URLSpan(tag.getAttributeValue()); + builder.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + HTMLUtils.makeLinkClickable(context, builder, urlSpan); + break; + default: + throw new UnsupportedCharsetException("Tag not supported"); + } + } + + //remove starting and ending tag and and do the same changes in the list + builder.delete(start, start + startTagLength); + for (int i = start; i < start + startTagLength; i++) { + stringIndices.remove(start); + } + builder.delete(end - startTagLength, end - startTagLength + endTagLength); + for (int i = end - startTagLength; i < end - startTagLength + endTagLength; i++) { + stringIndices.remove(end - startTagLength); + } + } + return builder; + } + + public static BBTag[] getBBTags(String bb) { + Pattern bbtagPattern = Pattern.compile("\\[(.+?)\\]"); + + LinkedList tags = new LinkedList<>(); + Matcher bbMatcher = bbtagPattern.matcher(bb); + while (bbMatcher.find()) { + String startTag = bbMatcher.group(1); + int separatorIndex = startTag.indexOf('='); + String name, attribute = null; + if (separatorIndex > 0) { + attribute = startTag.substring(separatorIndex); + name = startTag.substring(0, separatorIndex); + } else + name = startTag; + + if (name.startsWith("/")) { + //closing tag + name = name.substring(1); + for (int i = tags.size() - 1; i >= 0; i--) { + if (tags.get(i).getName().equals(name)) { + tags.get(i).setEnd(bbMatcher.start()); + break; + } + } + continue; + } + if (isBBTagSupported(name)) + tags.add(new BBTag(bbMatcher.start(), name, attribute)); + } + // remove parsed tags with no end tag + for (BBTag bbTag : tags) + if (bbTag.getEnd() == 0) + tags.remove(bbTag); + return tags.toArray(new BBTag[0]); + } + + public static HtmlTag[] getHtmlTags(String html) { + Pattern htmlPattern = Pattern.compile("<(.+?)>"); + + LinkedList tags = new LinkedList<>(); + Matcher htmlMatcher = htmlPattern.matcher(html); + while (htmlMatcher.find()) { + String startTag = htmlMatcher.group(1); + int separatorIndex = startTag.indexOf(' '); + String name, attribute = null, attributeValue = null; + if (separatorIndex > 0) { + String fullAttribute = startTag.substring(separatorIndex); + int equalsIndex = fullAttribute.indexOf('='); + attribute = fullAttribute.substring(1, equalsIndex); + attributeValue = fullAttribute.substring(equalsIndex + 2, fullAttribute.length() - 1); + name = startTag.substring(0, separatorIndex); + } else + name = startTag; + + if (name.startsWith("/")) { + //closing tag + name = name.substring(1); + for (int i = tags.size() - 1; i >= 0; i--) { + if (tags.get(i).getName().equals(name)) { + tags.get(i).setEnd(htmlMatcher.start()); + break; + } + } + continue; + } + if (isHtmlTag(name)) + tags.add(new HtmlTag(htmlMatcher.start(), name, attribute, attributeValue)); + } + // remove parsed tags with no end tag + for (HtmlTag htmlTag : tags) + if (htmlTag.getEnd() == 0) + tags.remove(htmlTag); + return tags.toArray(new HtmlTag[0]); + } + + private static boolean isHtmlTagSupported(String name, String attribute, String attributeValue) { + return name.equals("b") || name.equals("i") || name.equals("span") || name.equals("del") || name.equals("a"); + } + + public static boolean isBBTagSupported(String name) { + return name.equals("b") || name.equals("i") || name.equals("u") || name.equals("s"); + } + + public static boolean isHtmlTag(String tagName) { + for (String tag : ALL_HTML_TAGS) + if (TextUtils.equals(tag, tagName)) return true; + return false; + } + + public static boolean containsHtml(String s) { + return getHtmlTags(s).length > 0; + } +}