diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/NowPlayingFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/NowPlayingFragment.java index f771930..4beca18 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/NowPlayingFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/NowPlayingFragment.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.os.Handler; import android.support.v4.app.Fragment; import android.text.TextUtils; +import android.text.util.Linkify; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.MenuItem; @@ -776,7 +777,7 @@ public class NowPlayingFragment extends Fragment if (!TextUtils.isEmpty(descriptionPlot)) { mediaDescription.setVisibility(View.VISIBLE); - mediaDescription.setText(descriptionPlot); + mediaDescription.setText(UIUtils.applyMarkup(getContext(), descriptionPlot)); } else { mediaDescription.setVisibility(View.GONE); } diff --git a/app/src/main/java/org/xbmc/kore/utils/UIUtils.java b/app/src/main/java/org/xbmc/kore/utils/UIUtils.java index 0d83026..b3808b6 100644 --- a/app/src/main/java/org/xbmc/kore/utils/UIUtils.java +++ b/app/src/main/java/org/xbmc/kore/utils/UIUtils.java @@ -30,7 +30,9 @@ import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AlertDialog; +import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; @@ -54,6 +56,8 @@ import org.xbmc.kore.ui.widgets.RepeatModeButton; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * General UI Utils @@ -607,4 +611,181 @@ public class UIUtils { button.setMode(RepeatModeButton.MODE.ALL); } } + + /** + * Replaces some BBCode-ish tagged text with styled spans. + *

+ * Recognizes and styles CR, B, I, UPPERCASE, LOWERCASE and CAPITALIZE; recognizes + * and strips out LIGHT and COLOR. This is very strict/dumb, it only recognizes + * uppercase tags with no spaces around them. + * + * @param context Activity context needed to resolve the style resources + * @param src The text to style + * @return a styled CharSequence that can be passed to a {@link TextView#setText(CharSequence)} + * or derivatives. + */ + public static SpannableStringBuilder applyMarkup(Context context, String src) { + SpannableStringBuilder sb = new SpannableStringBuilder(); + int start = src.indexOf('['); + if (start == -1) { + sb.append(src); + return sb; + } + if (start > 0) { + sb.append(src, 0, start); + } + Nestable upper = new Nestable(); + Nestable lower = new Nestable(); + Nestable title = new Nestable(); + Nestable bold = new Nestable(); + Nestable italic = new Nestable(); + Nestable light = new Nestable(); + Nestable color = new Nestable(); + Pattern colorTag = Pattern.compile("^\\[COLOR [^\\]]+\\]"); + for (int i = start, length = src.length(); i < length;) { + String s = src.substring(i); + int nextTag = s.indexOf('['); + if (nextTag == -1) { + sb.append(s); + break; + } + if (nextTag > 0) { + sb.append(s, 0, nextTag); + i += nextTag; + } else if (s.startsWith("[CR]")) { + sb.append('\n'); + i += 4; + } else if (s.startsWith("[UPPERCASE]")) { + if (upper.start()) { + upper.index = sb.length(); + } + i += 11; + } else if (s.startsWith("[/UPPERCASE]")) { + if (upper.end()) { + String sub = sb.subSequence(upper.index, sb.length()).toString(); + sb.replace(upper.index, sb.length(), sub.toUpperCase()); + } else if (upper.imbalanced()) { + sb.append("[/UPPERCASE]"); + } + i += 12; + } else if (s.startsWith("[B]")) { + if (bold.start()) { + bold.index = sb.length(); + } + i += 3; + } else if (s.startsWith("[/B]")) { + if (bold.end()) { + sb.setSpan(new TextAppearanceSpan(context, R.style.TextAppearance_Bold), + bold.index, sb.length(), 0); + } else if (bold.imbalanced()) { + sb.append("[/B]"); + } + i += 4; + } else if (s.startsWith("[I]")) { + if (italic.start()) { + italic.index = sb.length(); + } + i += 3; + } else if (s.startsWith("[/I]")) { + if (italic.end()) { + sb.setSpan(new TextAppearanceSpan(context, R.style.TextAppearance_Italic), + italic.index, sb.length(), 0); + } else if (italic.imbalanced()) { + sb.append("[/I]"); + } + i += 4; + } else if (s.startsWith("[LOWERCASE]")) { + if (lower.start()) { + lower.index = sb.length(); + } + i += 11; + } else if (s.startsWith("[/LOWERCASE]")) { + if (lower.end()) { + String sub = sb.subSequence(lower.index, sb.length()).toString(); + sb.replace(lower.index, sb.length(), sub.toLowerCase()); + } else if (lower.imbalanced()) { + sb.append("[/LOWERCASE]"); + } + i += 12; + } else if (s.startsWith("[CAPITALIZE]")) { + if (title.start()) { + title.index = sb.length(); + } + i += 12; + } else if (s.startsWith("[/CAPITALIZE]")) { + if (title.end()) { + String sub = sb.subSequence(title.index, sb.length()).toString(); + sb.replace(title.index, sb.length(), toTitleCase(sub)); + } else if (title.imbalanced()) { + sb.append("[/CAPITALIZE]"); + } + i += 13; + } else if (s.startsWith("[LIGHT]")) { + light.start(); + i += 7; + } else if (s.startsWith("[/LIGHT]")) { + light.end(); + if (light.imbalanced()) { + sb.append("[/LIGHT]"); + } + i += 8; + } else if (s.startsWith("[/COLOR]")) { + color.end(); + if (color.imbalanced()) { + sb.append("[/COLOR]"); + } + i += 8; + } else { + Matcher m = colorTag.matcher(s); + if (m.find()) { + color.start(); + i += m.end(); + } else { + sb.append('['); + i += 1; + } + } + } + return sb; + } + + private static class Nestable { + int index = 0; + int level = 0; + + /** + * @return true if we just opened the first tag + */ + boolean start() { + return level++ == 0; + } + + /** + * @return true if we just closed the last open tag + */ + boolean end() { + return --level == 0; + } + + /** + * @return true if we found a close tag when there are no open tags + */ + boolean imbalanced() { + if (level < 0) { + level = 0; + return true; + } + return false; + } + } + + private static String toTitleCase(String text) { + StringBuilder sb = new StringBuilder(); + for (String word : text.toLowerCase().split("\\b")) { + if (word.isEmpty()) continue; + sb.append(Character.toUpperCase(word.charAt(0))); + sb.append(word, 1, word.length()); + } + return sb.toString(); + } } diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 416259e..c61338a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -325,5 +325,11 @@ @dimen/text_size_xlarge + +