public class Like {
    @SerializedName("obj_type") public String objType;      // 点赞对象类型
    @SerializedName("obj_id") public int objId;             // 点赞对象 id
    @SerializedName("count") public int count;              // 点赞数
} public class Topic {
    @SerializedName("id") public int id;                                            // 话题 id
    @SerializedName("title") public String title;                                   // 标题
    @SerializedName("created_at") public Date createdAt;                            // 创建时间
    @SerializedName("updated_at") public Date updatedAt;                            // 更新时间
    @SerializedName("replied_at") public Date repliedAt;                            // 最近一次回复时间
    @SerializedName("replies_count") public int repliesCount;                       // 回复总数量
    @SerializedName("node_name") public String nodeName;                            // 节点名称
    @SerializedName("node_id") public int nodeId;                                   // 节点 id
    @SerializedName("last_reply_user_id") public int lastReplyUserId;               // 最近一次回复的用户 id
    @SerializedName("last_reply_user_login") public String lastReplyUserLogin;      // 最近一次回复的用户登录名
    @SerializedName("user") public User user;                                       // 创建该话题的用户
    @SerializedName("deleted") public boolean deleted;                              // 是否已删除
    @SerializedName("excellent") public boolean excellent;                          // 是否加精
    @SerializedName("abilities") public Abilities abilities;                        // 当前用户对该话题拥有的权限
    @SerializedName("body") public String body;                                     // 话题详情(Markdown)
    @SerializedName("body_html") public String bodyHtml;                            // 话题详情(HTML)
    @SerializedName("hits") public int hits;                                        // 浏览次数
    @SerializedName("likes_count") public int likesCount;                           // 点赞数
    @SerializedName("suggested_at") public Date suggestedAt;                        // 置顶(推荐)时间
    @SerializedName("followed") public Boolean followed;                            // 是否关注
    @SerializedName("liked") public Boolean liked;                                  // 是否喜欢
    @SerializedName("favorited") public Boolean favorited;                          // 是否收藏
} public class LoginEvent {
} public class DiyCodeWebViewClient extends WebViewClient {

    private Context context;
    private ACache aCache;

    public DiyCodeWebViewClient(Context context) {
        this.context = context;
        aCache = ACache.get(context);
    }

    // 监听加载了哪些资源
    @Override
    public void onLoadResource(WebView view, final String url) {
        // 是图片就缓存
        if (isImageSuffix(url) || isGifSuffix(url)) {
            Glide.with(context)
                 .load(url)
                 .downloadOnly(new SimpleTarget<File>() {
                     @Override
                     public void onResourceReady(File resource, GlideAnimation<? super File> glideAnimation) {
                         aCache.put(url, resource.getAbsolutePath());
                     }
                 });
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        String url = request.getUrl().toString();
        WebResourceResponse response = getWebResourceResponse(url);
        if (response != null) return response;
        return super.shouldInterceptRequest(view, request);
    }

    @SuppressWarnings("deprecation")
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        WebResourceResponse response = getWebResourceResponse(url);
        if (response != null) return response;
        return super.shouldInterceptRequest(view, url);
    }

    /**
     * 获取本地资源
     */
    @Nullable
    private WebResourceResponse getWebResourceResponse(String url) {
        try {
            // 如果是图片且本地有缓存
            if (isImageSuffix(url) || isGifSuffix(url)) {
                String cachePath = aCache.getAsString(url);
                if (!isEmpty(cachePath)) {
                    File cacheFile = new File(cachePath);
                    if (cacheFile.exists()) {
                        return new WebResourceResponse(getMimeType(url), "base64", new FileInputStream(cacheFile));
                    }
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
} public interface OnWebImageClickListener {

    void onClick(String imageUrl);
} public class ImageActivity extends BaseActivity {
    private static final String EXTRA_IMAGE_URL = "extra_image_url";

    @BindView(R.id.photo_view) PhotoView photoView;
    @BindView(R.id.progress_bar) ProgressBar progressBar;

    public static void launch(Context context, String imageUrl) {
        Intent intent = new Intent(context, ImageActivity.class);
        intent.putExtra(EXTRA_IMAGE_URL, imageUrl);
        context.startActivity(intent);
    }

    @Override
    protected int provideContentViewId() {
        return R.layout.activity_image;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        photoView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });

        ImageLoadHelper.loadImage(this, getIntent().getStringExtra(EXTRA_IMAGE_URL), photoView, new RequestListener<String, GlideDrawable>() {
            @Override
            public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
                progressBar.setVisibility(View.INVISIBLE);
                return false;
            }

            @Override
            public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
                progressBar.setVisibility(View.INVISIBLE);
                return false;
            }
        });
    }
} public class LoginActivity extends MVPBaseActivity<LoginContract.View, LoginPresenter> implements LoginContract.View {

    @BindView(R.id.username_hint) TextInputLayout usernameHint;
    @BindView(R.id.edit_username) TextInputEditText editUsername;
    @BindView(R.id.password_hint) TextInputLayout passwordHint;
    @BindView(R.id.edit_password) TextInputEditText editPassword;

    private ProgressDialog progressDialog;

    public static void launch(Context context) {
        context.startActivity(new Intent(context, LoginActivity.class));
    }

    @Override
    protected int provideContentViewId() {
        return R.layout.activity_sign_in;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        progressDialog = new ProgressDialog(this);
        progressDialog.setCancelable(false);
        progressDialog.setMessage("请稍后...");

        editUsername.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                if (!isEmpty(s)) {
                    usernameHint.setErrorEnabled(false);
                }
            }
        });

        editPassword.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                if (!isEmpty(s)) {
                    passwordHint.setErrorEnabled(false);
                }
            }
        });
    }

    @OnClick({ R.id.button_close, R.id.button_sign_in })
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button_close:
                finish();
                break;
            case R.id.button_sign_in:
                presenter.login();
                break;
        }
    }

    @Override
    public String getUsername() {
        return trim(editUsername.getText());
    }

    @Override
    public String getPassword() {
        return trim(editPassword.getText());
    }

    @Override
    public void nameError() {
        usernameHint.setError(getString(R.string.username_non_null));
    }

    @Override
    public void passwordError() {
        passwordHint.setError(getString(R.string.password_non_null));
    }

    @Override
    public void LoginSuccessful() {
        EventBus.getDefault().post(new LoginEvent());
        finish();
    }

    @Override
    public void showProgress(boolean show) {
        if (show) {
            progressDialog.show();
        } else {
            progressDialog.dismiss();
        }
    }
} public class RepliesFragment extends ListBaseFragment<RepliesContract.View, RepliesPresenter, Reply> implements RepliesContract.View {

    private static final String FEED_TYPE = "feed_type";
    private static final String FEED_ID = "feed_id";

    private String feedType;
    private int feedId;

    public static RepliesFragment newInstance() {
        Bundle args = new Bundle();
        RepliesFragment fragment = new RepliesFragment();
        fragment.setArguments(args);
        return fragment;
    }

    public static RepliesFragment newInstance(String feedType, int feedId) {
        Bundle args = new Bundle();
        args.putString(FEED_TYPE, feedType);
        args.putInt(FEED_ID, feedId);
        RepliesFragment fragment = new RepliesFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    protected int provideViewLayoutId() {
        return R.layout.layout_recyclerview;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {

        Bundle arguments = getArguments();
        feedType = arguments.getString(FEED_TYPE);
        feedId = arguments.getInt(FEED_ID);

        super.onCreate(savedInstanceState);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
    }

    @NonNull
    @Override
    public BaseRecyclerListAdapter<Reply, ?> createAdapter() {
        return feedType != null ? new RepliesListAdapter(feedType) : new MyRepliesListAdapter();
    }

    @NonNull
    @Override
    public RecyclerView.LayoutManager createLayoutManager() {
        return new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
    }

    @Override
    protected void loadData() {
        if (feedType != null) {
            presenter.fetchRepliesList(feedType, feedId, clear ? 0 : adapter.getItemCount());
        } else {
            presenter.fetchMyRepliesList(clear ? 0 : adapter.getItemCount());
        }
    }

    @Override
    public void updateRepliesList(List<Reply> replies) {
        updateList(replies);
        updateViewState();
    }
} public class CreateReplyActivity extends MVPBaseActivity<CreateReplyContract.View, CreateReplyPresenter> implements CreateReplyContract.View {

    private static final String EXTRA_PREFIX = "extra_prefix";
    private static final String EXTRA_FEED_TYPE = "extra_feed_type";
    private static final String EXTRA_FEED_ID = "extra_feed_id";

    @BindView(R.id.edit_content) AppCompatEditText editContent;

    private ProgressDialog progressDialog;

    public static void launch(Context context, String prefix, String feedType, int feedId) {
        Intent intent = new Intent(context, CreateReplyActivity.class);
        intent.putExtra(EXTRA_PREFIX, prefix);
        intent.putExtra(EXTRA_FEED_TYPE, feedType);
        intent.putExtra(EXTRA_FEED_ID, feedId);
        context.startActivity(intent);
    }

    @Override
    protected int provideContentViewId() {
        return R.layout.activity_create_reply;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        progressDialog = new ProgressDialog(this);
        progressDialog.setCancelable(false);
        progressDialog.setMessage("请稍后...");

        String prefix = getIntent().getStringExtra(EXTRA_PREFIX);
        editContent.setText(prefix);
        editContent.setSelection(prefix.length());
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.send, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.action_send) {
            Intent intent = getIntent();
            presenter.newReply(
                    intent.getStringExtra(EXTRA_FEED_TYPE),
                    intent.getIntExtra(EXTRA_FEED_ID, -1)
            );
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public String getContent() {
        return trim(editContent.getText());
    }

    @Override
    public void contentError() {
        editContent.setError(getString(R.string.content_non_null));
    }

    @Override
    public void showProgress(boolean show) {
        if (show) {
            progressDialog.show();
        } else {
            progressDialog.dismiss();
        }
    }

    @Override
    public void createSuccessful() {
        finish();
    }
} public class WebActivity extends BaseActivity {

    private static final String EXTRA_URL = "extra_url";

    @BindView(R.id.web_view) WebView webView;
    @BindView(R.id.progressbar) NumberProgressBar progressbar;

    public static void launch(Context context, String url) {
        Intent intent = new Intent(context, WebActivity.class);
        intent.putExtra(EXTRA_URL, url);
        context.startActivity(intent);
    }

    @Override
    protected int provideContentViewId() {
        return R.layout.activity_web;
    }

    @Override
    @SuppressLint("SetJavaScriptEnabled")
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setLoadWithOverviewMode(true);
        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
        settings.setSupportZoom(true);
        settings.setAppCacheEnabled(true);
        settings.setDomStorageEnabled(true);
        webView.setWebViewClient(new WebClient());
        webView.setWebChromeClient(new ChromeClient());

        webView.loadUrl(getIntent().getStringExtra(EXTRA_URL));
    }

    @Override
    public void onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    protected void onDestroy() {
        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled(false);
        webView.destroy();

        super.onDestroy();
    }

    private static class WebClient extends WebViewClient {

        @SuppressWarnings("deprecation")
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (url != null) {
                view.loadUrl(url);
            }
            return true;
        }
    }

    private class ChromeClient extends WebChromeClient {

        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            super.onProgressChanged(view, newProgress);
            progressbar.setProgress(newProgress);
            if (newProgress == 100) {
                progressbar.setVisibility(View.INVISIBLE);
            } else {
                progressbar.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public void onReceivedTitle(WebView view, String title) {
            super.onReceivedTitle(view, title);
            setToolBarTitle(title);
        }
    }
} public abstract class MVPBaseActivity<V extends BaseView, T extends BasePresenterImpl<V>> extends BaseActivity implements BaseView {
    protected T presenter;

    @SuppressWarnings("unchecked")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        presenter = getInstance(this, 1);
        presenter.attachView((V) this);

        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onDestroy() {
        if (presenter != null) {
            presenter.detachView();
        }
        super.onDestroy();
    }

    @Override
    public Context getContext() {
        return this;
    }

    @Override
    public <T> LifecycleTransformer<T> bindToLife() {
        return this.<T>bindToLifecycle();
    }

    @SuppressWarnings("unchecked")
    public <T> T getInstance(Object o, int i) {
        try {
            return ((Class<T>) ((ParameterizedType) (o.getClass().getGenericSuperclass())).getActualTypeArguments()[i]).newInstance();
        } catch (InstantiationException | IllegalAccessException | ClassCastException e) {
            e.printStackTrace();
        }
        return null;
    }
} public class ImageLoadHelper {

    public static void loadImage(Context context, String url, ImageView imageView, @Nullable RequestListener<String, GlideDrawable> listener) {
        Glide.with(context).load(url).diskCacheStrategy(DiskCacheStrategy.SOURCE).listener(listener).into(imageView);
    }

    public static void loadAvatar(Context context, String url, ImageView imageView) {
        Glide.with(context).load(url).placeholder(R.mipmap.default_avatar).dontAnimate().into(imageView);
    }
} public class UserHelper {

    private static final String TOKEN = "token";
    private static final String USER = "user";

    private static SharedPreferences prefs;

    public static void init(Context context) {
        prefs = new SecurePreferences(context.getApplicationContext());
    }

    public static void saveToken(Token token) {
        prefs.edit().putString(TOKEN, GSON.toJson(token)).apply();
    }

    public static Token getToken() {
        String tokenJson = prefs.getString(TOKEN, null);
        if (tokenJson != null) {
            return GSON.fromJson(tokenJson, Token.class);
        }
        return null;
    }

    public static boolean isLogin() {
        return null != getToken();
    }

    public static boolean hasUser() {
        return null != getUser();
    }

    public static void saveUser(User user) {
        prefs.edit().putString(USER, GSON.toJson(user)).apply();
    }

    public static User getUser() {
        String userJson = prefs.getString(USER, null);
        if (userJson != null) {
            return GSON.fromJson(userJson, User.class);
        }
        return null;
    }

    public static void logout() {
        prefs.edit().remove(USER).remove(TOKEN).apply();
    }
}
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v7.widget.AppCompatImageButton;
import android.support.v7.widget.AppCompatTextView;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import com.kennyc.view.MultiStateView;
import com.makeramen.roundedimageview.RoundedImageView;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import moe.yukinoneko.diycode.R;
import moe.yukinoneko.diycode.bean.Like;
import moe.yukinoneko.diycode.bean.Topic;
import moe.yukinoneko.diycode.event.LoginEvent;
import moe.yukinoneko.diycode.misc.DiyCodeWebViewClient;
import moe.yukinoneko.diycode.misc.OnWebImageClickListener;
import moe.yukinoneko.diycode.module.image.ImageActivity;
import moe.yukinoneko.diycode.module.login.LoginActivity;
import moe.yukinoneko.diycode.module.reply.RepliesFragment;
import moe.yukinoneko.diycode.module.reply.create.CreateReplyActivity;
import moe.yukinoneko.diycode.module.web.WebActivity;
import moe.yukinoneko.diycode.mvp.MVPBaseActivity;
import moe.yukinoneko.diycode.tool.ImageLoadHelper;
import moe.yukinoneko.diycode.tool.UserHelper;
import static android.text.TextUtils.isEmpty;
import static com.kennyc.view.MultiStateView.VIEW_STATE_ERROR;
/*
 * Copyright (c) 2017 SamuelGjk <samuel.alva@outlook.com>
 *
 * This file is part of DiyCode
 *
 * DiyCode is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * DiyCode is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with DiyCode. If not, see <http://www.gnu.org/licenses/>.
 */

package moe.yukinoneko.diycode.module.topic.details;









/**
 * MVPPlugin
 * 邮箱 784787081@qq.com
 */

public class TopicDetailsActivity extends MVPBaseActivity<TopicDetailsContract.View, TopicDetailsPresenter> implements TopicDetailsContract.View {

    private static final String EXTRA_TOPIC_ID = "extra_topic_id";

    @BindView(R.id.multi_state_view) MultiStateView multiStateView;
    @BindView(R.id.image_user_avatar) RoundedImageView imageUserAvatar;
    @BindView(R.id.appbar_title) AppCompatTextView appbarTitle;
    @BindView(R.id.post_content) WebView postContent;
    @BindView(R.id.sliding_layout) SlidingUpPanelLayout slidingLayout;
    @BindView(R.id.button_like) AppCompatImageButton buttonLike;
    @BindView(R.id.text_like_count) AppCompatTextView textLikeCount;
    @BindView(R.id.button_favorite) AppCompatImageButton buttonFavorite;
    @BindView(R.id.replies_title) AppCompatTextView repliesTitle;

    private int topicId;
    private Topic topic;

    public static void launch(Context context, int topicId) {
        Intent intent = new Intent(context, TopicDetailsActivity.class);
        intent.putExtra(EXTRA_TOPIC_ID, topicId);
        context.startActivity(intent);
    }

    @Override
    protected int provideContentViewId() {
        return R.layout.activity_post;
    }

    @Override
    @SuppressLint({ "SetJavaScriptEnabled", "AddJavascriptInterface" })
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setDisplayShowTitleEnabled(false);

        WebSettings settings = postContent.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setDefaultFontSize(14);
        settings.setSupportZoom(false);
        postContent.setWebViewClient(new WebClient(this));

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            postContent.addJavascriptInterface(new OnWebImageClickListener() {
                @JavascriptInterface
                @Override
                public void onClick(String imageUrl) {
                    if (!isEmpty(imageUrl)) {
                        ImageActivity.launch(TopicDetailsActivity.this, imageUrl);
                    }
                }
            }, "onWebImageClickListener");
        }

        View errorView = multiStateView.getView(VIEW_STATE_ERROR);
        if (errorView != null) {
            ButterKnife.findById(errorView, R.id.text_error)
                       .setOnClickListener(new View.OnClickListener() {
                           @Override
                           public void onClick(View v) {
                               multiStateView.setViewState(MultiStateView.VIEW_STATE_LOADING);
                               presenter.fetchTopic(topicId);
                           }
                       });
        }

        topicId = getIntent().getIntExtra(EXTRA_TOPIC_ID, -1);
        presenter.fetchTopic(topicId);

        getSupportFragmentManager().beginTransaction()
                                   .replace(
                                           R.id.replies_container,
                                           RepliesFragment.newInstance("topics", topicId)
                                   )
                                   .commit();
    }

    @OnClick({ R.id.button_like, R.id.button_favorite, R.id.fab })
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button_like:
                if (UserHelper.isLogin()) {