From 056a6eb394fccaf2d9a5578e051976d68cf46f7d Mon Sep 17 00:00:00 2001 From: seven332 Date: Mon, 1 Feb 2016 22:34:55 +0800 Subject: [PATCH] Improve GalleryDetailScene --- app/build.gradle | 1 + .../java/com/hippo/ehviewer/DumpTest.java | 29 + .../com/hippo/ehviewer/EhApplication.java | 63 +- .../ehviewer/ui/scene/GalleryDetailScene.java | 674 ++++++++-- .../ehviewer/ui/scene/GalleryListScene.java | 114 +- .../hippo/ehviewer/widget/DividerView.java | 84 ++ .../com/hippo/scene/SceneApplication.java | 9 + .../java/com/hippo/scene/StageActivity.java | 23 +- .../java/com/hippo/scene/StageLayout.java | 2 +- app/src/main/java/com/hippo/text/Html.java | 1087 +++++++++++++++++ .../java/com/hippo/utils/ReadableTime.java | 178 +++ .../java/com/hippo/widget/AutoWrapLayout.java | 268 ++++ .../com/hippo/widget/LinkifyTextView.java | 88 ++ app/src/main/res/anim/path_heart.xml | 24 + app/src/main/res/anim/path_heart_outline.xml | 24 + .../drawable-hdpi/ic_share_primary_x48.png | Bin 0 -> 359 bytes .../drawable-hdpi/ic_thumb_up_primary_x48.png | Bin 0 -> 339 bytes .../drawable-hdpi/ic_utorrent_primary_x48.png | Bin 0 -> 655 bytes .../drawable-mdpi/ic_share_primary_x48.png | Bin 0 -> 289 bytes .../drawable-mdpi/ic_thumb_up_primary_x48.png | Bin 0 -> 272 bytes .../drawable-mdpi/ic_utorrent_primary_x48.png | Bin 0 -> 493 bytes .../drawable-xhdpi/ic_share_primary_x48.png | Bin 0 -> 423 bytes .../ic_thumb_up_primary_x48.png | Bin 0 -> 409 bytes .../ic_utorrent_primary_x48.png | Bin 0 -> 823 bytes .../drawable-xxhdpi/ic_share_primary_x48.png | Bin 0 -> 570 bytes .../ic_thumb_up_primary_x48.png | Bin 0 -> 538 bytes .../ic_utorrent_primary_x48.png | Bin 0 -> 1196 bytes .../main/res/drawable/ic_dots_vertical.xml | 32 + app/src/main/res/drawable/ic_heart.xml | 30 + .../main/res/drawable/ic_heart_animated.xml | 26 + .../main/res/drawable/ic_heart_outline.xml | 30 + .../drawable/ic_heart_outline_animated.xml | 26 + app/src/main/res/drawable/shadow_bottom.xml | 27 + app/src/main/res/layout/activity_main.xml | 1 - .../res/layout/gallery_detail_actions.xml | 106 ++ .../res/layout/gallery_detail_comments.xml | 34 + .../res/layout/gallery_detail_content.xml | 75 ++ .../main/res/layout/gallery_detail_header.xml | 180 ++- .../main/res/layout/gallery_detail_info.xml | 88 ++ .../res/layout/gallery_detail_progress.xml | 31 + .../main/res/layout/gallery_detail_tags.xml | 35 + app/src/main/res/layout/gallery_tag_group.xml | 24 + .../main/res/layout/item_gallery_comment.xml | 48 + app/src/main/res/layout/item_gallery_tag.xml | 28 + .../main/res/layout/scene_gallery_detail.xml | 15 +- app/src/main/res/layout/search_normal.xml | 6 +- .../res/menu/gallery_detail_other_action.xml | 29 + app/src/main/res/values-v21/themes.xml | 2 +- app/src/main/res/values/attrs.xml | 14 + app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/ids.xml | 5 + app/src/main/res/values/pathdata.xml | 4 + app/src/main/res/values/strings.xml | 69 ++ app/src/main/res/values/styles.xml | 11 + app/src/main/res/values/themes.xml | 7 +- beerbelly | 2 +- conaco | 2 +- rippleold | 2 +- vector | 2 +- 59 files changed, 3451 insertions(+), 212 deletions(-) create mode 100644 app/src/androidTest/java/com/hippo/ehviewer/DumpTest.java create mode 100644 app/src/main/java/com/hippo/ehviewer/widget/DividerView.java create mode 100644 app/src/main/java/com/hippo/text/Html.java create mode 100644 app/src/main/java/com/hippo/utils/ReadableTime.java create mode 100644 app/src/main/java/com/hippo/widget/AutoWrapLayout.java create mode 100644 app/src/main/java/com/hippo/widget/LinkifyTextView.java create mode 100644 app/src/main/res/anim/path_heart.xml create mode 100644 app/src/main/res/anim/path_heart_outline.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_share_primary_x48.png create mode 100644 app/src/main/res/drawable-hdpi/ic_thumb_up_primary_x48.png create mode 100644 app/src/main/res/drawable-hdpi/ic_utorrent_primary_x48.png create mode 100644 app/src/main/res/drawable-mdpi/ic_share_primary_x48.png create mode 100644 app/src/main/res/drawable-mdpi/ic_thumb_up_primary_x48.png create mode 100644 app/src/main/res/drawable-mdpi/ic_utorrent_primary_x48.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_share_primary_x48.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_thumb_up_primary_x48.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_utorrent_primary_x48.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_share_primary_x48.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_thumb_up_primary_x48.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_utorrent_primary_x48.png create mode 100644 app/src/main/res/drawable/ic_dots_vertical.xml create mode 100644 app/src/main/res/drawable/ic_heart.xml create mode 100644 app/src/main/res/drawable/ic_heart_animated.xml create mode 100644 app/src/main/res/drawable/ic_heart_outline.xml create mode 100644 app/src/main/res/drawable/ic_heart_outline_animated.xml create mode 100644 app/src/main/res/drawable/shadow_bottom.xml create mode 100644 app/src/main/res/layout/gallery_detail_actions.xml create mode 100644 app/src/main/res/layout/gallery_detail_comments.xml create mode 100644 app/src/main/res/layout/gallery_detail_content.xml create mode 100644 app/src/main/res/layout/gallery_detail_info.xml create mode 100644 app/src/main/res/layout/gallery_detail_progress.xml create mode 100644 app/src/main/res/layout/gallery_detail_tags.xml create mode 100644 app/src/main/res/layout/gallery_tag_group.xml create mode 100644 app/src/main/res/layout/item_gallery_comment.xml create mode 100644 app/src/main/res/layout/item_gallery_tag.xml create mode 100644 app/src/main/res/menu/gallery_detail_other_action.xml 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 mGlobalStuffMap = new SparseArray<>(); private EhCookieStore mEhCookieStore; private EhClient mEhClient; private OkHttpClient mOkHttpClient; private ImageWrapperHelper mImageWrapperHelper; private Conaco mConaco; + private LruMap mGalleryDetailCache; @Override public void onCreate() { @@ -49,6 +58,42 @@ public class EhApplication extends Application { CookieDB.initialize(this); StatusCodeException.initialize(this); Settings.initialize(this); + ReadableTime.initialize(this); + } + + public int putGlobalStuff(@NonNull Object o) { + int id = mIdGenerator.nextId(); + mGlobalStuffMap.put(id, o); + return id; + } + + public boolean containGlobalStuff(int id) { + return mGlobalStuffMap.indexOfKey(id) >= 0; + } + + public Object getGlobalStuff(int id) { + return mGlobalStuffMap.get(id); + } + + public Object removeGlobalStuff(int id) { + int index = mGlobalStuffMap.indexOfKey(id); + if (index >= 0) { + Object o = mGlobalStuffMap.valueAt(index); + mGlobalStuffMap.removeAt(index); + return o; + } else { + return null; + } + } + + public boolean removeGlobalStuff(Object o) { + int index = mGlobalStuffMap.indexOfValue(o); + if (index >= 0) { + mGlobalStuffMap.removeAt(index); + return true; + } else { + return false; + } } public static EhCookieStore getEhCookieStore(@NonNull Context context) { @@ -115,4 +160,18 @@ public class EhApplication extends Application { } return application.mConaco; } + + @NonNull + public static LruMap getGalleryDetailCache(@NonNull Context context) { + EhApplication application = ((EhApplication) context.getApplicationContext()); + if (application.mGalleryDetailCache == null) { + application.mGalleryDetailCache = new LruMap<>(new Comparator() { + @Override + public int compare(Integer lhs, Integer rhs) { + return lhs - rhs; + } + }, 3 * 60 * 1000); // 3 min timeout + } + return application.mGalleryDetailCache; + } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java index ca095256a..4a75876af 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java @@ -17,29 +17,48 @@ package com.hippo.ehviewer.ui.scene; import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.PopupMenu; +import android.text.TextUtils; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RatingBar; +import android.widget.ScrollView; import android.widget.TextView; -import com.hippo.easyrecyclerview.SimpleHolder; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.EhCacheKeyFactory; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.EhClient; +import com.hippo.ehviewer.client.EhRequest; +import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.EhUtils; +import com.hippo.ehviewer.client.data.GalleryComment; +import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.ehviewer.client.data.GalleryTagGroup; +import com.hippo.ehviewer.client.parser.GalleryDetailParser; +import com.hippo.rippleold.RippleSalon; +import com.hippo.scene.SceneFragment; +import com.hippo.scene.StageActivity; +import com.hippo.text.Html; import com.hippo.utils.ApiHelper; -import com.hippo.view.ViewTransition; +import com.hippo.utils.ReadableTime; +import com.hippo.vector.AnimatedVectorDrawable; +import com.hippo.vector.VectorDrawable; +import com.hippo.widget.AutoWrapLayout; import com.hippo.widget.LoadImageView; +import com.hippo.widget.SimpleImageView; // TODO Update drawer checked item -public class GalleryDetailScene extends BaseScene { +public class GalleryDetailScene extends BaseScene implements View.OnClickListener { public final static String KEY_ACTION = "action"; public static final String ACTION_GALLERY_INFO = "action_gallery_info"; @@ -49,22 +68,13 @@ public class GalleryDetailScene extends BaseScene { public static final String KEY_GID = "gid"; public static final String KEY_TOKEN = "token"; - public static final int TYPE_HEADER = 0; - public static final int TYPE_BUTTON = 1; - public static final int TYPE_INFO = 2; - public static final int TYPE_ACTION = 3; - public static final int TYPE_TAG = 4; - public static final int TYPE_COMMENT = 5; - public static final int TYPE_PREVIEW = 6; - public static final int TYPE_PROGRESS = 7; + private static final String KEY_GALLERY_DETAIL = "gallery_detail"; + private static final String KEY_REQUEST_ID = "request_id"; - private EhClient mClient; - - private RecyclerView mRecyclerView; - private View mMainProgressView; + private ScrollView mMainView; + private View mProgressView; private ViewGroup mFailedView; private TextView mFailedText; - private ViewTransition mViewTransition; // Header private View mHeader; @@ -72,15 +82,51 @@ public class GalleryDetailScene extends BaseScene { private TextView mTitle; private TextView mUploader; private TextView mCategory; + private SimpleImageView mOtherActions; + private ViewGroup mActionGroup; + private View mDownload; + private View mRead; + // Below header + private View mBelowHeader; + // Info + private View mInfo; + private TextView mLanguage; + private TextView mPages; + private TextView mSize; + private TextView mPosted; + private TextView mFavoredTimes; + // Actions + private View mActions; + private TextView mRatingText; + private RatingBar mRating; + private View mHeartGroup; + private TextView mHeart; + private TextView mHeartOutline; + private TextView mTorrent; + private TextView mShare; + private TextView mRate; + // Tags + private LinearLayout mTags; + private TextView mNoTags; + // Comments + private LinearLayout mComments; + private TextView mCommentsText; + + + // Progress + private View mProgress; + + private AnimatedVectorDrawable mHeartDrawable; + private AnimatedVectorDrawable mHeartOutlineDrawable; + private PopupMenu mPopupMenu; private String mAction; private GalleryInfo mGalleryInfo; private int mGid; - private String mKey; + private String mToken; - public GalleryDetailScene() { - - } + private GalleryDetail mGalleryDetail; + private int mRequestId; private void handleArgs(Bundle args) { if (args == null) { @@ -93,7 +139,39 @@ public class GalleryDetailScene extends BaseScene { mGalleryInfo = args.getParcelable(KEY_GALLERY_INFO); } else if (ACTION_GID_TOKEN.equals(action)) { mGid = args.getInt(KEY_GID); - mKey = args.getString(KEY_TOKEN); + mToken = args.getString(KEY_TOKEN); + } + } + + @Nullable + private String getGalleryDetailUrl() { + int gid; + String token; + if (mGalleryDetail != null) { + gid = mGalleryDetail.gid; + token = mGalleryDetail.token; + } else if (mGalleryInfo != null) { + gid = mGalleryInfo.gid; + token = mGalleryInfo.token; + } else if (ACTION_GID_TOKEN.equals(mAction)) { + gid = mGid; + token = mToken; + } else { + return null; + } + return EhUrl.getGalleryDetailUrl(gid, token); + } + + // -1 for error + private int getGid() { + if (mGalleryDetail != null) { + return mGalleryDetail.gid; + } else if (mGalleryInfo != null) { + return mGalleryInfo.gid; + } else if (ACTION_GID_TOKEN.equals(mAction)) { + return mGid; + } else { + return -1; } } @@ -101,39 +179,24 @@ public class GalleryDetailScene extends BaseScene { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mClient = EhApplication.getEhClient(getContext()); + if (savedInstanceState == null) { + onInit(); + } else { + onRestore(savedInstanceState); + } + } + private void onInit() { handleArgs(getArguments()); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void bindView() { - int gid = -1; - boolean notFound = true; - if (ACTION_GALLERY_INFO.equals(mAction) && mGalleryInfo != null) { - GalleryInfo gi = mGalleryInfo; - gid = gi.gid; - mThumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb, true); - mTitle.setText(gi.title); - mUploader.setText(gi.uploader); - mCategory.setText(EhUtils.getCategory(gi.category)); - mCategory.setTextColor(EhUtils.getCategoryColor(gi.category)); - notFound = false; - } else if (ACTION_GID_TOKEN.equals(mAction)) { - gid = mGid; - // TODO - } - - if (ApiHelper.SUPPORT_TRANSITION && gid != -1) { - mThumb.setTransitionName(TransitionNameFactory.getThumbTransitionName(gid)); - mTitle.setTransitionName(TransitionNameFactory.getTitleTransitionName(gid)); - mUploader.setTransitionName(TransitionNameFactory.getUploaderTransitionName(gid)); - mCategory.setTransitionName(TransitionNameFactory.getCategoryTransitionName(gid)); - } - - if (notFound) { - // TODO Show not found error - } + private void onRestore(Bundle savedInstanceState) { + mAction = savedInstanceState.getString(KEY_ACTION); + mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO); + mGid = savedInstanceState.getInt(KEY_GID); + mToken = savedInstanceState.getString(KEY_TOKEN); + mGalleryDetail = savedInstanceState.getParcelable(KEY_GALLERY_DETAIL); + mRequestId = savedInstanceState.getInt(KEY_REQUEST_ID); } @Nullable @@ -143,67 +206,500 @@ public class GalleryDetailScene extends BaseScene { View view = inflater.inflate(R.layout.scene_gallery_detail, container, false); ViewGroup main = (ViewGroup) view.findViewById(R.id.main); - mRecyclerView = (RecyclerView) main.findViewById(R.id.recycler_view); - mMainProgressView = main.findViewById(R.id.progress_view); + mMainView = (ScrollView) main.findViewById(R.id.scroll_view); + mProgressView = main.findViewById(R.id.progress_view); mFailedView = (ViewGroup) main.findViewById(R.id.tip); mFailedText = (TextView) mFailedView.getChildAt(1); - mViewTransition = new ViewTransition(mRecyclerView, mMainProgressView, mFailedView); - mRecyclerView.setAdapter(new GalleryDetailAdapter()); - mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - - mHeader = inflater.inflate(R.layout.gallery_detail_header, mRecyclerView, false); + View mainView = mMainView; + mHeader = mainView.findViewById(R.id.header); mThumb = (LoadImageView) mHeader.findViewById(R.id.thumb); mTitle = (TextView) mHeader.findViewById(R.id.title); mUploader = (TextView) mHeader.findViewById(R.id.uploader); mCategory = (TextView) mHeader.findViewById(R.id.category); + mOtherActions = (SimpleImageView) mHeader.findViewById(R.id.other_actions); + mActionGroup = (ViewGroup) mHeader.findViewById(R.id.action_card); + mDownload = mActionGroup.findViewById(R.id.download); + mRead = mActionGroup.findViewById(R.id.read); + mOtherActions.setDrawable(VectorDrawable.create(getContext(), R.drawable.ic_dots_vertical)); + RippleSalon.addRipple(mOtherActions, false); + RippleSalon.addRipple(mDownload, false); + RippleSalon.addRipple(mRead, false); + mOtherActions.setOnClickListener(this); + mDownload.setOnClickListener(this); + mRead.setOnClickListener(this); - bindView(); + mBelowHeader = mainView.findViewById(R.id.below_header); + View belowHeader = mBelowHeader; + + mInfo = belowHeader.findViewById(R.id.info); + mLanguage = (TextView) mInfo.findViewById(R.id.language); + mPages = (TextView) mInfo.findViewById(R.id.pages); + mSize = (TextView) mInfo.findViewById(R.id.size); + mPosted = (TextView) mInfo.findViewById(R.id.posted); + mFavoredTimes = (TextView) mInfo.findViewById(R.id.favoredTimes); + RippleSalon.addRipple(mInfo, false); + mInfo.setOnClickListener(this); + + mActions = belowHeader.findViewById(R.id.actions); + mRatingText = (TextView) mActions.findViewById(R.id.rating_text); + mRating = (RatingBar) mActions.findViewById(R.id.rating); + mHeartGroup = mActions.findViewById(R.id.heart_group); + mHeart = (TextView) mHeartGroup.findViewById(R.id.heart); + mHeartOutline = (TextView) mHeartGroup.findViewById(R.id.heart_outline); + mTorrent = (TextView) mActions.findViewById(R.id.torrent); + mShare = (TextView) mActions.findViewById(R.id.share); + mRate = (TextView) mActions.findViewById(R.id.rate); + RippleSalon.addRipple(mHeartGroup, false); + RippleSalon.addRipple(mTorrent, false); + RippleSalon.addRipple(mShare, false); + RippleSalon.addRipple(mRate, false); + mHeartGroup.setOnClickListener(this); + mTorrent.setOnClickListener(this); + mShare.setOnClickListener(this); + mRate.setOnClickListener(this); + + mTags = (LinearLayout) belowHeader.findViewById(R.id.tags); + mNoTags = (TextView) mTags.findViewById(R.id.no_tags); + + mComments = (LinearLayout) belowHeader.findViewById(R.id.comments); + mCommentsText = (TextView) mComments.findViewById(R.id.comments_text); + + mProgress = mainView.findViewById(R.id.progress); + + fetchGalleryDetail(); + adjustViewVisibility(); + if (mGalleryDetail == null) { + bindViewFirst(); + } else { + bindViewSecond(); + } + setTransitionName(); return view; } - private class GalleryDetailAdapter extends RecyclerView.Adapter { + @Override + public void onDestroyView() { + super.onDestroyView(); - @Override - public SimpleHolder onCreateViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - case TYPE_HEADER: - return new SimpleHolder(mHeader); - /* - case TYPE_BUTTON: - return new SimpleHolder(mButton); - case TYPE_INFO: - return new SimpleHolder(mInfo); - case TYPE_ACTION: - return new SimpleHolder(mAction); - case TYPE_TAG: - return new SimpleHolder(mTag); - case TYPE_COMMENT: - return new SimpleHolder(mComment); - case TYPE_PREVIEW: - return new SimpleHolder(mPreview); - case TYPE_PROGRESS: - return new SimpleHolder(mProgress); - */ - default: - throw new IllegalStateException("Unknown type " + viewType); + mMainView = null; + mProgressView = null; + mFailedView = null; + mFailedText = null; + + mHeader = null; + mThumb = null; + mTitle = null; + mUploader = null; + mCategory = null; + mOtherActions = null; + mActionGroup = null; + mDownload = null; + mRead = null; + mBelowHeader = null; + + mInfo = null; + mLanguage = null; + mPages = null; + mSize = null; + mPosted = null; + mFavoredTimes = null; + + mActions = null; + mRatingText = null; + mRating = null; + mHeartGroup = null; + mHeart = null; + mHeartOutline = null; + mTorrent = null; + mShare = null; + mRate = null; + + mProgress = null; + + mHeartDrawable = null; + mHeartOutlineDrawable = null; + mPopupMenu = null; + } + + private void fetchGalleryDetail() { + if (mGalleryDetail != null) { + return; + } + + int gid = getGid(); + if (gid == -1) { + return; + } + + // Get from cache + mGalleryDetail = EhApplication.getGalleryDetailCache(getContext()).get(gid); + if (mGalleryDetail != null) { + return; + } + + EhApplication application = (EhApplication) getContext().getApplicationContext(); + if (application.containGlobalStuff(mRequestId)) { + // request exist + return; + } + + // Do request + String url = getGalleryDetailUrl(); + if (url == null) { + return; + } + + EhClient.Callback callback = new GetGalleryDetailListener(getContext(), + ((StageActivity) getActivity()).getStageId(), getTag()); + mRequestId = ((EhApplication) getContext().getApplicationContext()).putGlobalStuff(callback); + EhRequest request = new EhRequest() + .setMethod(EhClient.METHOD_GET_GALLERY_DETAIL) + .setArgs(url) + .setCallback(callback); + EhApplication.getEhClient(getContext()).execute(request); + } + + private void adjustViewVisibility() { + if (mGalleryDetail != null) { + mMainView.setVisibility(View.VISIBLE); + mProgressView.setVisibility(View.GONE); + mFailedView.setVisibility(View.GONE); + + mBelowHeader.setVisibility(View.VISIBLE); + mProgress.setVisibility(View.GONE); + } else if (mGalleryInfo != null) { + mMainView.setVisibility(View.VISIBLE); + mProgressView.setVisibility(View.GONE); + mFailedView.setVisibility(View.GONE); + + mBelowHeader.setVisibility(View.GONE); + mProgress.setVisibility(View.VISIBLE); + } else { + mMainView.setVisibility(View.GONE); + if (TextUtils.isEmpty(mFailedText.getText())) { + mProgressView.setVisibility(View.VISIBLE); + mFailedView.setVisibility(View.GONE); + } else { + mProgressView.setVisibility(View.GONE); + mFailedView.setVisibility(View.VISIBLE); + } + } + } + + private void bindViewFirst() { + if (mGalleryDetail != null) { + return; + } + + if (ACTION_GALLERY_INFO.equals(mAction) && mGalleryInfo != null) { + GalleryInfo gi = mGalleryInfo; + mThumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb, true); + mTitle.setText(gi.title); + mUploader.setText(gi.uploader); + mCategory.setText(EhUtils.getCategory(gi.category)); + mCategory.setTextColor(EhUtils.getCategoryColor(gi.category)); + + // Hide actions + mActionGroup.setVisibility(View.GONE); + } + } + + private void ensureHeartDrawable() { + if (mHeartDrawable == null) { + mHeartDrawable = AnimatedVectorDrawable.create(getContext(), R.drawable.ic_heart_animated); + if (mHeartDrawable == null) { + throw new IllegalStateException("Can't parse heart drawable"); + } + mHeartDrawable.setBounds(0, 0, mHeartDrawable.getIntrinsicWidth(), + mHeartDrawable.getIntrinsicHeight()); + mHeart.setCompoundDrawables(null, mHeartDrawable, null, null); + } + + if (mHeartOutlineDrawable == null) { + mHeartOutlineDrawable = AnimatedVectorDrawable.create(getContext(), R.drawable.ic_heart_outline_animated); + if (mHeartOutlineDrawable == null) { + throw new IllegalStateException("Can't parse heart drawable"); + } + mHeartOutlineDrawable.setBounds(0, 0, mHeartOutlineDrawable.getIntrinsicWidth(), + mHeartOutlineDrawable.getIntrinsicHeight()); + mHeartOutline.setCompoundDrawables(null, mHeartOutlineDrawable, null, null); + } + } + + private void bindViewSecond() { + GalleryDetail gd = mGalleryDetail; + + if (gd == null) { + return; + } + + if (mActionGroup.getVisibility() == View.GONE) { + mActionGroup.setVisibility(View.VISIBLE); + } + + Resources resources = getContext().getResources(); + + if (TextUtils.isEmpty(mCategory.getText())) { + mThumb.load(EhCacheKeyFactory.getThumbKey(gd.gid), gd.thumb, true); + mTitle.setText(gd.title); + mUploader.setText(gd.uploader); + mCategory.setText(EhUtils.getCategory(gd.category)); + mCategory.setTextColor(EhUtils.getCategoryColor(gd.category)); + } + + mLanguage.setText(gd.language); + mPages.setText(resources.getQuantityString( + R.plurals.page_count, gd.pages, gd.pages)); + mSize.setText(gd.size); + mPosted.setText(gd.posted); + mFavoredTimes.setText(resources.getString(R.string.favored_times, gd.favoredTimes)); + + mRatingText.setText(getAllRatingText(gd.rating, gd.ratedTimes)); + mRating.setRating(gd.rating); + + ensureHeartDrawable(); + if (gd.isFavored) { + mHeart.setVisibility(View.VISIBLE); + mHeartOutline.setVisibility(View.GONE); + } else { + mHeart.setVisibility(View.GONE); + mHeartOutline.setVisibility(View.VISIBLE); + } + mTorrent.setText(resources.getString(R.string.torrent_count, gd.torrentCount)); + + bindTags(gd.tags); + bindComments(gd.comments); + } + + private void bindTags(GalleryTagGroup[] tagGroups) { + mTags.removeViews(1, mTags.getChildCount() - 1); + if (tagGroups == null || tagGroups.length == 0) { + mNoTags.setVisibility(View.VISIBLE); + return; + } else { + mNoTags.setVisibility(View.GONE); + } + + LayoutInflater inflater = getActivity().getLayoutInflater(); + int colorTag = getResources().getColor(R.color.colorPrimary); + int colorName = getResources().getColor(R.color.purple_a400); + for (int i = 0, s = tagGroups.length; i < s; i++) { + GalleryTagGroup tg = tagGroups[i]; + + LinearLayout ll = (LinearLayout) inflater.inflate(R.layout.gallery_tag_group, mTags, false); + ll.setOrientation(LinearLayout.HORIZONTAL); + mTags.addView(ll); + + TextView tgName = (TextView) inflater.inflate(R.layout.item_gallery_tag, ll, false); + ll.addView(tgName); + tgName.setText(tg.groupName); + tgName.setBackgroundColor(colorName); + + AutoWrapLayout awl = new AutoWrapLayout(getContext()); + ll.addView(awl, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + for (int j = 0, z = tg.size(); j < z; j++) { + TextView tag = (TextView) inflater.inflate(R.layout.item_gallery_tag, awl, false); + awl.addView(tag); + tag.setText(tg.getTagAt(j)); + tag.setBackgroundColor(colorTag); + } + } + } + + private void bindComments(GalleryComment[] comments) { + mComments.removeViews(0, mComments.getChildCount() - 1); + + final int maxShowCount = 2; + if (comments == null || comments.length == 0) { + mCommentsText.setText(R.string.no_comments); + return; + } else if (comments.length <= maxShowCount) { + mCommentsText.setText(R.string.no_more_comments); + } else { + mCommentsText.setText(R.string.more_comment); + } + + LayoutInflater inflater = getActivity().getLayoutInflater(); + int length = Math.min(maxShowCount, comments.length); + for (int i = 0; i < length; i++) { + GalleryComment comment = comments[i]; + View v = inflater.inflate(R.layout.item_gallery_comment, mComments, false); + mComments.addView(v, i); + ((TextView) v.findViewById(R.id.user)).setText(comment.user); + ((TextView) v.findViewById(R.id.time)).setText(ReadableTime.getTimeAgo(comment.time)); + ((TextView) v.findViewById(R.id.comment)).setText(Html.fromHtml(comment.comment)); + } + } + + private static String getRatingText(float rating, Resources resources) { + String undefine = "(´_ゝ`)"; + if (Float.isNaN(rating)) { + return undefine; + } + + int resId = 0; + switch (Math.round(rating * 2)) { + case 0: + resId = R.string.rating0; break; + case 1: + resId = R.string.rating1; break; + case 2: + resId = R.string.rating2; break; + case 3: + resId = R.string.rating3; break; + case 4: + resId = R.string.rating4; break; + case 5: + resId = R.string.rating5; break; + case 6: + resId = R.string.rating6; break; + case 7: + resId = R.string.rating7; break; + case 8: + resId = R.string.rating8; break; + case 9: + resId = R.string.rating9; break; + case 10: + resId = R.string.rating10; break; + } + + if (resId == 0) { + return undefine; + } else { + return resources.getString(resId); + } + } + + private String getAllRatingText(float rating, int ratedTimes) { + Resources resources = getResources(); + return resources.getString(R.string.rating_text, getRatingText(rating, resources), ratedTimes); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void setTransitionName() { + int gid = getGid(); + + if (gid != -1 && ApiHelper.SUPPORT_TRANSITION) { + mThumb.setTransitionName(TransitionNameFactory.getThumbTransitionName(gid)); + mTitle.setTransitionName(TransitionNameFactory.getTitleTransitionName(gid)); + mUploader.setTransitionName(TransitionNameFactory.getUploaderTransitionName(gid)); + mCategory.setTransitionName(TransitionNameFactory.getCategoryTransitionName(gid)); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + if (mAction != null) { + outState.putString(KEY_ACTION, mAction); + } + if (mGalleryInfo != null) { + outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo); + } + outState.putInt(KEY_GID, mGid); + if (mToken != null) { + outState.putString(KEY_TOKEN, mAction); + } + if (mGalleryDetail != null) { + outState.putParcelable(KEY_GALLERY_DETAIL, mGalleryDetail); + } + outState.putInt(KEY_REQUEST_ID, mRequestId); + } + + private void ensurePopMenu() { + if (mPopupMenu != null) { + return; + } + + PopupMenu popup = new PopupMenu(getContext(), mOtherActions); + mPopupMenu = popup; + popup.getMenuInflater().inflate(R.menu.gallery_detail_other_action, popup.getMenu()); + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_open_in_other_app: + // TODO + break; + } + return true; + } + }); + } + + @Override + public void onClick(View v) { + if (mOtherActions == v) { + ensurePopMenu(); + mPopupMenu.show(); + } + } + + private void onGetGalleryDetailSuccess(GalleryDetailParser.Result result) { + mGalleryDetail = result.galleryDetail; + adjustViewVisibility(); + bindViewSecond(); + } + + private void onGetGalleryDetailFailure(Exception e) { + + } + + private static class GetGalleryDetailListener implements EhClient.Callback { + + private EhApplication mApplication; + private int mStageId; + private String mSceneTag; + + public GetGalleryDetailListener(Context context, int stageId, String sceneTag) { + mApplication = (EhApplication) context.getApplicationContext(); + mStageId = stageId; + mSceneTag = sceneTag; + } + + private GalleryDetailScene getScene() { + StageActivity stage = mApplication.findStageActivityById(mStageId); + if (stage == null) { + return null; + } + SceneFragment scene = stage.findSceneByTag(mSceneTag); + if (scene instanceof GalleryDetailScene) { + return (GalleryDetailScene) scene; + } else { + return null; } } @Override - public void onBindViewHolder(SimpleHolder holder, int position) { + public void onSuccess(GalleryDetailParser.Result result) { + mApplication.removeGlobalStuff(this); + // Put gallery detail to cache + GalleryDetail gd = result.galleryDetail; + EhApplication.getGalleryDetailCache(mApplication).put(gd.gid, gd); + + // Notify success + GalleryDetailScene scene = getScene(); + if (scene != null) { + scene.onGetGalleryDetailSuccess(result); + } } @Override - public int getItemCount() { - return 1; + public void onFailure(Exception e) { + mApplication.removeGlobalStuff(this); + GalleryDetailScene scene = getScene(); + if (scene != null) { + scene.onGetGalleryDetailFailure(e); + } } @Override - public int getItemViewType(int position) { - return TYPE_HEADER; // TODO + public void onCancel() { + mApplication.removeGlobalStuff(this); } } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java index 5e59b2498..038352e8e 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java @@ -65,6 +65,9 @@ import com.hippo.ehviewer.widget.SearchBar; import com.hippo.ehviewer.widget.SearchLayout; import com.hippo.ehviewer.widget.SimpleRatingView; import com.hippo.rippleold.RippleSalon; +import com.hippo.scene.SceneApplication; +import com.hippo.scene.SceneFragment; +import com.hippo.scene.StageActivity; import com.hippo.scene.TransitionHelper; import com.hippo.utils.ApiHelper; import com.hippo.vector.VectorDrawable; @@ -129,8 +132,6 @@ public final class GalleryListScene extends BaseScene // Double click back exit private long mPressBackTime = 0; - private EhRequest mRequest; - private Animator.AnimatorListener mFabAnimatorListener = null; private boolean mHasFirstRefresh = false; @@ -178,6 +179,8 @@ public final class GalleryListScene extends BaseScene if (savedInstanceState == null) { onInit(); + } else { + onRestore(savedInstanceState); } } @@ -186,6 +189,13 @@ public final class GalleryListScene extends BaseScene handleArgs(getArguments()); } + @SuppressWarnings("WrongConstant") + private void onRestore(Bundle savedInstanceState) { + mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH); + mUrlBuilder = savedInstanceState.getParcelable(KEY_LIST_URL_BUILDER); + mState = savedInstanceState.getInt(KEY_STATE); + } + private void setSearchBarHint(Context context, SearchBar searchBar) { Resources resources = context.getResources(); Drawable searchImage = VectorDrawable.create(context, R.drawable.ic_magnify); @@ -235,22 +245,10 @@ public final class GalleryListScene extends BaseScene setNavCheckedItem(checkedItemId); } - private void restoreState(@Nullable Bundle savedInstanceState) { - if (savedInstanceState == null) { - return; - } - - mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH); - mUrlBuilder = savedInstanceState.getParcelable(KEY_LIST_URL_BUILDER); - } - @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - // Restore url builder - restoreState(savedInstanceState); - View view = inflater.inflate(R.layout.scene_gallery_list, container, false); Resources resources = getContext().getResources(); @@ -304,15 +302,15 @@ public final class GalleryListScene extends BaseScene // Update list url builder onUpdateUrlBuilder(); + // Restore state + int newState = mState; + mState = STATE_NORMAL; + setState(newState, false); + // Only refresh for the first time if (!mHasFirstRefresh) { mHasFirstRefresh = true; mHelper.firstRefresh(); - } else { - // Restore state - int newState = mState; - mState = STATE_NORMAL; - setState(newState, false); } return view; @@ -325,11 +323,6 @@ public final class GalleryListScene extends BaseScene // End animation mSearchBarMoveHelper.cancelAnimation(); - // Cancel request - if (mRequest != null) { - mRequest.cancel(); - } - mFitPaddingLayout = null; mRecyclerView = null; mSearchLayout = null; @@ -342,7 +335,6 @@ public final class GalleryListScene extends BaseScene mRightDrawable = null; mSearchBarMoveHelper = null; mFabAnimatorListener = null; - mRequest = null; } @Override @@ -353,13 +345,6 @@ public final class GalleryListScene extends BaseScene outState.putInt(KEY_STATE, mState); } - @Override - @SuppressWarnings("WrongConstant") - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - setState(savedInstanceState.getInt(KEY_STATE), false); - } - @Override public void onClick(View v) { if (mFab == v) { @@ -904,9 +889,9 @@ public final class GalleryListScene extends BaseScene String url = mUrlBuilder.build(); EhRequest request = new EhRequest(); - mRequest = request; request.setMethod(EhClient.METHOD_GET_GALLERY_LIST); - request.setCallback(new GetGalleryListListener(taskId)); + request.setCallback(new GetGalleryListListener(getContext(), + ((StageActivity) getActivity()).getStageId(), getTag(), taskId)); request.setArgs(url); mClient.execute(request); } @@ -932,38 +917,67 @@ public final class GalleryListScene extends BaseScene } } - private class GetGalleryListListener implements EhClient.Callback { + private void onGetGalleryListSuccess(GalleryListParser.Result result, int taskId) { + if (mHelper != null && mSearchBarMoveHelper != null && + mHelper.isCurrentTask(taskId)) { + mHelper.setPages(taskId, result.pages); + mHelper.onGetPageData(taskId, result.galleryInfos); + mSearchBarMoveHelper.returnSearchBarPosition(); + } + } + private void onGetGalleryListFailure(Exception e, int taskId) { + if (mHelper != null && mSearchBarMoveHelper != null && + mHelper.isCurrentTask(taskId)) { + mHelper.onGetExpection(taskId, e); + mSearchBarMoveHelper.returnSearchBarPosition(); + } + } + + private static class GetGalleryListListener implements EhClient.Callback { + + private SceneApplication mApplication; + private int mStageId; + private String mSceneTag; private int mTaskId; - public GetGalleryListListener(int taskId) { + public GetGalleryListListener(Context context, int stageId, String sceneTag, int taskId) { + mApplication = (SceneApplication) context.getApplicationContext(); + mStageId = stageId; + mSceneTag = sceneTag; mTaskId = taskId; } + private GalleryListScene getScene() { + StageActivity stage = mApplication.findStageActivityById(mStageId); + if (stage == null) { + return null; + } + SceneFragment scene = stage.findSceneByTag(mSceneTag); + if (scene instanceof GalleryListScene) { + return (GalleryListScene) scene; + } else { + return null; + } + } + @Override public void onSuccess(GalleryListParser.Result result) { - if (mHelper != null && mHelper.isCurrentTask(mTaskId)) { - mRequest = null; - mHelper.setPages(mTaskId, result.pages); - mHelper.onGetPageData(mTaskId, result.galleryInfos); - mSearchBarMoveHelper.returnSearchBarPosition(); + GalleryListScene scene = getScene(); + if (scene != null) { + scene.onGetGalleryListSuccess(result, mTaskId); } } @Override public void onFailure(Exception e) { - if (mHelper != null && mHelper.isCurrentTask(mTaskId)) { - mRequest = null; - mHelper.onGetExpection(mTaskId, e); - mSearchBarMoveHelper.returnSearchBarPosition(); + GalleryListScene scene = getScene(); + if (scene != null) { + scene.onGetGalleryListFailure(e, mTaskId); } } @Override - public void onCancel() { - if (mHelper != null && mHelper.isCurrentTask(mTaskId)) { - mRequest = null; - } - } + public void onCancel() {} } } diff --git a/app/src/main/java/com/hippo/ehviewer/widget/DividerView.java b/app/src/main/java/com/hippo/ehviewer/widget/DividerView.java new file mode 100644 index 000000000..5fe819bc2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/DividerView.java @@ -0,0 +1,84 @@ +/* + * 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.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import com.hippo.ehviewer.R; + +public class DividerView extends View { + + private Paint mPaint; + private Rect mRect; + private int mDividerWidth; + private int mDividerHeight; + + public DividerView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public DividerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DividerView); + int color = a.getColor(R.styleable.DividerView_dividerColor, Color.BLACK); + mDividerWidth = a.getDimensionPixelOffset(R.styleable.DividerView_dividerWidth, 0); + mDividerHeight = a.getDimensionPixelOffset(R.styleable.DividerView_dividerHeight, 0); + a.recycle(); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mPaint.setColor(color); + mRect = new Rect(); + } + + @Override + protected int getSuggestedMinimumWidth() { + return Math.max((getPaddingLeft() + getPaddingRight() + mDividerWidth), super.getSuggestedMinimumWidth()); + } + + @Override + protected int getSuggestedMinimumHeight() { + return Math.max((getPaddingTop() + getPaddingBottom() + mDividerHeight), super.getSuggestedMinimumHeight()); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + Rect rect = mRect; + rect.set(getPaddingLeft(), getPaddingTop(), + getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); + if (!rect.isEmpty()) { + canvas.drawRect(rect, mPaint); + } + } +} diff --git a/app/src/main/java/com/hippo/scene/SceneApplication.java b/app/src/main/java/com/hippo/scene/SceneApplication.java index 49efd79b3..d032182f7 100644 --- a/app/src/main/java/com/hippo/scene/SceneApplication.java +++ b/app/src/main/java/com/hippo/scene/SceneApplication.java @@ -32,6 +32,15 @@ public class SceneApplication extends Application { stage.onRegister(id); } + void registerStageActivity(StageActivity stage, int id) { + if (mStageMap.indexOfKey(id) >= 0) { + throw new IllegalStateException("The id exists: " + id); + } + + mStageMap.put(id, stage); + stage.onRegister(id); + } + void unregisterStageActivity(int id) { int index = mStageMap.indexOfKey(id); if (index >= 0) { diff --git a/app/src/main/java/com/hippo/scene/StageActivity.java b/app/src/main/java/com/hippo/scene/StageActivity.java index 3f0e68a57..f78c0e8e9 100644 --- a/app/src/main/java/com/hippo/scene/StageActivity.java +++ b/app/src/main/java/com/hippo/scene/StageActivity.java @@ -33,6 +33,7 @@ public abstract class StageActivity extends AppCompatActivity { private static final String TAG = StageActivity.class.getSimpleName(); + private static final String KEY_STAGE_ACTIVITY_STAGE_ID = "stage_activity_stage_id"; private static final String KEY_STAGE_ACTIVITY_SCENE_TAG_LIST = "stage_activity_scene_tag_list"; private static final String KEY_STAGE_ACTIVITY_NEXT_ID = "stage_activity_next_id"; @@ -47,7 +48,18 @@ public abstract class StageActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((SceneApplication) getApplicationContext()).registerStageActivity(this); + + if (savedInstanceState != null) { + mStageId = savedInstanceState.getInt(KEY_STAGE_ACTIVITY_STAGE_ID, IntIdGenerator.INVALID_ID); + mSceneTagList = savedInstanceState.getStringArrayList(KEY_STAGE_ACTIVITY_SCENE_TAG_LIST); + mIdGenerator.setNextId(savedInstanceState.getInt(KEY_STAGE_ACTIVITY_NEXT_ID)); + } + + if (mStageId == IntIdGenerator.INVALID_ID) { + ((SceneApplication) getApplicationContext()).registerStageActivity(this); + } else { + ((SceneApplication) getApplicationContext()).registerStageActivity(this, mStageId); + } } @Override @@ -61,7 +73,6 @@ public abstract class StageActivity extends AppCompatActivity { } protected void onUnregister() { - mStageId = IntIdGenerator.INVALID_ID; } public int getStageId() { @@ -250,14 +261,8 @@ public abstract class StageActivity extends AppCompatActivity { @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); + outState.putInt(KEY_STAGE_ACTIVITY_STAGE_ID, mStageId); outState.putStringArrayList(KEY_STAGE_ACTIVITY_SCENE_TAG_LIST, mSceneTagList); outState.putInt(KEY_STAGE_ACTIVITY_NEXT_ID, mIdGenerator.nextId()); } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - mSceneTagList = savedInstanceState.getStringArrayList(KEY_STAGE_ACTIVITY_SCENE_TAG_LIST); - mIdGenerator.setNextId(savedInstanceState.getInt(KEY_STAGE_ACTIVITY_NEXT_ID)); - } } diff --git a/app/src/main/java/com/hippo/scene/StageLayout.java b/app/src/main/java/com/hippo/scene/StageLayout.java index 59ffd7258..ef3543c44 100644 --- a/app/src/main/java/com/hippo/scene/StageLayout.java +++ b/app/src/main/java/com/hippo/scene/StageLayout.java @@ -125,7 +125,7 @@ public class StageLayout extends FrameLayout { protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ArrayList disappearingChildren = mSuperDisappearingChildren; - if (mDoTrick && disappearingChildren != null && disappearingChildren.size() > 0) { + if (mDoTrick) { if (child == mDumpView) { boolean more = false; for (int i = disappearingChildren.size() - 1; i >= 0; i--) { diff --git a/app/src/main/java/com/hippo/text/Html.java b/app/src/main/java/com/hippo/text/Html.java new file mode 100644 index 000000000..aabc9a0f3 --- /dev/null +++ b/app/src/main/java/com/hippo/text/Html.java @@ -0,0 +1,1087 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * 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.text; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.text.Editable; +import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.AlignmentSpan; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.ParagraphStyle; +import android.text.style.QuoteSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.SubscriptSpan; +import android.text.style.SuperscriptSpan; +import android.text.style.TextAppearanceSpan; +import android.text.style.TypefaceSpan; +import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; + +import com.hippo.ehviewer.R; + +import org.ccil.cowan.tagsoup.HTMLSchema; +import org.ccil.cowan.tagsoup.Parser; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +import java.io.IOException; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Locale; + +/** + * This class processes HTML strings into displayable styled text. + * Not all HTML tags are supported. + */ +public class Html { + + static Resources sResources; + + public static void initialize(Context context) { + sResources = context.getResources(); + } + + /** + * Retrieves images for HTML <img> tags. + */ + public interface ImageGetter { + /** + * This method is called when the HTML parser encounters an + * <img> tag. The source argument is the + * string from the "src" attribute; the return value should be + * a Drawable representation of the image or null + * for a generic replacement image. Make sure you call + * setBounds() on your Drawable if it doesn't already have + * its bounds set. + */ + Drawable getDrawable(String source); + } + + /** + * Is notified when HTML tags are encountered that the parser does + * not know how to interpret. + */ + public interface TagHandler { + /** + * This method will be called whenn the HTML parser encounters + * a tag that it does not know how to interpret. + */ + void handleTag(boolean opening, String tag, + Editable output, XMLReader xmlReader); + } + + private Html() { } + + /** + * Returns displayable styled text from the provided HTML string. + * Any <img> tags in the HTML will display as a generic + * replacement image which your program can then go through and + * replace with real images. + * + *

This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. + */ + public static Spanned fromHtml(String source) { + return fromHtml(source, null, null); + } + + /** + * Lazy initialization holder for HTML parser. This class will + * a) be preloaded by the zygote, or b) not loaded until absolutely + * necessary. + */ + private static class HtmlParser { + private static final HTMLSchema schema = new HTMLSchema(); + } + + /** + * Returns displayable styled text from the provided HTML string. + * Any <img> tags in the HTML will use the specified ImageGetter + * to request a representation of the image (use null if you don't + * want this) and the specified TagHandler to handle unknown tags + * (specify null if you don't want this). + * + *

This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. + */ + public static Spanned fromHtml(String source, ImageGetter imageGetter, + TagHandler tagHandler) { + Parser parser = new Parser(); + try { + parser.setProperty(Parser.schemaProperty, HtmlParser.schema); + } catch (org.xml.sax.SAXNotRecognizedException e) { + // Should not happen. + throw new RuntimeException(e); + } catch (org.xml.sax.SAXNotSupportedException e) { + // Should not happen. + throw new RuntimeException(e); + } + + HtmlToSpannedConverter converter = + new HtmlToSpannedConverter(source, imageGetter, tagHandler, + parser); + return converter.convert(); + } + + /** + * Returns an HTML representation of the provided Spanned text. A best effort is + * made to add HTML tags corresponding to spans. Also note that HTML metacharacters + * (such as "<" and "&") within the input text are escaped. + * + * @param text input text to convert + * @return string containing input converted to HTML + */ + public static String toHtml(Spanned text) { + StringBuilder out = new StringBuilder(); + withinHtml(out, text); + return out.toString(); + } + + /** + * Returns an HTML escaped representation of the given plain text. + */ + public static String escapeHtml(CharSequence text) { + StringBuilder out = new StringBuilder(); + withinStyle(out, text, 0, text.length()); + return out.toString(); + } + + private static void withinHtml(StringBuilder out, Spanned text) { + int len = text.length(); + + int next; + for (int i = 0; i < text.length(); i = next) { + next = text.nextSpanTransition(i, len, ParagraphStyle.class); + ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class); + String elements = " "; + boolean needDiv = false; + + for(int j = 0; j < style.length; j++) { + if (style[j] instanceof AlignmentSpan) { + Layout.Alignment align = + ((AlignmentSpan) style[j]).getAlignment(); + needDiv = true; + if (align == Layout.Alignment.ALIGN_CENTER) { + elements = "align=\"center\" " + elements; + } else if (align == Layout.Alignment.ALIGN_OPPOSITE) { + elements = "align=\"right\" " + elements; + } else { + elements = "align=\"left\" " + elements; + } + } + } + if (needDiv) { + out.append("

"); + } + + withinDiv(out, text, i, next); + + if (needDiv) { + out.append("
"); + } + } + } + + private static void withinDiv(StringBuilder out, Spanned text, + int start, int end) { + int next; + for (int i = start; i < end; i = next) { + next = text.nextSpanTransition(i, end, QuoteSpan.class); + QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class); + + for (QuoteSpan quote : quotes) { + out.append("
"); + } + + withinBlockquote(out, text, i, next); + + for (QuoteSpan quote : quotes) { + out.append("
\n"); + } + } + } + + private static String getOpenParaTagWithDirection(Spanned text, int start, int end) { + //final int len = end - start; + //final byte[] levels = new byte[len]; + //final char[] buffer = new char[len]; + //TextUtils.getChars(text, start, end, buffer, 0); + + //int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len, + // false /* no info */); + //switch(paraDir) { + // case Layout.DIR_RIGHT_TO_LEFT: + // return "

"; + // case Layout.DIR_LEFT_TO_RIGHT: + // default: + // return "

"; + //} + return "

"; + } + + private static void withinBlockquote(StringBuilder out, Spanned text, + int start, int end) { + out.append(getOpenParaTagWithDirection(text, start, end)); + + int next; + for (int i = start; i < end; i = next) { + next = TextUtils.indexOf(text, '\n', i, end); + if (next < 0) { + next = end; + } + + int nl = 0; + + while (next < end && text.charAt(next) == '\n') { + nl++; + next++; + } + + if (withinParagraph(out, text, i, next - nl, nl, next == end)) { + /* Paragraph should be closed */ + out.append("

\n"); + out.append(getOpenParaTagWithDirection(text, next, end)); + } + } + + out.append("

\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 0000000000000000000000000000000000000000..cd5e5ea91b36da5e7df2e90d3bf857a3b283595b GIT binary patch literal 359 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~aez;VE0AWG)@Umty7N}2(Se1KC&U|`TRGA zmk)i}sMYZQi^t{DIcC2z*J^Q0ZSitaT{tPgbA?KXkQOJ9dq~exK8b1Z6mJ%W*&)ge zky^YPytguzt#o%#5Oq-4DXX!>&Oz~O3(K#}K541+o1JqMiguldY&)U3C%F5g$Z^#; zk?)DO{r+b$`%5QU|M)3icR}Ob2TQ9z3rdY;PaMfLyzN)U+VE_xvJ%qy3X?z5k9G54_*%@0LB@0heJWy5;&z6F7%eYf>D-^}?oHqQ3x z7RLyVMgfkdMVDf&wsX4*FewU%HEmuQ^fL3<|Fd(=1bn;;||kDRR+um-$i z05h1tq~wL8?PU(9wHdZ<|wL{_q%;M^V0kv-hkwN f2eZgTe~DWM4fR&tFi literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c5f646a5c986a094dd6d1fa18b62367cc56a872b GIT binary patch literal 655 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~aez;VE0AWG)EFzb9?{v-XqH?9bW@IlR6*IUcQ^KOt*zU!|6?_?0y?uctfQDXTiT z=g&=#dqzR^pUx(oI9@Lu@cs)=Z-Pd^q@{hXul)VAqFgt)r9R88mOs+9h{Y-M;f>F; zqisHMfkS>(V%f_qsLtQM( zTh@F{{CT9$#QFW_h{j1X)Ml{x1AQSgJt}SaL94sWskZYTFy1(BVmmMKjU_|G`t^0v z>qI1(U3LWC;=W$rzG%t%m@U#>lO~rc>|)ISdN29NntVp1k9(&U-c5ITRw(kQY_ru@ zk*f#R{}5r7_->Q`?3eZJ9YTR2vzK08@!hgy=DQ$f>(wvq>&4|6(*ms}ot!*3f>M^J LtDnm{r-UW|ghm?8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6040570abcf0b377598febc761713cea348c6009 GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-s#sNMdu0Wb$S_et!`C6~ZKwW|* zL4Lsu^Zy$ZtiP|&@cz6*fk0y oZH8;Lj`IXvtK;!a5b0rJnDmroo1SEU0MK0wp00i_>zopr0PVhPP5=M^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..aea500f9977f83d7b0573c8f19dac5df1d0fdd13 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt>z*!-Ar*{oXGn`2b`Wv7o;mkX z$Z{*&FFc`|d2&ikxSXZ(EU&Aj?#=GIL$Q>U@F%RS+}A|6@AQ4mny!~DCY zhHb0jbHVcbE8z|M1s$&EyRf(%uy%1^Ig(it#HXR4&91{4%qKC0`5TZjc*@{Eqv*gA znE;0{z6Zg}V-75kIk4PsC&TZshV8bc`{Wb;+Whus3uxK?i+T0mq>T}8IO{v!Z8-n{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2b11897230daee32f392855bf51e3336958e2c87 GIT binary patch literal 493 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-s#sNMdu0Wb$S_et!`C6~ZKwW|* zL4Lsu3IYl9&;NfP(BI&2e}BPxgWYLdRtyY`5uPrNAr*{wr@HqYHQ;F5*sECR{jUC| z<~4 z=UIKj{U6M0+AE&+oLRccM~rFFDNf^su4+>8ECF-%c$7~p`MU1K4r4_|UJif#c8gh= z%))6~FD+Tz%H*bo?105!XUfjIfWg?^JkqxOgtVJ3gT^DBb^1ZTD)IKkOQ~6zc zT<*7%CJ}%2moJ~?QvUyV*3E;@4nNYL?e*+K`ns=1S*_p7jN9Hkn`bJ$tWM@%_=~TT zH%wf(?y9Y0pzjL%C68`-F*9-=IyP-^oj0s4tpyB+ob(sys5nHdV4l)irNDdRpZ1=Ldhz%9kN!q&`JLDCeR0KpvA6p^ z@6n0>apd2VOx^eDw_^%Ep0X)>vvJ)nt2~>4n)TZH`)5`ct~+eZl$p5idLrGcYi+c)B=-R4~3h-`VJ_$ip01tK*b+xqjdz*jf72M~2xBFo2`v?2J7u*+p zC%)r7+xOar-*$&;=Rf!-`R+H<_u5tv@ABW4-|q_UFMN6R5&LbEyM!usGzv#m2*ED4S(cE?%%^=9H;A0oU)Ngq;u;XxF))8~ZMdrPArchEp00i_ I>zopr00OQTn{D6k@M_URPF)%PK@^onCfAy_@ ze~}7QDmfc<|9xJMuHp&p&kycDKA85d zUY?H^*97cMxaJ+9F7j^C1K*5WqKC_8F7P)G3|)3%hrvASo|Pw9PaXOt^Yq;7%c5N| z%htT!y{=@c>Z+ZMp)(gQ7kjF@Yo_7c_}y=Qx@`XE$n@ibjFzxb#KMESH?ci#QIo#X zWIUnb;)K@HN#gVJ61eAOF4+BTs>+0h=niwK563SzG@mlAnA)@3`seBsCoi!dSLb)u zo4hM0Ltf!a)1L(OxurA2HH2QIEOm+UX12(2c&2_>W44^2h3FZNgOSJGKK+<=DUs*x z8{X*~gWtZGVrj-GTlOmNUQq&niDN{@_GF1AQ_mh?di$)+F`Q?b5k3=s#&Fu|ZS$AtHEc9?damcaS;SL3@4-;ActxprWeem+} zLi+%LEyelim;SNLw)Pc2o;P2iK3az-`ofMs)(Ns(7U<4!by{OkSZ#9pLGUB~>~}67 h9!c%})y&Rs$6WR&qgcuLrWPm%c)I$ztaD0e0sx>>Y|#J! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a8da0117a1fbf7b37b3b1b9618e5ee3b39312de4 GIT binary patch literal 570 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=j01c^T!A#hv<{Nc^R-@+fw}}s zg8YIR=Kps{=->ZdAi$vE{C$P>1};6h#5oz#Z})f4=`S zw|;kP@tzeYb~gnJx$S#k8#nKHB?JZXw#?!%70gn);`Qr5aYqrn) z-FVsQ$?dg!`eowH?(4)YKNmc4{pP@a*$-y%TQ8jye%^3?^YYE^36DR!*;cFh*M!|% z-pFHnGDRePk8bA~Hfx56 zAjz<2+vn*S+s5YG8a=+l>u`OhzIua-$%lQ7OH3NgPdKhvw@uvtjMS8_ZOtK@SyyK9 hIoU?Q102ZEXRP?FptJMoK|@d?@O1TaS?83{1OT~Q^a=m~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..487bcbab29ab2f461bd4920fa072b339e0d637eb GIT binary patch literal 538 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=j01c^T!A#hv<{Nc^R-@+fw}}s zg8YIR=ARe%->>lAA;DmOL%{m`1#|tGq8Jz$TRmMILn;{GUUw9_t-#}!sQHJX)A{M| z`P*KvyCr7&*=TV&V?7)5u~n;n9s6@JLXK0l2o3!WYOh?v`1wSlobL?xMU`6;Dx27=!+M2K{Dd7BhbKnc;4qAh$PBPIrbo?=#LB?tGtLw1IS8 zwBeuOF8l1F@Ux5D&jimF*WO|Nv26RlMSmFsH0)d7UuHaIzV@)b#(uvU8$ZX3{ylHS zyX0#+|8E(M424{&6PXJZG=?l-5?$fIsuj@4b%g0E|E&6({&lgk z8eC{=Je06Nnr#*@TTyc(8&8QO&z8gm1`;pY8q=B^?<6c>WR7KH&Xv&MVcV6s;AVn? zxkSbRhv^3$q}jNd8*v(C&DLrp{aoRE{O9xqYma;r)Tlpq$jHGvbFIjPQq`x^n2pYV zpVPSJdG$GmZBFe+xA{!C`OV_Ok8P7zLBK0@@2Rr|UV5Ah1x2!_tDnm{r-UW|zh>DX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f5f039c3dd241c61c2264d24bdae266d118ac0f3 GIT binary patch literal 1196 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=j01c^T!A#hv<{Nc^R-@+fw}}s zg8YIR3=$gF?|=XQe1Jp0zd^zgu0KmvtSkI^Loy}hL{gjWyrVqTHr`LDR*FgU1+bLp8C;mqpRqoEgZNW1>KGQgC zxE?r7c+l)nvf=a0xPXaG!HqXAxUbytF=N`x(;3g%ttzZ1%i12|bYYNJy;$s!cs|>D z{=7#A_&zW_bc}wYc4lvsO)8tc{GXPYdKr<6|6X3U%4*T%W1X=wf|))WvbJ7+bR+GS z&f#|n_PTv_ZmSE+FR(Ukd~%NE$b;KQt?%Dvn$+YixZ==Bu?q}y?!-ifM?2*F-p6|) ze(vuy-4&}2$BUkt#&hz(QV!XW-I@x4CFdT*H=PujVD1^XjJ;$V%ao6s+JxU2EY0(H zEf;cH?4Y61X(5w|e3KLvUUAO&CYT><7 z&Gl6qPTF6%tSuF`sbQ7$z9Y9zx-h9U32yZGZz?l_8mLy z<@~tgjbF5=epfr2r1(tu!@2c(J zBwSvmIALbP*PD6o%SFyRTv`}w7arAgI@>R#Xu0bP20vvhh4$APf9_Se-OfMFp#z&0;{*n?|7FX< + + + + + + + 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