diff --git a/app/build.gradle b/app/build.gradle
index ad3da0b2b..b813cf881 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -73,4 +73,5 @@ dependencies {
compile 'com.android.support:customtabs:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'de.greenrobot:greendao:2.0.0'
+ compile 'org.ccil.cowan.tagsoup:tagsoup:1.2.1'
}
diff --git a/app/src/androidTest/java/com/hippo/ehviewer/DumpTest.java b/app/src/androidTest/java/com/hippo/ehviewer/DumpTest.java
new file mode 100644
index 000000000..3aff6166e
--- /dev/null
+++ b/app/src/androidTest/java/com/hippo/ehviewer/DumpTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 Hippo Seven
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer;
+
+import junit.framework.TestCase;
+
+public class DumpTest extends TestCase {
+
+
+ public void testDump() {
+ String str = "M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55";
+ System.out.println(str.replaceAll("\\d+(\\.\\d+)?", "12"));
+ }
+
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/EhApplication.java b/app/src/main/java/com/hippo/ehviewer/EhApplication.java
index d09929bdc..61a0f8c9b 100644
--- a/app/src/main/java/com/hippo/ehviewer/EhApplication.java
+++ b/app/src/main/java/com/hippo/ehviewer/EhApplication.java
@@ -17,29 +17,38 @@
package com.hippo.ehviewer;
import android.app.ActivityManager;
-import android.app.Application;
import android.content.Context;
import android.support.annotation.NonNull;
+import android.util.SparseArray;
+import com.hippo.beerbelly.LruMap;
import com.hippo.conaco.Conaco;
import com.hippo.drawable.ImageWrapper;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhCookieStore;
+import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.network.StatusCodeException;
import com.hippo.okhttp.CookieDB;
+import com.hippo.scene.SceneApplication;
+import com.hippo.utils.ReadableTime;
+import com.hippo.yorozuya.IntIdGenerator;
import java.io.File;
+import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
-public class EhApplication extends Application {
+public class EhApplication extends SceneApplication {
+ private IntIdGenerator mIdGenerator = new IntIdGenerator();
+ private SparseArray
\n");
+ }
+
+ /* Returns true if the caller should close and reopen the paragraph. */
+ private static boolean withinParagraph(StringBuilder out, Spanned text,
+ int start, int end, int nl,
+ boolean last) {
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = text.nextSpanTransition(i, end, CharacterStyle.class);
+ CharacterStyle[] style = text.getSpans(i, next,
+ CharacterStyle.class);
+
+ for (int j = 0; j < style.length; j++) {
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("");
+ }
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if ("monospace".equals(s)) {
+ out.append("");
+ }
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof UnderlineSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof URLSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof ImageSpan) {
+ out.append("
");
+
+ // Don't output the dummy character underlying the image.
+ i = next;
+ }
+ if (style[j] instanceof AbsoluteSizeSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof ForegroundColorSpan) {
+ out.append("");
+ }
+ }
+
+ withinStyle(out, text, i, next);
+
+ for (int j = style.length - 1; j >= 0; j--) {
+ if (style[j] instanceof ForegroundColorSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof AbsoluteSizeSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof URLSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof UnderlineSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if (s.equals("monospace")) {
+ out.append("");
+ }
+ }
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("");
+ }
+ }
+ }
+ }
+
+ if (nl == 1) {
+ out.append("
\n");
+ return false;
+ } else {
+ for (int i = 2; i < nl; i++) {
+ out.append("
");
+ }
+ return !last;
+ }
+ }
+
+ private static void withinStyle(StringBuilder out, CharSequence text,
+ int start, int end) {
+ for (int i = start; i < end; i++) {
+ char c = text.charAt(i);
+
+ if (c == '<') {
+ out.append("<");
+ } else if (c == '>') {
+ out.append(">");
+ } else if (c == '&') {
+ out.append("&");
+ } else if (c >= 0xD800 && c <= 0xDFFF) {
+ if (c < 0xDC00 && i + 1 < end) {
+ char d = text.charAt(i + 1);
+ if (d >= 0xDC00 && d <= 0xDFFF) {
+ i++;
+ int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
+ out.append("").append(codepoint).append(";");
+ }
+ }
+ } else if (c > 0x7E || c < ' ') {
+ out.append("").append((int) c).append(";");
+ } else if (c == ' ') {
+ while (i + 1 < end && text.charAt(i + 1) == ' ') {
+ out.append(" ");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+ }
+}
+
+class HtmlToSpannedConverter implements ContentHandler {
+
+ private static final float[] HEADER_SIZES = {
+ 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
+ };
+
+ private String mSource;
+ private XMLReader mReader;
+ private SpannableStringBuilder mSpannableStringBuilder;
+ private Html.ImageGetter mImageGetter;
+ private Html.TagHandler mTagHandler;
+
+ public HtmlToSpannedConverter(
+ String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
+ Parser parser) {
+ mSource = source;
+ mSpannableStringBuilder = new SpannableStringBuilder();
+ mImageGetter = imageGetter;
+ mTagHandler = tagHandler;
+ mReader = parser;
+ }
+
+ public Spanned convert() {
+
+ mReader.setContentHandler(this);
+ try {
+ mReader.parse(new InputSource(new StringReader(mSource)));
+ } catch (IOException e) {
+ // We are reading from a string. There should not be IO problems.
+ throw new RuntimeException(e);
+ } catch (SAXException e) {
+ // TagSoup doesn't throw parse exceptions.
+ throw new RuntimeException(e);
+ }
+
+ // Fix flags and range for paragraph-type markup.
+ Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
+ for (int i = 0; i < obj.length; i++) {
+ int start = mSpannableStringBuilder.getSpanStart(obj[i]);
+ int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
+
+ // If the last line of the range is blank, back off by one.
+ if (end - 2 >= 0) {
+ if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
+ mSpannableStringBuilder.charAt(end - 2) == '\n') {
+ end--;
+ }
+ }
+
+ if (end == start) {
+ mSpannableStringBuilder.removeSpan(obj[i]);
+ } else {
+ mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
+ }
+ }
+
+ return mSpannableStringBuilder;
+ }
+
+ private void handleStartTag(String tag, Attributes attributes) {
+ if (tag.equalsIgnoreCase("br")) {
+ // We don't need to handle this. TagSoup will ensure that there's a for each
+ // so we can safely emite the linebreaks when we handle the close tag.
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("strong")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("b")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("em")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("cite")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("i")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("big")) {
+ start(mSpannableStringBuilder, new Big());
+ } else if (tag.equalsIgnoreCase("small")) {
+ start(mSpannableStringBuilder, new Small());
+ } else if (tag.equalsIgnoreCase("font")) {
+ startFont(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Blockquote());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ start(mSpannableStringBuilder, new Monospace());
+ } else if (tag.equalsIgnoreCase("a")) {
+ startA(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("u")) {
+ start(mSpannableStringBuilder, new Underline());
+ } else if (tag.equalsIgnoreCase("ins")) {
+ start(mSpannableStringBuilder, new Underline());
+ } else if (tag.equalsIgnoreCase("strike")) {
+ start(mSpannableStringBuilder, new Strike());
+ } else if (tag.equalsIgnoreCase("s")) {
+ start(mSpannableStringBuilder, new Strike());
+ } else if (tag.equalsIgnoreCase("del")) {
+ start(mSpannableStringBuilder, new Strike());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ start(mSpannableStringBuilder, new Super());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ start(mSpannableStringBuilder, new Sub());
+ } else if (tag.length() == 2 &&
+ Character.toLowerCase(tag.charAt(0)) == 'h' &&
+ tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
+ } else if (tag.equalsIgnoreCase("img")) {
+ startImg(mSpannableStringBuilder, attributes, mImageGetter);
+ } else if (mTagHandler != null) {
+ mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
+ }
+ }
+
+ private void handleEndTag(String tag) {
+ if (tag.equalsIgnoreCase("br")) {
+ handleBr(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("strong")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("b")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("em")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("cite")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("i")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("big")) {
+ end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
+ } else if (tag.equalsIgnoreCase("small")) {
+ end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
+ } else if (tag.equalsIgnoreCase("font")) {
+ endFont(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ end(mSpannableStringBuilder, Monospace.class,
+ new TypefaceSpan("monospace"));
+ } else if (tag.equalsIgnoreCase("a")) {
+ endA(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("u")) {
+ end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
+ } else if (tag.equalsIgnoreCase("ins")) {
+ end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
+ } else if (tag.equalsIgnoreCase("strike")) {
+ end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan());
+ } else if (tag.equalsIgnoreCase("s")) {
+ end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan());
+ } else if (tag.equalsIgnoreCase("del")) {
+ end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
+ } else if (tag.length() == 2 &&
+ Character.toLowerCase(tag.charAt(0)) == 'h' &&
+ tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ endHeader(mSpannableStringBuilder);
+ } else if (mTagHandler != null) {
+ mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
+ }
+ }
+
+ private static void handleP(SpannableStringBuilder text) {
+ int len = text.length();
+
+ if (len >= 1 && text.charAt(len - 1) == '\n') {
+ if (len >= 2 && text.charAt(len - 2) == '\n') {
+ return;
+ }
+
+ text.append("\n");
+ return;
+ }
+
+ if (len != 0) {
+ text.append("\n\n");
+ }
+ }
+
+ private static void handleBr(SpannableStringBuilder text) {
+ text.append("\n");
+ }
+
+ private static Object getLast(Spanned text, Class kind) {
+ /*
+ * This knows that the last returned object from getSpans()
+ * will be the most recently added.
+ */
+ Object[] objs = text.getSpans(0, text.length(), kind);
+
+ if (objs.length == 0) {
+ return null;
+ } else {
+ return objs[objs.length - 1];
+ }
+ }
+
+ private static void start(SpannableStringBuilder text, Object mark) {
+ int len = text.length();
+ text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void end(SpannableStringBuilder text, Class kind,
+ Object repl) {
+ int len = text.length();
+ Object obj = getLast(text, kind);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ private static void startImg(SpannableStringBuilder text,
+ Attributes attributes, Html.ImageGetter img) {
+ String src = attributes.getValue("", "src");
+ Drawable d = null;
+
+ if (img != null) {
+ d = img.getDrawable(src);
+ }
+
+ if (d == null) {
+ d = Html.sResources.getDrawable(R.drawable.image_failed);
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ }
+
+ int len = text.length();
+ text.append("\uFFFC");
+
+ text.setSpan(new ImageSpan(d, src), len, text.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static void startFont(SpannableStringBuilder text,
+ Attributes attributes) {
+ String color = attributes.getValue("", "color");
+ String face = attributes.getValue("", "face");
+
+ int len = text.length();
+ text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endFont(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Font.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Font f = (Font) obj;
+
+ if (!TextUtils.isEmpty(f.mColor)) {
+ if (f.mColor.startsWith("@")) {
+ Resources res = Resources.getSystem();
+ String name = f.mColor.substring(1);
+ int colorRes = res.getIdentifier(name, "color", "android");
+ text.setSpan(new TextAppearanceSpan(null, 0, 0, ColorStateList.valueOf(res.getColor(colorRes)), null),
+ where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ int c = getHtmlColor(f.mColor);
+ if (c != -1) {
+ text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
+ where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ if (f.mFace != null) {
+ text.setSpan(new TypefaceSpan(f.mFace), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void startA(SpannableStringBuilder text, Attributes attributes) {
+ String href = attributes.getValue("", "href");
+
+ int len = text.length();
+ text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endA(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Href.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Href h = (Href) obj;
+
+ if (h.mHref != null) {
+ text.setSpan(new URLSpan(h.mHref), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void endHeader(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Header.class);
+
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ // Back off not to change only the text, not the blank line.
+ while (len > where && text.charAt(len - 1) == '\n') {
+ len--;
+ }
+
+ if (where != len) {
+ Header h = (Header) obj;
+
+ text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ text.setSpan(new StyleSpan(Typeface.BOLD),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ @Override
+ public void setDocumentLocator(Locator locator) {
+ }
+
+ @Override
+ public void startDocument() throws SAXException {
+ }
+
+ @Override
+ public void endDocument() throws SAXException {
+ }
+
+ @Override
+ public void startPrefixMapping(String prefix, String uri) throws SAXException {
+ }
+
+ @Override
+ public void endPrefixMapping(String prefix) throws SAXException {
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes)
+ throws SAXException {
+ handleStartTag(localName, attributes);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ handleEndTag(localName);
+ }
+
+ @Override
+ public void characters(char ch[], int start, int length) throws SAXException {
+ StringBuilder sb = new StringBuilder();
+
+ /*
+ * Ignore whitespace that immediately follows other whitespace;
+ * newlines count as spaces.
+ */
+
+ for (int i = 0; i < length; i++) {
+ char c = ch[i + start];
+
+ if (c == ' ' || c == '\n') {
+ char pred;
+ int len = sb.length();
+
+ if (len == 0) {
+ len = mSpannableStringBuilder.length();
+
+ if (len == 0) {
+ pred = '\n';
+ } else {
+ pred = mSpannableStringBuilder.charAt(len - 1);
+ }
+ } else {
+ pred = sb.charAt(len - 1);
+ }
+
+ if (pred != ' ' && pred != '\n') {
+ sb.append(' ');
+ }
+ } else {
+ sb.append(c);
+ }
+ }
+
+ mSpannableStringBuilder.append(sb);
+ }
+
+ @Override
+ public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
+ }
+
+ @Override
+ public void processingInstruction(String target, String data) throws SAXException {
+ }
+
+ @Override
+ public void skippedEntity(String name) throws SAXException {
+ }
+
+ private static class Bold { }
+ private static class Italic { }
+ private static class Underline { }
+ private static class Strike { }
+ private static class Big { }
+ private static class Small { }
+ private static class Monospace { }
+ private static class Blockquote { }
+ private static class Super { }
+ private static class Sub { }
+
+ private static class Font {
+ public String mColor;
+ public String mFace;
+
+ public Font(String color, String face) {
+ mColor = color;
+ mFace = face;
+ }
+ }
+
+ private static class Href {
+ public String mHref;
+
+ public Href(String href) {
+ mHref = href;
+ }
+ }
+
+ private static class Header {
+ private int mLevel;
+
+ public Header(int level) {
+ mLevel = level;
+ }
+ }
+
+ /**
+ * Parse the color string, and return the corresponding color-int.
+ * If the string cannot be parsed, throws an IllegalArgumentException
+ * exception. Supported formats are:
+ * #RRGGBB
+ * #AARRGGBB
+ * rgb(255, 255, 255)
+ * or color name
+ */
+ @ColorInt
+ public static int getHtmlColor(@NonNull String colorString) {
+ if (colorString.charAt(0) == '#') {
+ // Use a long to avoid rollovers on #ffXXXXXX
+ long color = Long.parseLong(colorString.substring(1), 16);
+ if (colorString.length() == 7) {
+ // Set the alpha value
+ color |= 0x00000000ff000000;
+ } else if (colorString.length() != 9) {
+ throw new IllegalArgumentException("Unknown color: " + colorString);
+ }
+ return (int)color;
+ } else if (colorString.startsWith("rgb(") && colorString.endsWith(")")) {
+ String str = colorString.substring(4, colorString.length() - 1);
+ String[] colors = str.split("[\\s]*,[\\s]*");
+ if (colors.length == 3) {
+ try {
+ return Color.argb(0xff, Integer.valueOf(colors[0]), Integer.valueOf(colors[1]), Integer.valueOf(colors[2]));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Unknown color: " + colorString);
+ }
+ }
+ } else {
+ Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT));
+ if (color != null) {
+ return color;
+ }
+ }
+ throw new IllegalArgumentException("Unknown color: " + colorString);
+ }
+
+ private static final HashMap sColorNameMap;
+
+ static {
+ sColorNameMap = new HashMap<>();
+ sColorNameMap.put("aliceblue", 0xFFF0F8FF);
+ sColorNameMap.put("antiquewhite", 0xFFFAEBD7);
+ sColorNameMap.put("aqua", 0xFFFFFF);
+ sColorNameMap.put("aquamarine", 0xFF7FFFD4);
+ sColorNameMap.put("azure", 0xFFF0FFFF);
+ sColorNameMap.put("beige", 0xFFF5F5DC);
+ sColorNameMap.put("bisque", 0xFFFFE4C4);
+ sColorNameMap.put("black", 0xFF0);
+ sColorNameMap.put("blanchedalmond", 0xFFFFEBCD);
+ sColorNameMap.put("blue", 0xFFFF);
+ sColorNameMap.put("blueviolet", 0xFF8A2BE2);
+ sColorNameMap.put("brown", 0xFFA52A2A);
+ sColorNameMap.put("burlywood", 0xFFDEB887);
+ sColorNameMap.put("cadetblue", 0xFF5F9EA0);
+ sColorNameMap.put("chartreuse", 0xFF7FFF00);
+ sColorNameMap.put("chocolate", 0xFFD2691E);
+ sColorNameMap.put("coral", 0xFFFF7F50);
+ sColorNameMap.put("cornflowerblue", 0xFF6495ED);
+ sColorNameMap.put("cornsilk", 0xFFFFF8DC);
+ sColorNameMap.put("crimson", 0xFFDC143C);
+ sColorNameMap.put("cyan", 0xFFFFFF);
+ sColorNameMap.put("darkblue", 0xFF8B);
+ sColorNameMap.put("darkcyan", 0xFF8B8B);
+ sColorNameMap.put("darkgoldenrod", 0xFFB8860B);
+ sColorNameMap.put("darkgray", 0xFFA9A9A9);
+ sColorNameMap.put("darkgreen", 0xFF6400);
+ sColorNameMap.put("darkkhaki", 0xFFBDB76B);
+ sColorNameMap.put("darkmagenta", 0xFF8B008B);
+ sColorNameMap.put("darkolivegreen", 0xFF556B2F);
+ sColorNameMap.put("darkorange", 0xFFFF8C00);
+ sColorNameMap.put("darkorchid", 0xFF9932CC);
+ sColorNameMap.put("darkred", 0xFF8B0000);
+ sColorNameMap.put("darksalmon", 0xFFE9967A);
+ sColorNameMap.put("darkseagreen", 0xFF8FBC8F);
+ sColorNameMap.put("darkslateblue", 0xFF483D8B);
+ sColorNameMap.put("darkslategray", 0xFF2F4F4F);
+ sColorNameMap.put("darkturquoise", 0xFFCED1);
+ sColorNameMap.put("darkviolet", 0xFF9400D3);
+ sColorNameMap.put("deeppink", 0xFFFF1493);
+ sColorNameMap.put("deepskyblue", 0xFFBFFF);
+ sColorNameMap.put("dimgray", 0xFF696969);
+ sColorNameMap.put("dodgerblue", 0xFF1E90FF);
+ sColorNameMap.put("firebrick", 0xFFB22222);
+ sColorNameMap.put("floralwhite", 0xFFFFFAF0);
+ sColorNameMap.put("forestgreen", 0xFF228B22);
+ sColorNameMap.put("fuchsia", 0xFFFF00FF);
+ sColorNameMap.put("gainsboro", 0xFFDCDCDC);
+ sColorNameMap.put("ghostwhite", 0xFFF8F8FF);
+ sColorNameMap.put("gold", 0xFFFFD700);
+ sColorNameMap.put("goldenrod", 0xFFDAA520);
+ sColorNameMap.put("gray", 0xFF808080);
+ sColorNameMap.put("green", 0xFF8000);
+ sColorNameMap.put("greenyellow", 0xFFADFF2F);
+ sColorNameMap.put("honeydew", 0xFFF0FFF0);
+ sColorNameMap.put("hotpink", 0xFFFF69B4);
+ sColorNameMap.put("indianred", 0xFFCD5C5C);
+ sColorNameMap.put("indigo", 0xFF4B0082);
+ sColorNameMap.put("ivory", 0xFFFFFFF0);
+ sColorNameMap.put("khaki", 0xFFF0E68C);
+ sColorNameMap.put("lavender", 0xFFE6E6FA);
+ sColorNameMap.put("lavenderblush", 0xFFFFF0F5);
+ sColorNameMap.put("lawngreen", 0xFF7CFC00);
+ sColorNameMap.put("lemonchiffon", 0xFFFFFACD);
+ sColorNameMap.put("lightblue", 0xFFADD8E6);
+ sColorNameMap.put("lightcoral", 0xFFF08080);
+ sColorNameMap.put("lightcyan", 0xFFE0FFFF);
+ sColorNameMap.put("lightgoldenrodyellow", 0xFFFAFAD2);
+ sColorNameMap.put("lightgreen", 0xFF90EE90);
+ sColorNameMap.put("lightgrey", 0xFFD3D3D3);
+ sColorNameMap.put("lightpink", 0xFFFFB6C1);
+ sColorNameMap.put("lightsalmon", 0xFFFFA07A);
+ sColorNameMap.put("lightseagreen", 0xFF20B2AA);
+ sColorNameMap.put("lightskyblue", 0xFF87CEFA);
+ sColorNameMap.put("lightslategray", 0xFF778899);
+ sColorNameMap.put("lightsteelblue", 0xFFB0C4DE);
+ sColorNameMap.put("lightyellow", 0xFFFFFFE0);
+ sColorNameMap.put("lime", 0xFFFF00);
+ sColorNameMap.put("limegreen", 0xFF32CD32);
+ sColorNameMap.put("linen", 0xFFFAF0E6);
+ sColorNameMap.put("magenta", 0xFFFF00FF);
+ sColorNameMap.put("maroon", 0xFF800000);
+ sColorNameMap.put("mediumaquamarine", 0xFF66CDAA);
+ sColorNameMap.put("mediumblue", 0xFFCD);
+ sColorNameMap.put("mediumorchid", 0xFFBA55D3);
+ sColorNameMap.put("mediumpurple", 0xFF9370DB);
+ sColorNameMap.put("mediumseagreen", 0xFF3CB371);
+ sColorNameMap.put("mediumslateblue", 0xFF7B68EE);
+ sColorNameMap.put("mediumspringgreen", 0xFFFA9A);
+ sColorNameMap.put("mediumturquoise", 0xFF48D1CC);
+ sColorNameMap.put("mediumvioletred", 0xFFC71585);
+ sColorNameMap.put("midnightblue", 0xFF191970);
+ sColorNameMap.put("mintcream", 0xFFF5FFFA);
+ sColorNameMap.put("mistyrose", 0xFFFFE4E1);
+ sColorNameMap.put("moccasin", 0xFFFFE4B5);
+ sColorNameMap.put("navajowhite", 0xFFFFDEAD);
+ sColorNameMap.put("navy", 0xFF80);
+ sColorNameMap.put("oldlace", 0xFFFDF5E6);
+ sColorNameMap.put("olive", 0xFF808000);
+ sColorNameMap.put("olivedrab", 0xFF6B8E23);
+ sColorNameMap.put("orange", 0xFFFFA500);
+ sColorNameMap.put("orangered", 0xFFFF4500);
+ sColorNameMap.put("orchid", 0xFFDA70D6);
+ sColorNameMap.put("palegoldenrod", 0xFFEEE8AA);
+ sColorNameMap.put("palegreen", 0xFF98FB98);
+ sColorNameMap.put("paleturquoise", 0xFFAFEEEE);
+ sColorNameMap.put("palevioletred", 0xFFDB7093);
+ sColorNameMap.put("papayawhip", 0xFFFFEFD5);
+ sColorNameMap.put("peachpuff", 0xFFFFDAB9);
+ sColorNameMap.put("peru", 0xFFCD853F);
+ sColorNameMap.put("pink", 0xFFFFC0CB);
+ sColorNameMap.put("plum", 0xFFDDA0DD);
+ sColorNameMap.put("powderblue", 0xFFB0E0E6);
+ sColorNameMap.put("purple", 0xFF800080);
+ sColorNameMap.put("red", 0xFFFF0000);
+ sColorNameMap.put("rosybrown", 0xFFBC8F8F);
+ sColorNameMap.put("royalblue", 0xFF4169E1);
+ sColorNameMap.put("saddlebrown", 0xFF8B4513);
+ sColorNameMap.put("salmon", 0xFFFA8072);
+ sColorNameMap.put("sandybrown", 0xFFF4A460);
+ sColorNameMap.put("seagreen", 0xFF2E8B57);
+ sColorNameMap.put("seashell", 0xFFFFF5EE);
+ sColorNameMap.put("sienna", 0xFFA0522D);
+ sColorNameMap.put("silver", 0xFFC0C0C0);
+ sColorNameMap.put("skyblue", 0xFF87CEEB);
+ sColorNameMap.put("slateblue", 0xFF6A5ACD);
+ sColorNameMap.put("slategray", 0xFF708090);
+ sColorNameMap.put("snow", 0xFFFFFAFA);
+ sColorNameMap.put("springgreen", 0xFFFF7F);
+ sColorNameMap.put("steelblue", 0xFF4682B4);
+ sColorNameMap.put("tan", 0xFFD2B48C);
+ sColorNameMap.put("teal", 0xFF8080);
+ sColorNameMap.put("thistle", 0xFFD8BFD8);
+ sColorNameMap.put("tomato", 0xFFFF6347);
+ sColorNameMap.put("turquoise", 0xFF40E0D0);
+ sColorNameMap.put("violet", 0xFFEE82EE);
+ sColorNameMap.put("wheat", 0xFFF5DEB3);
+ sColorNameMap.put("white", 0xFFFFFFFF);
+ sColorNameMap.put("whitesmoke", 0xFFF5F5F5);
+ sColorNameMap.put("yellow", 0xFFFFFF00);
+ sColorNameMap.put("yellowgreen", 0xFF9ACD32);
+ }
+}
diff --git a/app/src/main/java/com/hippo/utils/ReadableTime.java b/app/src/main/java/com/hippo/utils/ReadableTime.java
new file mode 100644
index 000000000..469a08142
--- /dev/null
+++ b/app/src/main/java/com/hippo/utils/ReadableTime.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2015 Hippo Seven
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.utils;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.hippo.ehviewer.R;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public final class ReadableTime {
+
+ private static Resources sResources;
+
+ public static final long SECOND_MILLIS = 1000;
+ public static final long MINUTE_MILLIS = 60 * SECOND_MILLIS;
+ public static final long HOUR_MILLIS = 60 * MINUTE_MILLIS;
+ public static final long DAY_MILLIS = 24 * HOUR_MILLIS;
+ public static final long WEEK_MILLIS = 7 * DAY_MILLIS;
+ public static final long YEAR_MILLIS = 365 * DAY_MILLIS;
+
+ public static final int SIZE = 5;
+
+ public static final long[] MULTIPLES = {
+ YEAR_MILLIS,
+ DAY_MILLIS,
+ HOUR_MILLIS,
+ MINUTE_MILLIS,
+ SECOND_MILLIS
+ };
+
+ public static final int[] UNITS = {
+ R.plurals.year,
+ R.plurals.day,
+ R.plurals.hour,
+ R.plurals.minute,
+ R.plurals.second
+ };
+
+ private static final Calendar sCalendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+08:00"));
+ private static final Object sCalendarLock = new Object();
+
+ private static final SimpleDateFormat DATE_FORMAT_WITHOUT_YEAR =
+ new SimpleDateFormat("MMM d", Locale.getDefault());
+
+ private static final SimpleDateFormat DATE_FORMAT_WIT_YEAR =
+ new SimpleDateFormat("yyy MMM d", Locale.getDefault());
+
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault());
+ private static final Object sDateFormatLock1 = new Object();
+
+ private static final SimpleDateFormat FILENAMABLE_DATE_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.getDefault());
+ private static final Object sDateFormatLock2 = new Object();
+
+ static {
+ // The website use GMT+08:00, so tell user the same
+ DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+08:00"));
+
+ DATE_FORMAT_WITHOUT_YEAR.setTimeZone(TimeZone.getTimeZone("GMT+08:00"));
+ }
+
+ public static void initialize(Context context) {
+ sResources = context.getApplicationContext().getResources();
+ }
+
+ /*
+ public static String getDisplayTime(long time) {
+ if (Settings.getPrettyTime()) {
+ return getTimeAgo(time);
+ } else {
+ return getPlainTime(time);
+ }
+ }
+ */
+
+ public static String getPlainTime(long time) {
+ synchronized (sDateFormatLock1) {
+ return DATE_FORMAT.format(new Date(time));
+ }
+ }
+
+ public static String getTimeAgo(long time) {
+ Resources resources = sResources;
+
+ long now = System.currentTimeMillis();
+ if (time > now + (2 * MINUTE_MILLIS) || time <= 0) {
+ return resources.getString(R.string.from_the_future);
+ }
+
+ final long diff = now - time;
+ if (diff < MINUTE_MILLIS) {
+ return resources.getString(R.string.just_now);
+ } else if (diff < 2 * MINUTE_MILLIS) {
+ return resources.getQuantityString(R.plurals.some_minutes_ago, 1, 1);
+ } else if (diff < 50 * MINUTE_MILLIS) {
+ int minutes = (int) (diff / MINUTE_MILLIS);
+ return resources.getQuantityString(R.plurals.some_minutes_ago, minutes, minutes);
+ } else if (diff < 90 * MINUTE_MILLIS) {
+ return resources.getQuantityString(R.plurals.some_hours_ago, 1, 1);
+ } else if (diff < 24 * HOUR_MILLIS) {
+ int hours = (int) (diff / HOUR_MILLIS);
+ return resources.getQuantityString(R.plurals.some_hours_ago, hours, hours);
+ } else if (diff < 48 * HOUR_MILLIS) {
+ return resources.getString(R.string.yesterday);
+ } else if (diff < WEEK_MILLIS) {
+ int days = (int) (diff / DAY_MILLIS);
+ return resources.getString(R.string.some_days_ago, days);
+ } else {
+ synchronized (sCalendarLock) {
+ Date nowDate = new Date(now);
+ Date timeDate = new Date(time);
+ sCalendar.setTime(nowDate);
+ int nowYear = sCalendar.get(Calendar.YEAR);
+ sCalendar.setTime(timeDate);
+ int timeYear = sCalendar.get(Calendar.YEAR);
+
+ if (nowYear == timeYear) {
+ return DATE_FORMAT_WITHOUT_YEAR.format(timeDate);
+ } else {
+ return DATE_FORMAT_WIT_YEAR.format(timeDate);
+ }
+ }
+ }
+ }
+
+ public static String getTimeInterval(long time) {
+ StringBuilder sb = new StringBuilder();
+ Resources resources = sResources;
+
+ long leftover = time;
+ boolean start = false;
+
+ for (int i = 0; i < SIZE; i++) {
+ long multiple = MULTIPLES[i];
+ long quotient = leftover / multiple;
+ long remainder = leftover % multiple;
+ if (start || quotient != 0 || i == SIZE - 1) {
+ if (start) {
+ sb.append(" ");
+ }
+ sb.append(quotient)
+ .append(" ")
+ .append(resources.getQuantityString(UNITS[i], (int) quotient));
+ start = true;
+ }
+ leftover = remainder;
+ }
+
+ return sb.toString();
+ }
+
+ public static String getFilenamableTime(long time) {
+ synchronized (sDateFormatLock2) {
+ return FILENAMABLE_DATE_FORMAT.format(new Date(time));
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/widget/AutoWrapLayout.java b/app/src/main/java/com/hippo/widget/AutoWrapLayout.java
new file mode 100644
index 000000000..fca20c477
--- /dev/null
+++ b/app/src/main/java/com/hippo/widget/AutoWrapLayout.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2015 Hippo Seven
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.widget;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.hippo.ehviewer.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A ViewGroup that can layout views in line and auto wrap
+ *
+ * @author Hippo
+ *
+ */
+public class AutoWrapLayout extends ViewGroup {
+
+ private final List rectList = new ArrayList<>();
+
+ private Alignment mAlignment;
+
+ private static final Alignment[] sBaseLineArray = { Alignment.TOP,
+ Alignment.CENTER, Alignment.BOTTOM };
+
+
+ public enum Alignment {
+ TOP(0),
+ CENTER(1),
+ BOTTOM(2);
+
+ Alignment(int ni) {
+ nativeInt = ni;
+ }
+
+ final int nativeInt;
+ }
+
+ public AutoWrapLayout(Context context) {
+ super(context);
+ }
+
+ public AutoWrapLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AutoWrapLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.AutoWrapLayout, defStyle, 0);
+
+ int index = a.getInt(R.styleable.AutoWrapLayout_alignment, -1);
+ if (index >= 0) {
+ setAlignment(sBaseLineArray[index]);
+ }
+
+ a.recycle();
+ }
+
+ public void setAlignment(Alignment baseLine) {
+ if (baseLine == null) {
+ return;
+ }
+
+ if (mAlignment != baseLine) {
+ mAlignment = baseLine;
+
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ public Alignment getAlignment() {
+ return mAlignment;
+ }
+
+ private void adjustBaseLine(int lineHeight, int startIndex, int endIndex) {
+ if (mAlignment == Alignment.TOP)
+ return;
+
+ for (int index = startIndex; index < endIndex; index++) {
+ final View child = getChildAt(index);
+ final MarginLayoutParams lp =
+ (MarginLayoutParams)child.getLayoutParams();
+ Rect rect = rectList.get(index);
+ int offsetRaw = lineHeight - rect.height() - lp.topMargin - lp.bottomMargin;
+ if (mAlignment == Alignment.CENTER)
+ rect.offset(0, offsetRaw/2);
+ else if (mAlignment == Alignment.BOTTOM)
+ rect.offset(0, offsetRaw);
+ }
+ }
+
+
+ // TODO Take vertical mode
+ /**
+ * each row or line at least show one child
+ *
+ * horizontal only show child can show or partly show in parent
+ */
+ @SuppressLint("DrawAllocation")
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode == MeasureSpec.UNSPECIFIED)
+ maxWidth = Integer.MAX_VALUE;
+ if (heightMode == MeasureSpec.UNSPECIFIED)
+ maxHeight = Integer.MAX_VALUE;
+
+ int paddingLeft = getPaddingLeft();
+ int paddingTop = getPaddingTop();
+ int paddingRight = getPaddingRight();
+ int paddingBottom = getPaddingBottom();
+
+ int maxRightBound = maxWidth - paddingRight;
+ int maxBottomBound = maxHeight - paddingBottom;
+
+ int left;
+ int top;
+ int right;
+ int bottom;
+ int rightBound = paddingLeft;
+ int maxRightNoPadding = rightBound;
+ int bottomBound;
+ int lastMaxBottom = paddingTop;
+ int maxBottom = lastMaxBottom;
+ int childWidth;
+ int childHeight;
+
+ int lineStartIndex = 0;
+ int lineEndIndex; // endIndex + 1
+
+ rectList.clear();
+ int childCount = getChildCount();
+ for (int index = 0; index < childCount; index++) {
+ final View child = getChildAt(index);
+ child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ if (child.getVisibility() == View.GONE)
+ continue;
+ final MarginLayoutParams lp =
+ (MarginLayoutParams)child.getLayoutParams();
+ childWidth = child.getMeasuredWidth();
+ childHeight = child.getMeasuredHeight();
+
+ left = rightBound + lp.leftMargin;
+ right = left + childWidth;
+ rightBound = right + lp.rightMargin;
+ if (rightBound > maxRightBound) { // Go to next row
+ lineEndIndex = index;
+ // Adjust child position base on baseline
+ adjustBaseLine(maxBottom - lastMaxBottom, lineStartIndex, lineEndIndex);
+
+ // If child can't show in parent begin this line
+ if (maxBottom >= maxBottomBound)
+ break;
+
+ // If it is first item in line, try to show it all
+ if (lineEndIndex == lineStartIndex) {
+ child.measure(MeasureSpec.makeMeasureSpec(
+ maxWidth - paddingLeft - paddingRight
+ - lp.leftMargin - lp.rightMargin, MeasureSpec.AT_MOST),
+ MeasureSpec.UNSPECIFIED);
+ childWidth = child.getMeasuredWidth();
+ childHeight = child.getMeasuredHeight();
+ }
+ left = paddingLeft + lp.leftMargin;
+ right = left + childWidth;
+ rightBound = right + lp.rightMargin;
+
+ lastMaxBottom = maxBottom;
+ top = lastMaxBottom + lp.topMargin;
+ bottom = top + childHeight;
+ bottomBound = bottom + lp.bottomMargin;
+
+ lineStartIndex = index;
+ } else {
+ top = lastMaxBottom + lp.topMargin;
+ bottom = top + childHeight;
+ bottomBound = bottom + lp.bottomMargin;
+ }
+ // Update max
+ if (rightBound > maxRightNoPadding)
+ maxRightNoPadding = rightBound;
+ if (bottomBound > maxBottom)
+ maxBottom = bottomBound;
+ Rect rect = new Rect();
+ rect.left = left;
+ rect.top = top;
+ rect.right = right;
+ rect.bottom = bottom;
+ rectList.add(rect);
+ }
+
+ // Handle last line baseline
+ adjustBaseLine(maxBottom - lastMaxBottom, lineStartIndex, rectList.size());
+
+ int measuredWidth;
+ int measuredHeight;
+
+ if (widthMode == MeasureSpec.EXACTLY)
+ measuredWidth = maxWidth;
+ else
+ measuredWidth = maxRightNoPadding + paddingRight;
+ if (heightMode == MeasureSpec.EXACTLY)
+ measuredHeight = maxHeight;
+ else {
+ measuredHeight = maxBottom + paddingBottom;
+ if (heightMode == MeasureSpec.AT_MOST)
+ measuredHeight = measuredHeight > maxHeight ? maxHeight : measuredHeight;
+ }
+
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int count = rectList.size();
+ for(int i = 0; i < count; i++){
+ final View child = this.getChildAt(i);
+ if (child.getVisibility() == View.GONE)
+ continue;
+ Rect rect = rectList.get(i);
+ child.layout(rect.left, rect.top, rect.right, rect.bottom);
+ }
+ }
+
+ @Override
+ public MarginLayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected MarginLayoutParams generateDefaultLayoutParams() {
+ return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected MarginLayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new MarginLayoutParams(p);
+ }
+}
diff --git a/app/src/main/java/com/hippo/widget/LinkifyTextView.java b/app/src/main/java/com/hippo/widget/LinkifyTextView.java
new file mode 100644
index 000000000..3dc77502e
--- /dev/null
+++ b/app/src/main/java/com/hippo/widget/LinkifyTextView.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2015 Hippo Seven
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.widget;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.text.Layout;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+public class LinkifyTextView extends TextView {
+
+ private ClickableSpan mCurrentSpan;
+
+ public LinkifyTextView(Context context) {
+ super(context);
+ }
+
+ public LinkifyTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public LinkifyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public ClickableSpan getCurrentSpan() {
+ return mCurrentSpan;
+ }
+
+ public void clearCurrentSpan() {
+ mCurrentSpan = null;
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ // Let the parent or grandparent of TextView to handles click aciton.
+ // Otherwise click effect like ripple will not work, and if touch area
+ // do not contain a url, the TextView will still get MotionEvent.
+ // onTouchEven must be called with MotionEvent.ACTION_DOWN for each touch
+ // action on it, so we analyze touched url here.
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ mCurrentSpan = null;
+
+ if (getText() instanceof Spanned) {
+ // Get this code from android.text.method.LinkMovementMethod.
+ // Work fine !
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= getTotalPaddingLeft();
+ y -= getTotalPaddingTop();
+
+ x += getScrollX();
+ y += getScrollY();
+
+ Layout layout = getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ ClickableSpan[] spans = ((Spanned)getText()).getSpans(off, off, ClickableSpan.class);
+
+ if (spans.length > 0) {
+ mCurrentSpan = spans[0];
+ }
+ }
+ }
+
+ return super.onTouchEvent(event);
+ }
+}
diff --git a/app/src/main/res/anim/path_heart.xml b/app/src/main/res/anim/path_heart.xml
new file mode 100644
index 000000000..f10510483
--- /dev/null
+++ b/app/src/main/res/anim/path_heart.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/app/src/main/res/anim/path_heart_outline.xml b/app/src/main/res/anim/path_heart_outline.xml
new file mode 100644
index 000000000..3535ea1b4
--- /dev/null
+++ b/app/src/main/res/anim/path_heart_outline.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/app/src/main/res/drawable-hdpi/ic_share_primary_x48.png b/app/src/main/res/drawable-hdpi/ic_share_primary_x48.png
new file mode 100644
index 000000000..cd5e5ea91
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_share_primary_x48.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_thumb_up_primary_x48.png b/app/src/main/res/drawable-hdpi/ic_thumb_up_primary_x48.png
new file mode 100644
index 000000000..2c0fa4560
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_thumb_up_primary_x48.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_utorrent_primary_x48.png b/app/src/main/res/drawable-hdpi/ic_utorrent_primary_x48.png
new file mode 100644
index 000000000..c5f646a5c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_utorrent_primary_x48.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_share_primary_x48.png b/app/src/main/res/drawable-mdpi/ic_share_primary_x48.png
new file mode 100644
index 000000000..6040570ab
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_share_primary_x48.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_thumb_up_primary_x48.png b/app/src/main/res/drawable-mdpi/ic_thumb_up_primary_x48.png
new file mode 100644
index 000000000..aea500f99
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_thumb_up_primary_x48.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_utorrent_primary_x48.png b/app/src/main/res/drawable-mdpi/ic_utorrent_primary_x48.png
new file mode 100644
index 000000000..2b1189723
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_utorrent_primary_x48.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_share_primary_x48.png b/app/src/main/res/drawable-xhdpi/ic_share_primary_x48.png
new file mode 100644
index 000000000..8870aaaf2
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_share_primary_x48.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_thumb_up_primary_x48.png b/app/src/main/res/drawable-xhdpi/ic_thumb_up_primary_x48.png
new file mode 100644
index 000000000..36aaab179
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_thumb_up_primary_x48.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_utorrent_primary_x48.png b/app/src/main/res/drawable-xhdpi/ic_utorrent_primary_x48.png
new file mode 100644
index 000000000..67826a1bb
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_utorrent_primary_x48.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_share_primary_x48.png b/app/src/main/res/drawable-xxhdpi/ic_share_primary_x48.png
new file mode 100644
index 000000000..a8da0117a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_share_primary_x48.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_thumb_up_primary_x48.png b/app/src/main/res/drawable-xxhdpi/ic_thumb_up_primary_x48.png
new file mode 100644
index 000000000..487bcbab2
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_thumb_up_primary_x48.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_utorrent_primary_x48.png b/app/src/main/res/drawable-xxhdpi/ic_utorrent_primary_x48.png
new file mode 100644
index 000000000..f5f039c3d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_utorrent_primary_x48.png differ
diff --git a/app/src/main/res/drawable/ic_dots_vertical.xml b/app/src/main/res/drawable/ic_dots_vertical.xml
new file mode 100644
index 000000000..af35ab4ca
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dots_vertical.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml
new file mode 100644
index 000000000..e20441f1f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_heart.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_heart_animated.xml b/app/src/main/res/drawable/ic_heart_animated.xml
new file mode 100644
index 000000000..0c1ab80d5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_heart_animated.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_heart_outline.xml b/app/src/main/res/drawable/ic_heart_outline.xml
new file mode 100644
index 000000000..29f5aa871
--- /dev/null
+++ b/app/src/main/res/drawable/ic_heart_outline.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_heart_outline_animated.xml b/app/src/main/res/drawable/ic_heart_outline_animated.xml
new file mode 100644
index 000000000..ce71ca40e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_heart_outline_animated.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/shadow_bottom.xml b/app/src/main/res/drawable/shadow_bottom.xml
new file mode 100644
index 000000000..680f43191
--- /dev/null
+++ b/app/src/main/res/drawable/shadow_bottom.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 39b8a921e..36e664e6d 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -34,7 +34,6 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="left"
- android:background="@color/background_light"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/nav_drawer_main"/>
diff --git a/app/src/main/res/layout/gallery_detail_actions.xml b/app/src/main/res/layout/gallery_detail_actions.xml
new file mode 100644
index 000000000..3c2d776c3
--- /dev/null
+++ b/app/src/main/res/layout/gallery_detail_actions.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/gallery_detail_comments.xml b/app/src/main/res/layout/gallery_detail_comments.xml
new file mode 100644
index 000000000..960f9823b
--- /dev/null
+++ b/app/src/main/res/layout/gallery_detail_comments.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/gallery_detail_content.xml b/app/src/main/res/layout/gallery_detail_content.xml
new file mode 100644
index 000000000..352fcd54b
--- /dev/null
+++ b/app/src/main/res/layout/gallery_detail_content.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/gallery_detail_header.xml b/app/src/main/res/layout/gallery_detail_header.xml
index 63ab6f5e1..2768409c3 100644
--- a/app/src/main/res/layout/gallery_detail_header.xml
+++ b/app/src/main/res/layout/gallery_detail_header.xml
@@ -15,57 +15,145 @@
~ limitations under the License.
-->
-
+ android:orientation="vertical">
-
-
-
+ android:background="@color/colorPrimary">
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/gallery_detail_info.xml b/app/src/main/res/layout/gallery_detail_info.xml
new file mode 100644
index 000000000..6a63867ed
--- /dev/null
+++ b/app/src/main/res/layout/gallery_detail_info.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/gallery_detail_progress.xml b/app/src/main/res/layout/gallery_detail_progress.xml
new file mode 100644
index 000000000..90cc52b18
--- /dev/null
+++ b/app/src/main/res/layout/gallery_detail_progress.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/gallery_detail_tags.xml b/app/src/main/res/layout/gallery_detail_tags.xml
new file mode 100644
index 000000000..6caaf784f
--- /dev/null
+++ b/app/src/main/res/layout/gallery_detail_tags.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/gallery_tag_group.xml b/app/src/main/res/layout/gallery_tag_group.xml
new file mode 100644
index 000000000..919ffa647
--- /dev/null
+++ b/app/src/main/res/layout/gallery_tag_group.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/app/src/main/res/layout/item_gallery_comment.xml b/app/src/main/res/layout/item_gallery_comment.xml
new file mode 100644
index 000000000..83b9e3a57
--- /dev/null
+++ b/app/src/main/res/layout/item_gallery_comment.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_gallery_tag.xml b/app/src/main/res/layout/item_gallery_tag.xml
new file mode 100644
index 000000000..f85ce7556
--- /dev/null
+++ b/app/src/main/res/layout/item_gallery_tag.xml
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/app/src/main/res/layout/scene_gallery_detail.xml b/app/src/main/res/layout/scene_gallery_detail.xml
index 5f0e99c94..b6cf13f7e 100644
--- a/app/src/main/res/layout/scene_gallery_detail.xml
+++ b/app/src/main/res/layout/scene_gallery_detail.xml
@@ -19,12 +19,18 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:animateLayoutChanges="true">
-
+ android:layout_height="match_parent">
+
+
+
+
-
diff --git a/app/src/main/res/layout/search_normal.xml b/app/src/main/res/layout/search_normal.xml
index 85463440f..f86fc4f22 100644
--- a/app/src/main/res/layout/search_normal.xml
+++ b/app/src/main/res/layout/search_normal.xml
@@ -40,19 +40,19 @@
android:paddingTop="@dimen/search_item_interval"
android:checkedButton="@+id/search_normal_search">
-
-
-
+
+
+
diff --git a/app/src/main/res/values-v21/themes.xml b/app/src/main/res/values-v21/themes.xml
index f04730d85..85dd461d6 100644
--- a/app/src/main/res/values-v21/themes.xml
+++ b/app/src/main/res/values-v21/themes.xml
@@ -17,7 +17,7 @@
-
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 77c443c08..0509fa051 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -57,4 +57,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 718e32cd1..072456a6e 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -44,6 +44,7 @@
#ff00796b
#ff9c27b0
#ffe040fb
+ #ffd500f9
#ffaa00ff
#ff00acc1
#ff7cb342
@@ -91,4 +92,7 @@
#20000000
+
+ #38000000
+
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index 2e7c97009..12f4ca598 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -17,6 +17,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/pathdata.xml b/app/src/main/res/values/pathdata.xml
index 787045a51..765fba685 100644
--- a/app/src/main/res/values/pathdata.xml
+++ b/app/src/main/res/values/pathdata.xml
@@ -21,5 +21,9 @@
M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M15.5,8C16.3,8 17,8.7 17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M12,14C13.75,14 15.29,14.72 16.19,15.81L14.77,17.23C14.32,16.5 13.25,16 12,16C10.75,16 9.68,16.5 9.23,17.23L7.81,15.81C8.71,14.72 10.25,14 12,14Z
M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z
M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z
+ M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z
+
+ M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z
+ M12,12L12,12L12,12C12,12 12,12 12,12C12,12 12,12 12,12C12,12 12,12 12,12H12C12,12 12,12 12,12C12,12 12,12 12,12C12,12 12,12 12,12M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9a10a8986..6f6daea64 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -122,4 +122,73 @@
4 stars
5 stars
+
+ Read
+ More information
+
+ - %d page
+ - %d pages
+
+
+ %d \u2665
+ %1$s (%2$d)
+
+ Favorite
+ Torrent (%d)
+ Share
+ Rate
+ No tags
+ No comments
+ No more comments
+ More comments
+ Refresh
+ Open in other app
+
+
+ MASTERPIECE
+ AMAZING
+ GREAT
+ GOOD
+ OKAY
+ MEDIOCRE
+ BAD
+ AWFUL
+ PAINFUL
+ UNBEARABLE
+ DISASTER
+
+
+ From the future
+ Just now
+
+ - A minute ago
+ - %d minutes ago
+
+
+ - An hour ago
+ - %d hours ago
+
+ Yesterday
+ %d days ago
+
+ - second
+ - seconds
+
+
+ - minute
+ - minutes
+
+
+ - hour
+ - hours
+
+
+ - day
+ - days
+
+
+ - year
+ - years
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index adfc5bf1a..6eab96eb4 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -43,6 +43,7 @@
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 76bfe809e..8001a9d84 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -24,6 +24,11 @@
- @color/colorAccent
+
+
-
diff --git a/beerbelly b/beerbelly
index f13aeed90..40be56ef2 160000
--- a/beerbelly
+++ b/beerbelly
@@ -1 +1 @@
-Subproject commit f13aeed90431e50b6a06951c6a24411b866f1bbe
+Subproject commit 40be56ef200386ec723155e9a4076ea32f869aa8
diff --git a/conaco b/conaco
index becb4a8c4..6f3aec6cd 160000
--- a/conaco
+++ b/conaco
@@ -1 +1 @@
-Subproject commit becb4a8c420476198d1cb63d894ca4e599e7195d
+Subproject commit 6f3aec6cd090f856980155db2823fbcb55a2ad0c
diff --git a/rippleold b/rippleold
index a007cc820..3170458de 160000
--- a/rippleold
+++ b/rippleold
@@ -1 +1 @@
-Subproject commit a007cc820cbb7746f6ba045923e5f09c0596bfd1
+Subproject commit 3170458debc9c2c756f831f85a5514d7c7d9b98b
diff --git a/vector b/vector
index c64d4c31a..a1c9374b5 160000
--- a/vector
+++ b/vector
@@ -1 +1 @@
-Subproject commit c64d4c31a4e1854a2f646b8d03ba828ede224f86
+Subproject commit a1c9374b5a8a9b4e66adaa1f066157f1cd82f2cf