Implemented simple Kodi markup code applier for the Now Playing fragment (#471)
* Implemented simple Kodi markup code applier for the Now Playing fragment * Moved to UIUtils; added early exit; now handles nesting of the same type * Added handlers for LOWERCASE, CAPITALIZE, LIGHT and COLOR - LOWERCASE and CAPITALIZE work, LIGHT and COLOR are just stripped out - inlined the TextAppearanceSpan new's. Turns out they can't be reused. * updated javadoc * fixed crash when capitalizing an empty word
This commit is contained in:
parent
41a433c985
commit
45ecfc2e25
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -325,5 +325,11 @@
|
|||
<item name="android:textSize">@dimen/text_size_xlarge</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Bold">
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
<style name="TextAppearance.Italic">
|
||||
<item name="android:textStyle">italic</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
|
|
Loading…
Reference in New Issue