>
14 | }
15 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/base/network/TLSSocketFactory.java:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.base.network;
2 |
3 | import java.io.IOException;
4 | import java.net.InetAddress;
5 | import java.net.Socket;
6 | import java.security.KeyManagementException;
7 | import java.security.NoSuchAlgorithmException;
8 |
9 | import javax.net.ssl.SSLContext;
10 | import javax.net.ssl.SSLSocket;
11 | import javax.net.ssl.SSLSocketFactory;
12 |
13 | /**
14 | * Socket factory enabling TLS
15 | *
16 | * Source: http://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
17 | */
18 | public class TLSSocketFactory extends SSLSocketFactory {
19 | private SSLSocketFactory delegate;
20 |
21 | public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
22 | SSLContext context = SSLContext.getInstance("TLS");
23 | context.init(null, null, null);
24 | delegate = context.getSocketFactory();
25 | }
26 |
27 | @Override
28 | public String[] getDefaultCipherSuites() {
29 | return delegate.getDefaultCipherSuites();
30 | }
31 |
32 | @Override
33 | public String[] getSupportedCipherSuites() {
34 | return delegate.getSupportedCipherSuites();
35 | }
36 |
37 | @Override
38 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
39 | return enableTLSOnSocket(delegate.createSocket(s, host, port, autoClose));
40 | }
41 |
42 | @Override
43 | public Socket createSocket(String host, int port) throws IOException {
44 | return enableTLSOnSocket(delegate.createSocket(host, port));
45 | }
46 |
47 | @Override
48 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
49 | return enableTLSOnSocket(delegate.createSocket(host, port, localHost, localPort));
50 | }
51 |
52 | @Override
53 | public Socket createSocket(InetAddress host, int port) throws IOException {
54 | return enableTLSOnSocket(delegate.createSocket(host, port));
55 | }
56 |
57 | @Override
58 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
59 | return enableTLSOnSocket(delegate.createSocket(address, port, localAddress, localPort));
60 | }
61 |
62 | private Socket enableTLSOnSocket(Socket socket) {
63 | if (socket != null && (socket instanceof SSLSocket)) {
64 | ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"});
65 | }
66 | return socket;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/base/network/WhatIfAPI.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.base.network
2 |
3 |
4 | import io.reactivex.Single
5 | import okhttp3.ResponseBody
6 | import retrofit2.http.*
7 | import xyz.jienan.xkcd.model.WhatIfArticle
8 |
9 | interface WhatIfAPI {
10 |
11 | @get:Headers("$HEADER_CACHEABLE: 600")
12 | @get:GET("archive/")
13 | val archive: Single
14 |
15 | @GET("{article_id}/")
16 | fun getArticle(@Path("article_id") id: Long): Single
17 |
18 | @FormUrlEncoded
19 | @POST
20 | fun thumbsUpWhatIf(@Url url: String, @Field("what_if_id") whatIfId: Int): Single
21 |
22 | @Headers("$HEADER_CACHEABLE: 60")
23 | @GET
24 | fun getTopWhatIfs(@Url url: String, @Query("sortby") sortby: String): Single>
25 | }
26 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/base/network/XkcdAPI.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.base.network
2 |
3 | import io.reactivex.Observable
4 | import io.reactivex.Single
5 | import okhttp3.ResponseBody
6 | import retrofit2.http.*
7 | import xyz.jienan.xkcd.model.ExtraComics
8 | import xyz.jienan.xkcd.model.XkcdPic
9 |
10 | interface XkcdAPI {
11 | @get:GET("info.0.json")
12 | val latest: Observable
13 |
14 | @get:GET(XKCD_SPECIAL_LIST)
15 | val specialXkcds: Observable>
16 |
17 | @get:GET(XKCD_EXTRA_LIST)
18 | val extraComics: Observable>
19 |
20 | @Headers("$HEADER_CACHEABLE: 600")
21 | @GET("{comic_id}/info.0.json")
22 | fun getComics(@Path("comic_id") comicId: Long): Observable
23 |
24 | @Headers("$HEADER_CACHEABLE: 2419200")
25 | @GET
26 | fun getExplain(@Url url: String): Observable
27 |
28 | @GET
29 | fun getExplainWithShortCache(@Url url: String, @Header(HEADER_CACHEABLE) cache: Long): Observable
30 |
31 | @Headers("$HEADER_CACHEABLE: 600")
32 | @GET(XKCD_SEARCH_SUGGESTION)
33 | fun getXkcdsSearchResult(@Query("q") query: String): Observable>
34 |
35 | /**
36 | * Get the xkcd list with paging
37 | *
38 | * @param url
39 | * @param start the start index of xkcd list
40 | * @param reversed 0 not reversed, 1 reversed
41 | * @param size the size of returned xkcd list
42 | * @return
43 | */
44 | @GET
45 | fun getXkcdList(@Url url: String,
46 | @Query("start") start: Int, @Query("reversed") reversed: Int, @Query("size") size: Int): Observable>
47 |
48 | @FormUrlEncoded
49 | @POST(XKCD_THUMBS_UP)
50 | fun thumbsUp(@Field("comic_id") comicId: Int): Observable
51 |
52 | @Headers("$HEADER_CACHEABLE: 60")
53 | @GET(XKCD_TOP)
54 | fun getTopXkcds(@Query("sortby") sortby: String): Observable>
55 |
56 | @Headers("$HEADER_CACHEABLE: 600")
57 | @GET
58 | fun getLocalizedXkcd(@Url url: String): Single
59 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/comics/ComicsPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.comics
2 |
3 | import androidx.fragment.app.Fragment
4 | import xyz.jienan.xkcd.comics.fragment.SingleComicFragment
5 | import xyz.jienan.xkcd.home.base.BaseStatePagerAdapter
6 |
7 | class ComicsPagerAdapter(fragment: Fragment) : BaseStatePagerAdapter(fragment) {
8 |
9 | override fun createFragment(position: Int)= SingleComicFragment.newInstance(position + 1)
10 | }
11 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/comics/SearchCursorAdapter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.comics
2 |
3 | import android.content.Context
4 | import android.database.Cursor
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.ImageView
9 | import android.widget.TextView
10 |
11 | import androidx.cursoradapter.widget.CursorAdapter
12 |
13 | import com.bumptech.glide.Glide
14 | import com.bumptech.glide.RequestManager
15 |
16 | import xyz.jienan.xkcd.R
17 | import xyz.jienan.xkcd.base.glide.XkcdGlideUtils
18 |
19 | /**
20 | * Created by jienanzhang on 21/03/2018.
21 | */
22 |
23 | class SearchCursorAdapter(context: Context?, c: Cursor? = null, flags: Int = 0, private val itemBgColor: Int?) : CursorAdapter(context, c, flags) {
24 |
25 | private val glide: RequestManager = Glide.with(context)
26 |
27 | override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
28 | val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
29 |
30 | val view = inflater.inflate(R.layout.item_search_suggestion, parent, false)
31 |
32 | loadContent(view)
33 |
34 | return view
35 | }
36 |
37 | override fun bindView(view: View, context: Context, cursor: Cursor) {
38 | loadContent(view)
39 | }
40 |
41 | private fun loadContent(view: View) {
42 | val url = cursor.getString(1)
43 | val ivThumbnail = view.findViewById(R.id.iv_thumbnail)
44 |
45 | if (itemBgColor != null) {
46 | ivThumbnail.setBackgroundColor(itemBgColor)
47 | }
48 |
49 | XkcdGlideUtils.load(glide, url, ivThumbnail)
50 |
51 | (view.findViewById(R.id.tv_xkcd_title) as TextView).text = ivThumbnail.context.resources.getString(R.string.item_search_title,
52 | cursor.getString(3),
53 | cursor.getString(2))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/comics/contract/ComicsMainContract.java:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.comics.contract;
2 |
3 | import java.util.List;
4 |
5 | import xyz.jienan.xkcd.base.BaseView;
6 | import xyz.jienan.xkcd.home.base.ContentMainBasePresenter;
7 | import xyz.jienan.xkcd.model.XkcdPic;
8 |
9 | public interface ComicsMainContract {
10 |
11 | interface View extends BaseView {
12 |
13 | void latestXkcdLoaded(XkcdPic xkcdPic);
14 |
15 | void showFab(XkcdPic xkcdPic);
16 |
17 | void toggleFab(boolean isFavorite);
18 |
19 | void showThumbUpCount(Long thumbCount);
20 |
21 | void renderXkcdSearch(List xkcdPics);
22 | }
23 |
24 | interface Presenter extends ContentMainBasePresenter {
25 |
26 | long getBookmark();
27 |
28 | boolean setBookmark(long index);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/comics/contract/ImageDetailPageContract.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.comics.contract
2 |
3 | import android.graphics.Bitmap
4 |
5 | import xyz.jienan.xkcd.base.BasePresenter
6 | import xyz.jienan.xkcd.base.BaseView
7 | import xyz.jienan.xkcd.model.XkcdPic
8 |
9 | interface ImageDetailPageContract {
10 |
11 | interface View : BaseView {
12 |
13 | fun setLoading(isLoading: Boolean)
14 |
15 | fun renderPic(url: String)
16 |
17 | fun renderTitle(xkcdPic: XkcdPic)
18 |
19 | fun renderSeekBar(duration: Int)
20 |
21 | fun renderFrame(bitmap: Bitmap)
22 |
23 | fun changeGifSeekBarProgress(progress: Int)
24 |
25 | fun showGifPlaySpeed(speed: Int)
26 | }
27 |
28 | interface Presenter : BasePresenter {
29 |
30 | fun requestImage(index: Int)
31 |
32 | fun parseGifData(data: ByteArray?)
33 |
34 | fun parseFrame(progress: Int)
35 |
36 | fun adjustGifSpeed(increaseByOne: Int)
37 |
38 | fun adjustGifFrame(isForward: Boolean)
39 |
40 | var isEcoMode: Boolean
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/comics/contract/SingleComicContract.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.comics.contract
2 |
3 | import android.graphics.Bitmap
4 |
5 | import xyz.jienan.xkcd.base.BasePresenter
6 | import xyz.jienan.xkcd.base.BaseView
7 | import xyz.jienan.xkcd.model.XkcdPic
8 |
9 | interface SingleComicContract {
10 |
11 | interface View : BaseView {
12 |
13 | var translationMode : Int
14 |
15 | fun explainLoaded(result: String)
16 |
17 | fun explainFailed()
18 |
19 | fun renderXkcdPic(xkcdPic: XkcdPic)
20 |
21 | fun setLoading(isLoading: Boolean)
22 |
23 | fun setAltTextVisibility(gone: Boolean)
24 | }
25 |
26 | interface Presenter : BasePresenter {
27 |
28 | fun loadXkcd(index: Int)
29 |
30 | fun getExplain(index: Long)
31 |
32 | fun updateXkcdSize(xkcdPic: XkcdPic?, resource: Bitmap?)
33 |
34 | val showLocalXkcd : Boolean
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/extra/ExtraPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.extra
2 |
3 | import androidx.fragment.app.Fragment
4 | import xyz.jienan.xkcd.extra.fragment.SingleExtraFragment
5 | import xyz.jienan.xkcd.extra.fragment.SingleExtraWebViewFragment
6 | import xyz.jienan.xkcd.home.base.BaseStatePagerAdapter
7 | import xyz.jienan.xkcd.model.ExtraComics
8 |
9 | class ExtraPagerAdapter(fragment: Fragment) : BaseStatePagerAdapter(fragment) {
10 |
11 | private var extraComicsList: List? = null
12 |
13 | override fun createFragment(position: Int): Fragment {
14 | val realPosition = position.coerceIn(0, extraComicsList!!.size - 1)
15 | val extraComics = extraComicsList!![realPosition]
16 |
17 | return if (extraComics.isWebExtra()) {
18 | SingleExtraWebViewFragment.newInstance(extraComics)
19 | } else {
20 | SingleExtraFragment.newInstance(realPosition + 1)
21 | }
22 | }
23 |
24 | fun setEntities(extraComics: List?) {
25 | extraComicsList = extraComics
26 | }
27 |
28 | override fun getItemCount(): Int = extraComicsList?.size ?: 0
29 |
30 | private fun ExtraComics.isWebExtra(): Boolean {
31 | return !links?.get(0).isNullOrBlank()
32 | }
33 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/extra/contract/ExtraMainContract.java:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.extra.contract;
2 |
3 | import java.util.List;
4 |
5 | import xyz.jienan.xkcd.base.BaseView;
6 | import xyz.jienan.xkcd.home.base.ContentMainBasePresenter;
7 | import xyz.jienan.xkcd.model.ExtraComics;
8 |
9 | public interface ExtraMainContract {
10 |
11 | interface View extends BaseView {
12 |
13 | void showExtras(List extraComics);
14 | }
15 |
16 | interface Presenter extends ContentMainBasePresenter {
17 | void observe();
18 | void dispose();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/extra/contract/SingleExtraContract.java:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.extra.contract;
2 |
3 | import xyz.jienan.xkcd.base.BasePresenter;
4 | import xyz.jienan.xkcd.base.BaseView;
5 | import xyz.jienan.xkcd.model.ExtraComics;
6 |
7 | public interface SingleExtraContract {
8 |
9 | interface View extends BaseView {
10 |
11 | void explainLoaded(String result);
12 |
13 | void explainFailed();
14 |
15 | void renderExtraPic(ExtraComics extraComicsInDB);
16 |
17 | void setLoading(boolean isLoading);
18 | }
19 |
20 | interface Presenter extends BasePresenter {
21 |
22 | void loadExtra(int index);
23 |
24 | void getExplain(String url, Boolean refresh);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/extra/presenter/ExtraMainPresenter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.extra.presenter
2 |
3 | import io.objectbox.reactive.DataSubscription
4 | import io.reactivex.disposables.CompositeDisposable
5 | import timber.log.Timber
6 | import xyz.jienan.xkcd.extra.contract.ExtraMainContract
7 | import xyz.jienan.xkcd.model.ExtraComics
8 | import xyz.jienan.xkcd.model.ExtraModel
9 | import xyz.jienan.xkcd.model.persist.SharedPrefManager
10 | import java.util.*
11 |
12 | class ExtraMainPresenter(private val view: ExtraMainContract.View) : ExtraMainContract.Presenter {
13 |
14 | private val sharedPrefManager = SharedPrefManager
15 |
16 | private val compositeDisposable = CompositeDisposable()
17 |
18 | private var extraComics: List = ArrayList()
19 |
20 | private var subscription: DataSubscription? = null
21 |
22 | override fun loadLatest() {
23 | extraComics = ExtraModel.all
24 | view.showExtras(extraComics)
25 | }
26 |
27 | override fun observe() {
28 | subscription?.cancel()
29 | subscription = ExtraModel.observe
30 | .observer {
31 | if (it.size != extraComics.size && it.isNotEmpty()) {
32 | Timber.d("Show extra $it")
33 | extraComics = it
34 | view.showExtras(extraComics)
35 | subscription?.cancel()
36 | }
37 | }
38 | }
39 |
40 | override fun dispose() {
41 | subscription?.cancel()
42 | }
43 |
44 | override fun liked(index: Long) {
45 | // no-ops
46 | }
47 |
48 | override fun favorited(index: Long, isFav: Boolean) {
49 | // no-ops
50 | }
51 |
52 | override fun getInfoAndShowFab(index: Int) {
53 | // no-ops
54 | }
55 |
56 | override var latest: Int
57 | get() = extraComics.size
58 | set(value) {
59 | // no ops
60 | }
61 |
62 | override fun setLastViewed(lastViewed: Int) {
63 | sharedPrefManager.setLastViewedExtra(lastViewed)
64 | }
65 |
66 | override fun getLastViewed(latestIndex: Int) = sharedPrefManager.getLastViewedExtra(latestIndex)
67 |
68 | override fun onDestroy() {
69 | compositeDisposable.dispose()
70 | }
71 |
72 |
73 | override fun searchContent(query: String) {
74 | // no-ops
75 | }
76 |
77 | override val randomUntouchedIndex: Long
78 | get() = 0L
79 | }
80 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/extra/presenter/SingleExtraPresenter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.extra.presenter
2 |
3 | import android.text.TextUtils
4 | import io.reactivex.android.schedulers.AndroidSchedulers
5 | import io.reactivex.disposables.Disposables
6 | import timber.log.Timber
7 | import xyz.jienan.xkcd.extra.contract.SingleExtraContract
8 | import xyz.jienan.xkcd.model.ExtraModel
9 |
10 | class SingleExtraPresenter(private val view: SingleExtraContract.View) : SingleExtraContract.Presenter {
11 |
12 | private var explainDisposable = Disposables.disposed()
13 |
14 | override fun getExplain(url: String, refresh: Boolean) {
15 | explainDisposable.dispose()
16 | ExtraModel.loadExplain(url, refresh)
17 | .observeOn(AndroidSchedulers.mainThread())
18 | .subscribe(
19 | { explainContent ->
20 | view.explainLoaded(explainContent)
21 | if (!TextUtils.isEmpty(explainContent)) {
22 | ExtraModel.saveExtraWithExplain(url, explainContent)
23 | }
24 | },
25 | { e ->
26 | view.explainFailed()
27 | Timber.e(e)
28 | }
29 | ).also { explainDisposable = it }
30 | }
31 |
32 | override fun loadExtra(index: Int) {
33 | view.renderExtraPic(ExtraModel.getExtra(index))
34 | }
35 |
36 | override fun onDestroy() {
37 | explainDisposable.dispose()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/home/base/BaseStatePagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.home.base
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.viewpager2.adapter.FragmentStateAdapter
5 |
6 |
7 | abstract class BaseStatePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
8 |
9 | var size = 0
10 | set(value) {
11 | field = value
12 | notifyDataSetChanged()
13 | }
14 |
15 | override fun getItemCount(): Int = size
16 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/home/base/ContentMainBasePresenter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.home.base
2 |
3 | import xyz.jienan.xkcd.base.BasePresenter
4 |
5 | interface ContentMainBasePresenter : BasePresenter {
6 | fun favorited(currentIndex: Long, isFav: Boolean)
7 |
8 | fun liked(currentIndex: Long)
9 |
10 | fun setLastViewed(lastViewed: Int)
11 |
12 | fun getInfoAndShowFab(currentIndex: Int)
13 |
14 | var latest: Int
15 |
16 | fun getLastViewed(latestIndex: Int): Int
17 |
18 | fun loadLatest()
19 |
20 | fun searchContent(query: String)
21 |
22 | val randomUntouchedIndex: Long
23 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/list/ListBaseAdapter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.list
2 |
3 | import androidx.recyclerview.widget.RecyclerView
4 |
5 | abstract class ListBaseAdapter : RecyclerView.Adapter(), IAdapter
6 |
7 | interface IAdapter {
8 |
9 | var pauseLoading: Boolean
10 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/list/activity/BaseListView.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.list.activity
2 |
3 | interface BaseListView {
4 |
5 | fun setLoading(isLoading: Boolean)
6 |
7 | fun showScroller(visibility: Int)
8 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/list/activity/WhatIfListActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.list.activity
2 |
3 | import androidx.recyclerview.widget.LinearLayoutManager
4 | import androidx.recyclerview.widget.RecyclerView
5 | import xyz.jienan.xkcd.Const.FIRE_WHAT_IF_SUFFIX
6 | import xyz.jienan.xkcd.R
7 | import xyz.jienan.xkcd.list.ListBaseAdapter
8 | import xyz.jienan.xkcd.list.WhatIfListAdapter
9 | import xyz.jienan.xkcd.list.contract.WhatIfListContract
10 | import xyz.jienan.xkcd.list.presenter.WhatIfListPresenter
11 | import xyz.jienan.xkcd.model.WhatIfArticle
12 |
13 | /**
14 | * Created by jienanzhang on 22/03/2018.
15 | */
16 |
17 | class WhatIfListActivity : BaseListActivity(), WhatIfListContract.View {
18 |
19 | override val mAdapter: ListBaseAdapter = WhatIfListAdapter()
20 |
21 | override val layoutManager = LinearLayoutManager(this)
22 |
23 | override val logSuffix = FIRE_WHAT_IF_SUFFIX
24 |
25 | override val presenter by lazy { WhatIfListPresenter(this) }
26 |
27 | override val filters = intArrayOf(R.string.filter_all_articles, R.string.filter_my_fav, R.string.filter_people_choice)
28 |
29 | override fun lastItemReached(): Boolean {
30 | if (!(mAdapter as WhatIfListAdapter).articles.isNullOrEmpty()) {
31 | val articles = mAdapter.articles!!
32 | val lastArticle = articles.last()
33 | return presenter.lastItemReached(lastArticle.num)
34 | }
35 | return false
36 | }
37 |
38 | override fun updateData(articles: List) {
39 | (mAdapter as WhatIfListAdapter).updateData(articles)
40 | }
41 |
42 | override fun getItemIndexOnPosition(position: Int) =
43 | (mAdapter as WhatIfListAdapter).getArticle(position)?.num?.toInt()
44 | }
45 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/list/contract/WhatIfListContract.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.list.contract
2 |
3 | import xyz.jienan.xkcd.base.BaseView
4 | import xyz.jienan.xkcd.list.activity.BaseListView
5 | import xyz.jienan.xkcd.list.presenter.ListPresenter
6 | import xyz.jienan.xkcd.model.WhatIfArticle
7 |
8 | interface WhatIfListContract {
9 |
10 | interface View : BaseView, BaseListView {
11 | fun updateData(articles: List)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/list/contract/XkcdListContract.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.list.contract
2 |
3 | import xyz.jienan.xkcd.base.BaseView
4 | import xyz.jienan.xkcd.list.activity.BaseListView
5 | import xyz.jienan.xkcd.list.presenter.ListPresenter
6 | import xyz.jienan.xkcd.model.XkcdPic
7 |
8 | interface XkcdListContract {
9 |
10 | interface View : BaseView, BaseListView {
11 |
12 | fun updateData(pics: List)
13 |
14 | fun isLoadingMore(isLoadingMore: Boolean)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/list/presenter/ListPresenter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.list.presenter
2 |
3 | import xyz.jienan.xkcd.base.BasePresenter
4 |
5 | interface ListPresenter : BasePresenter {
6 |
7 | fun loadList(startIndex: Int = 1, reversed: Boolean = false)
8 |
9 | fun hasFav(): Boolean
10 |
11 | fun loadFavList()
12 |
13 | fun loadPeopleChoiceList()
14 |
15 | fun lastItemReached(index: Long): Boolean
16 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/ExtraComics.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import io.objectbox.annotation.Convert
5 | import io.objectbox.annotation.Entity
6 | import io.objectbox.annotation.Id
7 | import io.objectbox.converter.PropertyConverter
8 | import java.io.Serializable
9 |
10 | @Entity
11 | data class ExtraComics constructor(
12 | @Id(assignable = true)
13 | var num: Long = 0,
14 | val title: String = "",
15 | val date: String = "",
16 | val img: String = "",
17 | @SerializedName("explain")
18 | val explainUrl: String = "",
19 | var explainContent: String? = null,
20 | @Convert(converter = ListConverter::class, dbType = String::class)
21 | var links: List? = null,
22 | val alt: String = ""
23 | ) : Serializable {
24 |
25 | class ListConverter : PropertyConverter, String> {
26 |
27 | override fun convertToEntityProperty(databaseValue: String?) =
28 | databaseValue?.split("||")
29 |
30 | override fun convertToDatabaseValue(entityProperty: List) = entityProperty.joinToString("||")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/ExtraModel.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model
2 |
3 |
4 | import android.text.TextUtils
5 | import io.objectbox.reactive.SubscriptionBuilder
6 |
7 | import io.reactivex.Observable
8 | import io.reactivex.android.schedulers.AndroidSchedulers
9 | import io.reactivex.schedulers.Schedulers
10 | import xyz.jienan.xkcd.base.network.NetworkService
11 | import xyz.jienan.xkcd.model.persist.BoxManager
12 | import xyz.jienan.xkcd.model.util.ExtraHtmlUtil
13 | import xyz.jienan.xkcd.model.util.XkcdExplainUtil
14 |
15 | object ExtraModel {
16 |
17 | val all: List
18 | get() = BoxManager.extraList
19 |
20 | val observe: SubscriptionBuilder>
21 | get() = BoxManager.extraListObservable
22 |
23 | fun getExtra(index: Int): ExtraComics? = BoxManager.getExtra(index)
24 |
25 | fun update(extraComics: List) {
26 | BoxManager.saveExtras(extraComics)
27 | }
28 |
29 | fun loadExplain(url: String, refresh: Boolean): Observable {
30 | val explainFromDB = BoxManager.loadExtraExplain(url)
31 | return if (!TextUtils.isEmpty(explainFromDB) && !refresh) {
32 | Observable.just(explainFromDB)
33 | } else NetworkService.xkcdAPI.getExplain(url).subscribeOn(Schedulers.io())
34 | .map { responseBody -> XkcdExplainUtil.getExplainFromHtml(responseBody, url) }
35 |
36 | }
37 |
38 | fun saveExtraWithExplain(url: String, explainContent: String) {
39 | BoxManager.updateExtra(url, explainContent)
40 | }
41 |
42 | fun parseContentFromUrl(url: String): Observable {
43 | return ExtraHtmlUtil.getContentFromUrl(url)
44 | .observeOn(AndroidSchedulers.mainThread())
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/Quote.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model
2 |
3 | import android.text.TextUtils
4 |
5 | import androidx.annotation.Keep
6 |
7 | import xyz.jienan.xkcd.Const.TAG_XKCD
8 |
9 | /**
10 | * author : Stacey's dad
11 | * content : Get out while you still can
12 | * num : 61
13 | * source : xkcd
14 | */
15 |
16 | @Keep
17 | data class Quote(var author: String = "Man in Chair",
18 | var content: String = "Sudo make me a sandwich",
19 | var num: Int = 149,
20 | var source: String = TAG_XKCD,
21 | var timestamp: Long = System.currentTimeMillis()) {
22 |
23 | override fun equals(other: Any?): Boolean {
24 | if (other is Quote) {
25 | val q = other as Quote?
26 | return (q!!.num == this.num
27 | && !TextUtils.isEmpty(q.content)
28 | && q.content == this.content)
29 | }
30 | return false
31 | }
32 |
33 | override fun hashCode() = super.hashCode()
34 | }
35 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/QuoteModel.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model
2 |
3 | import android.content.res.Resources
4 | import androidx.annotation.RawRes
5 | import com.google.gson.Gson
6 | import com.google.gson.reflect.TypeToken
7 | import io.reactivex.Observable
8 | import io.reactivex.schedulers.Schedulers
9 | import xyz.jienan.xkcd.BuildConfig
10 | import xyz.jienan.xkcd.R
11 | import xyz.jienan.xkcd.base.network.NetworkService
12 | import java.util.*
13 |
14 | object QuoteModel {
15 |
16 | private val INTERVAL = (if (BuildConfig.DEBUG) 10000 else 1000 * 60 * 60 * 24).toLong()
17 |
18 | fun getQuoteOfTheDay(previousQuote: Quote, resources: Resources): Observable =
19 | Observable.just(previousQuote)
20 | .flatMap {
21 | if (System.currentTimeMillis() - it.timestamp < INTERVAL) {
22 | Observable.just(it)
23 | } else {
24 | queryNewQuote(it, resources)
25 | }
26 | }
27 |
28 | private fun queryNewQuote(quote: Quote, resources: Resources) =
29 | NetworkService.quoteAPI.quotes
30 | .subscribeOn(Schedulers.io())
31 | .onErrorReturnItem(resources.readRawJson(R.raw.quotes))
32 | .map { quotes ->
33 | quotes.remove(quote)
34 | quotes
35 | }
36 | .map { quotes -> quotes[Random().nextInt(quotes.size)] }
37 | .doOnNext { result -> result.timestamp = System.currentTimeMillis() }
38 | }
39 |
40 | private inline fun Resources.readRawJson(@RawRes rawResId: Int): T {
41 | openRawResource(rawResId).bufferedReader().use {
42 | return Gson().fromJson(it, object: TypeToken() {}.type)
43 | }
44 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/WhatIfArticle.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import io.objectbox.annotation.Entity
5 | import io.objectbox.annotation.Id
6 |
7 | @Entity
8 | data class WhatIfArticle constructor(
9 | @Id(assignable = true)
10 | var num: Long = 0,
11 | var title: String = "",
12 | var featureImg: String? = null,
13 | var content: String? = null,
14 | @Transient
15 | var date: Long = 0L,
16 | @SerializedName("date")
17 | var dateInString: String = "",
18 | var isFavorite: Boolean = false,
19 | var hasThumbed: Boolean = false,
20 | var thumbCount: Long = 0,
21 | )
22 |
23 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/XkcdPic.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model
2 |
3 |
4 | import androidx.core.text.HtmlCompat
5 | import com.google.gson.annotations.SerializedName
6 | import io.objectbox.annotation.Entity
7 | import io.objectbox.annotation.Id
8 | import io.objectbox.annotation.Uid
9 | import xyz.jienan.xkcd.model.util.XkcdSideloadUtils
10 | import java.io.Serializable
11 |
12 | /**
13 | * Created by jienanzhang on 09/07/2017.
14 | */
15 |
16 | @Entity
17 | data class XkcdPic constructor(
18 | val year: String = "",
19 | val month: String = "",
20 | val day: String = "",
21 | @Id(assignable = true)
22 | var num: Long = 0L,
23 | @Uid(9035471003790175147L)
24 | @SerializedName("alt")
25 | val _alt: String = "",
26 | var width: Int = 0,
27 | var height: Int = 0,
28 | var isFavorite: Boolean = false,
29 | var hasThumbed: Boolean = false,
30 | @io.objectbox.annotation.Transient
31 | val thumbCount: Long = 0L,
32 | @Uid(7047913805660868881L)
33 | @SerializedName("title")
34 | val _title: String = "",
35 | var img: String = "",
36 | @io.objectbox.annotation.Transient
37 | @Transient
38 | val translated: Boolean = false) : Serializable {
39 |
40 | val targetImg: String
41 | get() = if (translated) {
42 | img
43 | } else {
44 | XkcdSideloadUtils.sideload(this).img
45 | }
46 |
47 | val title
48 | get() = HtmlCompat.fromHtml(_title, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
49 |
50 | val alt
51 | get() = HtmlCompat.fromHtml(_alt, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
52 |
53 | override fun equals(other: Any?) = other is XkcdPic && this.num == other.num
54 |
55 | override fun hashCode() = super.hashCode()
56 | }
57 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/util/ExplainLinkUtil.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model.util
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.text.SpannableStringBuilder
7 | import android.text.method.LinkMovementMethod
8 | import android.text.style.ClickableSpan
9 | import android.text.style.URLSpan
10 | import android.view.View
11 | import android.webkit.URLUtil
12 | import android.widget.TextView
13 | import android.widget.Toast
14 | import androidx.core.text.HtmlCompat
15 | import xyz.jienan.xkcd.BuildConfig
16 | import xyz.jienan.xkcd.Const.URI_XKCD_EXPLAIN_EDIT
17 | import xyz.jienan.xkcd.R
18 | import xyz.jienan.xkcd.comics.activity.ImageDetailPageActivity
19 | import xyz.jienan.xkcd.ui.ToastUtils
20 |
21 | object ExplainLinkUtil {
22 |
23 | fun setTextViewHTML(text: TextView, html: String) {
24 | val sequence = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
25 | val strBuilder = SpannableStringBuilder(sequence)
26 | val urls = strBuilder.getSpans(0, sequence.length, URLSpan::class.java)
27 | urls.forEach { strBuilder.makeLinkClickable(text.context, it) }
28 | text.text = strBuilder
29 | text.movementMethod = LinkMovementMethod.getInstance()
30 | }
31 |
32 | private fun SpannableStringBuilder.makeLinkClickable(context: Context, span: URLSpan) {
33 | val start = getSpanStart(span)
34 | val end = getSpanEnd(span)
35 | val flags = getSpanFlags(span)
36 | val clickable = object : ClickableSpan() {
37 | override fun onClick(view: View) {
38 | val url = span.url
39 | if (XkcdExplainUtil.isXkcdImageLink(url)) {
40 | val id = XkcdExplainUtil.getXkcdIdFromExplainImageLink(url)
41 | ImageDetailPageActivity.startActivityFromId(context, id)
42 | } else if (URLUtil.isNetworkUrl(url)) {
43 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
44 | if (browserIntent.resolveActivity(context.packageManager) != null) {
45 | context.startActivity(browserIntent)
46 | }
47 | } else if (URI_XKCD_EXPLAIN_EDIT == url) {
48 | ToastUtils.showToast(context, context.getString(R.string.uri_hint_explain_edit))
49 | }
50 | if (BuildConfig.DEBUG) {
51 | Toast.makeText(context, url, Toast.LENGTH_SHORT).show()
52 | }
53 | }
54 | }
55 | setSpan(clickable, start, end, flags)
56 | removeSpan(span)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/util/ExtraHtmlUtil.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model.util
2 |
3 | import io.reactivex.Observable
4 | import io.reactivex.schedulers.Schedulers
5 | import org.jsoup.Jsoup
6 | import xyz.jienan.xkcd.base.network.NetworkService
7 |
8 | object ExtraHtmlUtil {
9 |
10 | private const val XKCD_LINK = "http://www.xkcd.com/"
11 |
12 | fun getContentFromUrl(url: String): Observable {
13 | return NetworkService.xkcdAPI.getExplain(url)
14 | .map { responseBody -> Jsoup.parse(responseBody.string()) }
15 | .map { doc ->
16 | doc.head().appendCss("style.css")
17 | val tableElement = doc.body().selectFirst("table[width=90%]")
18 | tableElement?.removeAttr("width")
19 | val imgElements = doc.body().select("img")
20 | for (imgElement in imgElements) {
21 | val src = imgElement.attr("src")
22 | if (src != null && src.startsWith("http://imgs.xkcd")) {
23 | imgElement.attr("src", src.replaceFirst("http:".toRegex(), "https:"))
24 | }
25 | if (XKCD_LINK == imgElement.parent().attr("href")) {
26 | imgElement.parent()
27 | .removeAttr("href")
28 | .attr("href", imgElement.attr("src"))
29 | }
30 | }
31 |
32 | doc
33 | }
34 | .map { it.outerHtml() }
35 | .subscribeOn(Schedulers.io())
36 | }
37 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/work/WhatIfFastLoadWorker.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model.work
2 |
3 | import android.content.Context
4 | import androidx.work.RxWorker
5 | import androidx.work.WorkerParameters
6 | import io.reactivex.Single
7 | import timber.log.Timber
8 | import xyz.jienan.xkcd.model.WhatIfModel
9 | import xyz.jienan.xkcd.model.persist.SharedPrefManager
10 |
11 | class WhatIfFastLoadWorker(appContext: Context, workerParams: WorkerParameters)
12 | : RxWorker(appContext, workerParams) {
13 |
14 | init {
15 | Timber.d("init")
16 | }
17 |
18 | override fun createWork(): Single {
19 | return WhatIfModel.loadLatest()
20 | .doOnSubscribe { Timber.d("what if fast load start") }
21 | .map { it.num }
22 | .doOnSuccess { SharedPrefManager.latestWhatIf = it }
23 | .flatMapCompletable { WhatIfModel.fastLoadWhatIfs(it) }
24 | .toSingleDefault(Result.success())
25 | .doOnSuccess { Timber.d("what if fast load complete") }
26 | .onErrorReturnItem(Result.failure())
27 | }
28 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/model/work/XkcdFastLoadWorker.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model.work
2 |
3 | import android.content.Context
4 | import androidx.work.RxWorker
5 | import androidx.work.WorkerParameters
6 | import io.reactivex.Single
7 | import timber.log.Timber
8 | import xyz.jienan.xkcd.model.XkcdModel
9 | import xyz.jienan.xkcd.model.persist.SharedPrefManager
10 | import java.util.concurrent.TimeUnit
11 |
12 | class XkcdFastLoadWorker(appContext: Context, workerParams: WorkerParameters)
13 | : RxWorker(appContext, workerParams) {
14 |
15 | init {
16 | Timber.d("init")
17 | }
18 |
19 | override fun createWork(): Single {
20 | return XkcdModel.loadLatest()
21 | .doOnSubscribe { Timber.d("xkcd fast load start") }
22 | .map { it.num }
23 | .doOnNext { Timber.d("Latest xkcd $it") }
24 | .doOnNext { SharedPrefManager.latestXkcd = it }
25 | .doOnNext { Timber.d("Ready to fast load $it") }
26 | .flatMapSingle { XkcdModel.fastLoad(it.toInt()) }
27 | .doOnNext { Timber.d("Fast load xkcd complete $it") }
28 | .map { Result.success() }
29 | .singleOrError()
30 | .doOnSuccess { Timber.d("xkcd fast load complete") }
31 | .timeout(20L, TimeUnit.SECONDS)
32 | .doOnError { Timber.e(it) }
33 | .onErrorReturnItem(Result.failure())
34 | }
35 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/settings/ButtonPreference.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.settings
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.View
6 | import android.widget.Button
7 | import androidx.annotation.StringRes
8 | import androidx.preference.Preference
9 | import androidx.preference.PreferenceViewHolder
10 | import xyz.jienan.xkcd.R
11 |
12 | class ButtonPreference : Preference {
13 |
14 | constructor(context: Context) : super(context)
15 |
16 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
17 |
18 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
19 |
20 | @StringRes
21 | private var stringRes: Int = 0
22 |
23 | private var onClickListener: ((View) -> Unit)? = null
24 |
25 | override fun onBindViewHolder(holder: PreferenceViewHolder) {
26 | super.onBindViewHolder(holder)
27 | val button = holder.findViewById(R.id.deleteButton)
28 | if (button != null && button is Button && onClickListener != null) {
29 | if (stringRes != 0) {
30 | button.setText(stringRes)
31 | }
32 | button.setOnClickListener(onClickListener)
33 | }
34 | }
35 |
36 | fun setup(summaryText: String? = null, @StringRes resId: Int = 0, onClickListener: (View) -> Unit) {
37 | summary = summaryText
38 | this.stringRes = resId
39 | this.onClickListener = onClickListener
40 | }
41 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/settings/ManageSpaceActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.settings
2 |
3 | import android.os.Bundle
4 | import android.view.MenuItem
5 | import xyz.jienan.xkcd.base.BaseActivity
6 |
7 | class ManageSpaceActivity: BaseActivity() {
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 | supportFragmentManager.beginTransaction()
11 | .replace(android.R.id.content, ManageSpaceFragment()).commit()
12 | val actionBar = supportActionBar
13 | actionBar?.setDisplayHomeAsUpEnabled(true)
14 | }
15 |
16 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
17 | when (item.itemId) {
18 | android.R.id.home -> finish()
19 | }
20 | return false
21 | }
22 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/settings/PreferenceActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.settings
2 |
3 | import android.os.Bundle
4 | import android.view.MenuItem
5 | import xyz.jienan.xkcd.base.BaseActivity
6 |
7 | /**
8 | * Created by Jienan on 2018/3/9.
9 | */
10 |
11 | class PreferenceActivity : BaseActivity() {
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | supportFragmentManager.beginTransaction()
16 | .replace(android.R.id.content, SettingsFragment()).commit()
17 | val actionBar = supportActionBar
18 | actionBar?.setDisplayHomeAsUpEnabled(true)
19 | }
20 |
21 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
22 | when (item.itemId) {
23 | android.R.id.home -> finish()
24 | }
25 | return false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/AnimUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui;
2 |
3 | import android.graphics.drawable.Animatable;
4 | import android.graphics.drawable.Drawable;
5 | import android.os.Build;
6 | import android.widget.ImageView;
7 |
8 | public class AnimUtils {
9 |
10 | public static void vectorAnim(ImageView view, int animId, int fallbackResId) {
11 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
12 | vectorAnim(view, animId);
13 | } else {
14 | view.setImageResource(fallbackResId);
15 | }
16 | }
17 |
18 | public static void vectorAnim(ImageView view, int animId) {
19 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
20 | view.setImageResource(animId);
21 | Drawable drawable = view.getDrawable();
22 | if (drawable instanceof Animatable) {
23 | ((Animatable) drawable).start();
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/CustomMovementMethod.java:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui;
2 |
3 | import android.text.Layout;
4 | import android.text.Selection;
5 | import android.text.Spannable;
6 | import android.text.method.LinkMovementMethod;
7 | import android.text.method.MovementMethod;
8 | import android.text.method.Touch;
9 | import android.text.style.ClickableSpan;
10 | import android.view.MotionEvent;
11 | import android.view.View;
12 | import android.widget.TextView;
13 |
14 | public class CustomMovementMethod extends LinkMovementMethod {
15 |
16 | private static CustomMovementMethod sInstance;
17 |
18 | public static MovementMethod getInstance() {
19 | if (sInstance == null)
20 | sInstance = new CustomMovementMethod();
21 |
22 | return sInstance;
23 | }
24 |
25 | @Override
26 | public boolean canSelectArbitrarily() {
27 | return true;
28 | }
29 |
30 | @Override
31 | public void initialize(TextView widget, Spannable text) {
32 | Selection.setSelection(text, text.length());
33 | }
34 |
35 | @Override
36 | public void onTakeFocus(TextView view, Spannable text, int dir) {
37 | if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
38 | if (view.getLayout() == null) {
39 | Selection.setSelection(text, text.length());
40 | }
41 | } else {
42 | Selection.setSelection(text, text.length());
43 | }
44 | }
45 |
46 | @Override
47 | public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
48 | int action = event.getAction();
49 |
50 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
51 | int x = (int) event.getX();
52 | int y = (int) event.getY();
53 |
54 | x -= widget.getTotalPaddingLeft();
55 | y -= widget.getTotalPaddingTop();
56 |
57 | x += widget.getScrollX();
58 | y += widget.getScrollY();
59 |
60 | Layout layout = widget.getLayout();
61 | int line = layout.getLineForVertical(y);
62 | int off = layout.getOffsetForHorizontal(line, x);
63 |
64 | ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
65 |
66 | if (link.length != 0) {
67 | if (action == MotionEvent.ACTION_UP) {
68 | link[0].onClick(widget);
69 | } else {
70 | Selection.setSelection(buffer,
71 | buffer.getSpanStart(link[0]),
72 | buffer.getSpanEnd(link[0]));
73 | }
74 | return true;
75 | }
76 | }
77 | return Touch.onTouchEvent(widget, buffer, event);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/Progressable.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui
2 |
3 | import android.view.animation.Animation
4 |
5 | interface Progressable {
6 | var progress: Int
7 | }
8 |
9 | interface Animatable {
10 | fun getAnimation() : Animation?
11 |
12 | fun startAnimation(animation: Animation)
13 |
14 | fun clearAnimation()
15 |
16 | fun setVisibility(visibility: Int)
17 | }
18 |
19 | interface IProgressbar : Progressable, Animatable
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/RecyclerItemClickListener.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui
2 |
3 | import android.view.GestureDetector
4 | import android.view.MotionEvent
5 | import android.view.View
6 | import androidx.recyclerview.widget.RecyclerView
7 |
8 | class RecyclerItemClickListener(private val recyclerView: RecyclerView, private val mListener: OnItemClickListener?)
9 | : RecyclerView.SimpleOnItemTouchListener() {
10 |
11 | private val mGestureDetector = GestureDetector(recyclerView.context, object : GestureDetector.SimpleOnGestureListener() {
12 | override fun onSingleTapUp(e: MotionEvent): Boolean {
13 | return true
14 | }
15 |
16 | override fun onLongPress(e: MotionEvent) {
17 | val child = recyclerView.findChildViewUnder(e.x, e.y)
18 | if (child != null && mListener != null) {
19 | mListener.onLongItemClick(child, recyclerView.getChildAdapterPosition(child))
20 | }
21 | }
22 | })
23 |
24 | override fun onInterceptTouchEvent(view: RecyclerView, e: MotionEvent): Boolean {
25 | val childView = view.findChildViewUnder(e.x, e.y)
26 | if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
27 | mListener.onItemClick(childView, view.getChildAdapterPosition(childView))
28 | return true
29 | }
30 | return false
31 | }
32 |
33 | abstract class OnItemClickListener {
34 |
35 | open fun onItemClick(view: View, position: Int) {}
36 |
37 | open fun onLongItemClick(view: View, position: Int) {}
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/ToastUtils.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.view.Gravity
6 | import android.widget.TextView
7 | import android.widget.Toast
8 | import androidx.core.content.res.ResourcesCompat
9 | import xyz.jienan.xkcd.R
10 |
11 | object ToastUtils {
12 |
13 | private var toast: Toast? = null
14 |
15 | @SuppressLint("ShowToast")
16 | fun showToast(context: Context, text: String, position : Int = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, duration: Int = Toast.LENGTH_SHORT) {
17 | try {
18 | toast!!.view?.isShown
19 | toast!!.setText(text)
20 | } catch (e: Exception) {
21 | toast = Toast.makeText(context.applicationContext, text, duration)
22 | }
23 |
24 | if (position != Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL) {
25 | toast!!.setGravity(position, 0, 0)
26 | }
27 |
28 | val textView = toast!!.view?.findViewById(android.R.id.message)
29 | if (textView != null) {
30 | textView.typeface = ResourcesCompat.getFont(context, R.font.xkcd)
31 | }
32 | toast!!.show()
33 | }
34 |
35 | fun cancelToast() {
36 | if (toast != null && toast!!.view?.isShown == true) {
37 | toast!!.cancel()
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/UiUtils.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui
2 |
3 | import android.content.Context
4 | import android.content.res.Configuration
5 | import android.util.TypedValue
6 | import androidx.annotation.AttrRes
7 | import androidx.annotation.ColorInt
8 | import androidx.core.content.ContextCompat
9 |
10 | @ColorInt
11 | fun Context.getColorResCompat(@AttrRes id: Int): Int {
12 | val resolvedAttr = TypedValue()
13 | theme.resolveAttribute(id, resolvedAttr, true)
14 | val colorRes = resolvedAttr.run { if (resourceId != 0) resourceId else data }
15 | return ContextCompat.getColor(this, colorRes)
16 | }
17 |
18 | fun Context.getUiNightModeFlag() = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
19 |
20 | fun dp2px(context: Context, dpValue: Float): Int {
21 | val scale = context.resources.displayMetrics.density
22 | return (dpValue * scale + 0.5f).toInt()
23 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/like/Icon.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui.like
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | /**
6 | * Created by Joel on 23/12/2015.
7 | */
8 | class Icon internal constructor(@DrawableRes val onIconResourceId: Int,
9 | @DrawableRes val offIconResourceId: Int,
10 | val iconType: IconType)
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/like/IconType.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui.like
2 |
3 | /**
4 | * Created by Joel on 23/12/2015.
5 | */
6 | enum class IconType {
7 | HEART,
8 | THUMB
9 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/like/LikeButtonToggleAnimation.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui.like
2 |
3 | import android.animation.AnimatorSet
4 | import android.animation.ObjectAnimator
5 | import android.view.View
6 | import androidx.core.animation.doOnEnd
7 | import androidx.core.animation.doOnStart
8 |
9 | private val ANIMATING = "animating".hashCode()
10 |
11 | fun LikeButton.animateShow(destTranslateX: Float) {
12 | animate(true, destTranslateX)
13 | }
14 |
15 | fun LikeButton.animateHide(destTranslateX: Float) {
16 | animate(false, destTranslateX)
17 | }
18 |
19 | private fun LikeButton.animate(isShow: Boolean, destTranslateX: Float) {
20 | (getTag(ANIMATING) as AnimatorSet?)?.cancel()
21 |
22 | AnimatorSet().apply {
23 | playTogether(ObjectAnimator.ofFloat(this@animate, View.TRANSLATION_X, translationX, destTranslateX),
24 | ObjectAnimator.ofFloat(this@animate, View.ALPHA, alpha, if (isShow) 1f else 0f))
25 | duration = 300L
26 | doOnStart {
27 | if (this@animate != null) {
28 | isClickable = isShow
29 | setTag(ANIMATING, this@apply)
30 | if (isShow) {
31 | this@animate.visibility = View.VISIBLE
32 | }
33 | }
34 | }
35 | doOnEnd {
36 | if (this@animate != null) {
37 | setTag(ANIMATING, null)
38 | if (!isShow) {
39 | this@animate.visibility = View.GONE
40 | }
41 | }
42 | }
43 | }.start()
44 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/like/LikeUtils.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui.like
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.graphics.drawable.Drawable
6 | import androidx.core.graphics.drawable.toBitmap
7 | import androidx.core.graphics.drawable.toDrawable
8 | import androidx.core.graphics.scale
9 | import xyz.jienan.xkcd.R
10 |
11 | /**
12 | * Created by Joel on 23/12/2015.
13 | */
14 |
15 | internal val icons = listOf(Icon(R.drawable.ic_heart_on, R.drawable.ic_heart_off, IconType.HEART),
16 | Icon(R.drawable.ic_thumb_on, R.drawable.ic_thumb_off, IconType.THUMB))
17 |
18 | internal fun Float.mapValueFromRangeToRange(fromLow: Float, fromHigh: Float, toLow: Float, toHigh: Float): Float {
19 | return toLow + (this - fromLow) / (fromHigh - fromLow) * (toHigh - toLow)
20 | }
21 |
22 | internal fun Drawable.resizeDrawable(context: Context, width: Int, height: Int): Drawable {
23 | return toBitmap(width = width, height = height, config = Bitmap.Config.ARGB_8888)
24 | .scale(width, height, true)
25 | .toDrawable(context.resources)
26 | }
27 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/like/OnLikeListener.java:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui.like;
2 |
3 | /**
4 | * Created by Joel on 23/12/2015.
5 | */
6 | public interface OnLikeListener {
7 | void liked(LikeButton likeButton);
8 |
9 | void unliked(LikeButton likeButton);
10 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/xkcdimageview/ImageLoader.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui.xkcdimageview
2 |
3 | import android.net.Uri
4 | import androidx.annotation.UiThread
5 | import java.io.File
6 |
7 |
8 | interface ImageLoader {
9 |
10 | fun loadImage(requestId: Int, uri: Uri, callback: Callback)
11 |
12 | fun prefetch(uri: Uri)
13 |
14 | fun cancel(requestId: Int)
15 |
16 | fun cancelAll()
17 |
18 | @UiThread
19 | interface Callback {
20 | fun onCacheHit(imageType: Int, image: File)
21 |
22 | fun onCacheMiss(imageType: Int, image: File)
23 |
24 | fun onStart()
25 |
26 | fun onProgress(progress: Int)
27 |
28 | fun onFinish()
29 |
30 | fun onSuccess(image: File)
31 |
32 | fun onFail(error: Exception)
33 | }
34 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/xkcdimageview/ImageLoaderFactory.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui.xkcdimageview
2 |
3 |
4 | /**
5 | * Created by Piasy{github.com/Piasy} on 06/11/2016.
6 | * Modified by Charlie Zhang for xkcd project
7 | *
8 | * This is not a singleton, you can initialize it multiple times, but before you initialize it
9 | * again, it will use the same {@link ImageLoader} globally.
10 | */
11 |
12 | object ImageLoaderFactory {
13 | // companion object {
14 | // @Volatile
15 | // private var sInstance: BigImageViewer? = null
16 | //
17 | // fun initialize(imageLoader: ImageLoader) {
18 | // sInstance = BigImageViewer(imageLoader)
19 | // }
20 | //
21 | // fun imageLoader(): ImageLoader {
22 | // if (sInstance == null) {
23 | // throw IllegalStateException("You must initialize BigImageViewer before use it!")
24 | // }
25 | // return sInstance!!.mImageLoader
26 | // }
27 | //
28 | // fun prefetch(vararg uris: Uri) {
29 | // if (uris == null) {
30 | // return
31 | // }
32 | //
33 | // val imageLoader = imageLoader()
34 | // for (uri in uris) {
35 | // imageLoader.prefetch(uri)
36 | // }
37 | // }
38 | // }
39 |
40 | lateinit var imageLoader: ImageLoader
41 |
42 | fun initialize(imageLoader: ImageLoader): ImageLoader {
43 | this.imageLoader = imageLoader
44 | return imageLoader
45 | }
46 |
47 |
48 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/ui/xkcdimageview/ImageViewFactory.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.ui.xkcdimageview
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
6 | import java.io.File
7 |
8 |
9 | /**
10 | * Created by Piasy{github.com/Piasy} on 2018/8/12.
11 | */
12 | open class ImageViewFactory {
13 |
14 | fun createMainView(context: Context, imageType: Int, imageFile: File?,
15 | initScaleType: Int): View? {
16 | return when (imageType) {
17 | ImageInfoExtractor.TYPE_GIF, ImageInfoExtractor.TYPE_ANIMATED_WEBP -> createAnimatedImageView(context, imageType, imageFile, initScaleType)
18 | ImageInfoExtractor.TYPE_STILL_WEBP, ImageInfoExtractor.TYPE_STILL_IMAGE -> createStillImageView(context)
19 | ImageInfoExtractor.TYPE_BITMAP -> createStillImageView(context)
20 | else -> createStillImageView(context)
21 | }
22 | }
23 |
24 | open fun createStillImageView(context: Context): SubsamplingScaleImageView {
25 | return SubsamplingScaleImageView(context)
26 | }
27 |
28 | open fun createAnimatedImageView(context: Context, imageType: Int, imageFile: File?,
29 | initScaleType: Int): View? {
30 | return null
31 | }
32 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/whatif/WhatIfPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.whatif
2 |
3 | import androidx.fragment.app.Fragment
4 | import xyz.jienan.xkcd.home.base.BaseStatePagerAdapter
5 | import xyz.jienan.xkcd.whatif.fragment.SingleWhatIfFragment
6 |
7 | class WhatIfPagerAdapter(fragment: Fragment) : BaseStatePagerAdapter(fragment) {
8 |
9 | override fun createFragment(position: Int) = SingleWhatIfFragment.newInstance(position + 1)
10 | }
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/whatif/contract/WhatIfMainContract.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.whatif.contract
2 |
3 | import xyz.jienan.xkcd.base.BaseView
4 | import xyz.jienan.xkcd.home.base.ContentMainBasePresenter
5 | import xyz.jienan.xkcd.model.WhatIfArticle
6 |
7 | interface WhatIfMainContract {
8 |
9 | interface View : BaseView {
10 |
11 | fun latestWhatIfLoaded(whatIfArticle: WhatIfArticle)
12 |
13 | fun showFab(whatIfArticle: WhatIfArticle)
14 |
15 | fun toggleFab(isFavorite: Boolean)
16 |
17 | fun showThumbUpCount(thumbCount: Long?)
18 |
19 | fun renderWhatIfSearch(articles: List)
20 | }
21 |
22 | interface Presenter : ContentMainBasePresenter {
23 | fun getBookmark() : Long
24 |
25 | fun setBookmark(index: Long) : Boolean
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/whatif/interfaces/ImgInterface.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.whatif.interfaces
2 |
3 | import android.webkit.JavascriptInterface
4 |
5 | class ImgInterface(private val imgCallback: ImgCallback?) {
6 |
7 | @JavascriptInterface
8 | fun doLongPress(title: String) {
9 | imgCallback?.onImgLongClick(title)
10 | }
11 |
12 | interface ImgCallback {
13 | fun onImgLongClick(title: String)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/whatif/interfaces/LatexInterface.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.whatif.interfaces
2 |
3 | import android.webkit.JavascriptInterface
4 |
5 | class LatexInterface {
6 |
7 | private var canParentScroll = true
8 |
9 | @JavascriptInterface
10 | fun onTouch(i: Int) {
11 | canParentScroll = i == 3
12 | }
13 |
14 | fun canScrollHor() = canParentScroll
15 | }
16 |
--------------------------------------------------------------------------------
/xkcd/src/main/java/xyz/jienan/xkcd/whatif/interfaces/RefInterface.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.whatif.interfaces
2 |
3 | import android.webkit.JavascriptInterface
4 |
5 | class RefInterface(private val callback: RefCallback?) {
6 |
7 | @JavascriptInterface
8 | fun refContent(refs: String) {
9 | callback?.onRefClick(refs)
10 | }
11 |
12 | interface RefCallback {
13 | fun onRefClick(content: String)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/anim/fadein.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/anim/fadeout.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/anim/fadeout_drop.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/anim/rotate.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable-hdpi/ic_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable-hdpi/ic_notification.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable-mdpi/ic_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable-mdpi/ic_notification.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable-night/what_if_webview_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable-v21/ripple.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 | -
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable-xhdpi/ic_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable-xhdpi/ic_notification.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable-xxhdpi/ic_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable-xxhdpi/ic_notification.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/anim_fast_forward_shake.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/anim_fast_rewind_shake.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/anim_pause_to_play.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/anim_play_to_pause.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/graph_tile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable/graph_tile.jpg
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_action_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_action_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_action_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_action_share.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_beret.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_black_hat.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_blondie.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_cueball.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_fast_forward.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_fast_rewind.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_filter_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_hairbun.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_hairy.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_heart_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_heart_on.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_heart_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_megan.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_pause.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_play_arrow.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_ponytail.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_sort_calendar_ascending.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_sort_calendar_descending.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_thumb_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_thumb_on.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_what_if_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_white_circle_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_whitehat.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ic_xkcd_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/item_num_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/recycler_view_fast_scroller_bubble.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/recycler_view_fast_scroller_handle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 |
9 |
10 | -
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/ripple.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/drawable/what_if_webview_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/font/xkcd.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
11 |
18 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/font/xkcd_script.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/font/xkcd_script.ttf
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout-land/drawer_footer.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
19 |
20 |
25 |
26 |
31 |
32 |
37 |
38 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/activity_image_webview.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
20 |
21 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/activity_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
17 |
18 |
27 |
28 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
23 |
24 |
28 |
29 |
30 |
31 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/dialog_explain.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
20 |
21 |
22 |
28 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/dialog_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/drawer_footer.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
19 |
20 |
24 |
25 |
29 |
30 |
34 |
35 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/fragment_comic_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/fragment_extra_single.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/fragment_what_if_single.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/item_filter_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
29 |
30 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/item_search_suggestion.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
34 |
35 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/item_what_if_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
32 |
33 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/item_xkcd_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/likeview.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
18 |
19 |
27 |
28 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/nav_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
23 |
24 |
33 |
34 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/pref_widget_delete.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/layout/rv_scroller.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
18 |
19 |
27 |
28 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/menu/menu_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/menu/menu_extra.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/menu/menu_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/menu/menu_what_if.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/menu/menu_xkcd.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/xkcd/src/main/res/raw/quotes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "author": "Stacey's dad",
4 | "content": "Get out while you still can",
5 | "num": 61,
6 | "source": "xkcd"
7 | },
8 | {
9 | "author": "Man in Chair",
10 | "content": "Sudo make me a sandwich",
11 | "num": 149,
12 | "source": "xkcd"
13 | },
14 | {
15 | "author": "Cueball",
16 | "content": "Someone is wrong on the Internet",
17 | "num": 386,
18 | "source": "xkcd"
19 | },
20 | {
21 | "author": "Man in Chair",
22 | "content": "Ouch",
23 | "num": 744,
24 | "source": "xkcd"
25 | },
26 | {
27 | "author": "xkcd",
28 | "content": "BACKWARD. I READ THINK ENGINEERS HIGHWAY",
29 | "num": 781,
30 | "source": "xkcd"
31 | },
32 | {
33 | "author": "Title Text",
34 | "content": "Our brains have just one scale, and we resize our experiences to fit",
35 | "num": 915,
36 | "source": "xkcd"
37 | },
38 | {
39 | "author": "xkcd",
40 | "content": "I disagree strongly with whatever work this quote is attached to",
41 | "num": 1942,
42 | "source": "xkcd"
43 | },
44 | {
45 | "author": "xkcd",
46 | "content": "This quote was taken out of context",
47 | "num": 1942,
48 | "source": "xkcd"
49 | },
50 | {
51 | "author": "Randall",
52 | "content": "But at least now we know",
53 | "num": 8,
54 | "source": "what if"
55 | },
56 | {
57 | "author": "Tim Minchin",
58 | "content": "I really think that I would have somebody else",
59 | "num": 9,
60 | "source": "what if"
61 | },
62 | {
63 | "author": "Randall",
64 | "content": "Unit cancellation is weird",
65 | "num": 11,
66 | "source": "what if"
67 | },
68 | {
69 | "author": "Black Hat",
70 | "content": "What if we tried more power",
71 | "num": 13,
72 | "source": "what if"
73 | },
74 | {
75 |
76 | "author": "Randall",
77 | "content": "All that changes when this cat enters the equation",
78 | "num": 15,
79 | "source": "what if"
80 |
81 | },
82 | {
83 | "author": "Randall",
84 | "content": "This can't really happen",
85 | "num": 39,
86 | "source": "what if"
87 | },
88 | {
89 | "author": "Randall",
90 | "content": "Pressure cookers are dangerous",
91 | "num": 40,
92 | "source": "what if"
93 | }
94 | ]
95 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/raw/xkcd_extra.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "num": 1,
4 | "date": "2006.10.11",
5 | "title": "Blue Eyes",
6 | "img": "https://www.explainxkcd.com/wiki/images/a/a0/Blue_Eyes.jpg",
7 | "links": ["https://xkcd.com/blue_eyes.html", "https://xkcd.com/solution.html"],
8 | "explain": "https://www.explainxkcd.com/wiki/index.php/Blue_Eyes"
9 | },
10 | {
11 | "num": 2,
12 | "date": "2009.06.17",
13 | "title": "Conservation",
14 | "img": "https://www.explainxkcd.com/wiki/images/1/10/conservation.png",
15 | "explain": "https://www.explainxkcd.com/wiki/index.php/Conservation"
16 | },
17 | {
18 | "num": 3,
19 | "date": "2009.08.11",
20 | "title": "Prescriptions",
21 | "img": "https://www.explainxkcd.com/wiki/images/b/bc/prescriptions.png",
22 | "explain": "https://www.explainxkcd.com/wiki/index.php/Prescriptions"
23 | },
24 | {
25 | "num": 4,
26 | "date": "2011.03.19",
27 | "title": "Radiation",
28 | "img": "https://imgs.xkcd.com/blag/radiation.png",
29 | "explain": "https://www.explainxkcd.com/wiki/index.php/Radiation"
30 | },
31 | {
32 | "num": 5,
33 | "date": "2011.08.19",
34 | "title": "Five-Minute Comics: Part 4",
35 | "img": "https://www.explainxkcd.com/wiki/images/6/60/five_minute_comics_part_4.png",
36 | "explain": "https://www.explainxkcd.com/wiki/index.php/Five-Minute_Comics:_Part_4"
37 | },
38 | {
39 | "num": 6,
40 | "date": "2013.10.04",
41 | "title": "The Rise of Open Access",
42 | "img": "https://www.explainxkcd.com/wiki/images/archive/4/48/20150825153049%21the_rise_of_open_access.jpg",
43 | "explain": "https://www.explainxkcd.com/wiki/index.php/The_Rise_of_Open_Access"
44 | },
45 | {
46 | "num": 7,
47 | "date": "2015.10.22",
48 | "title": "XKCD Marks the Spot",
49 | "img": "https://www.explainxkcd.com/wiki/images/9/9c/world_polio_day.png",
50 | "explain": "https://www.explainxkcd.com/wiki/index.php/XKCD_Marks_the_Spot"
51 | },
52 | {
53 | "num": 8,
54 | "date": "2019.08.04",
55 | "title": "Disappearing Sunday Update",
56 | "img": "https://www.explainxkcd.com/wiki/images/6/6c/disappearing_sunday_update.png",
57 | "explain": "https://www.explainxkcd.com/wiki/index.php/Disappearing_Sunday_Update"
58 | },
59 | {
60 | "num": 9,
61 | "date": "2020.07.06",
62 | "title": "No One Was Hurt",
63 | "img": "https://explainxkcd.com/wiki/images/7/7f/no_one_was_hurt.png",
64 | "explain": "https://explainxkcd.com/wiki/index.php/No_One_Was_Hurt",
65 | "alt": "See how the smoke obscures things so you don't see the approaching tornado until the firework streamers start to swirl right before the lightning hits the launcher. Thank God Cousin Ed was filming in slow motion. And, uh, that no one was hurt."
66 | }
67 | ]
68 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-b+zh+hant+TW/api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=zh-tw
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-b+zh/api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://xkcd.jienan.xyz/%d/info.0.json
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-de/api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=de
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Teile dieses xkcd
3 | Bisherige
4 | Nächster
5 | Suche
6 |
7 | Übersetzung by @xkcDE
8 | Eine Schaltfläche wird angezeigt, wenn eine Übersetzung vorhanden ist
9 | Übersetzungen ignorieren
10 | Übersetzung
11 | Original
12 |
13 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-es/api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=es
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Traducción by @es.xkcd.com
3 | Se mostrará un cambio cuando exista traducción
4 | Ignorar traducciones
5 | Traducción
6 | Original
7 |
8 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-fr/api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=fr
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Traduction by @lapin.org
3 | Un commutateur sera affiché lorsque la traduction existe
4 | Ignorer les traductions
5 | Traduction
6 | Original
7 |
8 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #1F2020
4 | #121212
5 | #111111
6 | #AA1F2020
7 |
8 | #ADD8E6
9 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-ru/api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=ru
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Перевод by @xkcd.ru
3 | Переключатель будет показан, когда перевод существует
4 | Игнорировать переводы
5 | Перевод
6 | Oригинал
7 |
8 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values/api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #283593
6 | #AA3F51B5
7 |
8 | #FF4081
9 |
10 | #aeaeae
11 | #e91e63
12 | #FFF
13 | #2196f3
14 | #DEffffff
15 |
16 | #ffffff
17 |
18 | #42000000
19 | #50DDDDDD
20 |
21 |
22 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 10dp
4 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/values/non_translatable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | xkcd
4 |
5 | xkcd
6 | what if
7 | extra
8 |
9 |
10 | M25.1 2.6c12.5 0 18.7 5.9 24.9 17.6C56.2 8.5 62.4 2.6 74.9 2.6c13.8 0 25 10.5 25 23.6C100 50 75 73.7 50 97.4 25 73.7 0 50 0 26.2c0-13 11.3-23.7 25.1-23.6z
11 |
12 |
13 |
14 | M1 21h4V9H1v12zm22-11a2 2 0 0 0-2-2h-6.3l1-4.6v-0.3c0-0.4-0.2-0.8-0.5-1l-1-1.1-6.6 6.6A2 2 0 0 0 7 9v10c0 1.1 0.9 2 2 2h9a2 2 0 0 0 1.8-1.2l3-7 0.2-0.8v-2
15 |
16 |
17 | 1
18 | 10
19 | 30
20 | arrow_1
21 | arrow_10
22 | arrow_30
23 |
24 | what if
25 | xkcd
26 |
27 | %d-%s
28 | %s - %s
29 |
30 | %s.%s.%s
31 |
32 | %d - %s
33 | - %s %s:%d
34 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/xml/backup_descriptor.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | xkcd.com
5 |
6 |
--------------------------------------------------------------------------------
/xkcd/src/main/res/xml/searchable.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/proprietary/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/xkcd/src/proprietary/java/xyz/jienan/xkcd/FlavorUtils.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd
2 |
3 | import android.app.Application
4 | import com.google.android.gms.common.ConnectionResult
5 | import com.google.android.gms.common.GoogleApiAvailabilityLight
6 | import com.google.android.gms.tasks.OnCompleteListener
7 | import com.google.firebase.crashlytics.FirebaseCrashlytics
8 | import com.google.firebase.installations.FirebaseInstallations
9 | import com.google.firebase.messaging.FirebaseMessaging
10 | import timber.log.Timber
11 | import java.util.*
12 | import kotlin.concurrent.thread
13 |
14 | object FlavorUtils {
15 |
16 | private const val FCM_TOPIC_NEW_COMICS = "new_comics"
17 |
18 | private const val FCM_TOPIC_NEW_WHAT_IF = "new_what_if"
19 |
20 | fun init() {
21 | FirebaseMessaging.getInstance().apply {
22 | subscribeToTopic(FCM_TOPIC_NEW_COMICS)
23 | subscribeToTopic(FCM_TOPIC_NEW_WHAT_IF)
24 | }
25 | if (BuildConfig.DEBUG) {
26 | thread(start = true) {
27 | Timber.d("FCM id ${FirebaseInstallations.getInstance().id}")
28 | }
29 |
30 | FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
31 | if (!task.isSuccessful) {
32 | Timber.w(task.exception, "Fetching FCM registration token failed")
33 | return@OnCompleteListener
34 | }
35 |
36 | // Get new FCM registration token
37 | val token = task.result
38 | Timber.d("FCM token $token")
39 | })
40 | }
41 | }
42 |
43 | fun updateLocale() {
44 | if (!BuildConfig.DEBUG) {
45 | FirebaseCrashlytics.getInstance()
46 | .setCustomKey("locale", Locale.getDefault().toString())
47 | }
48 | }
49 |
50 | fun getGmsAvailability(app: Application): Boolean {
51 | val status = GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(app)
52 | Timber.d("GMS status = $status")
53 | return status == ConnectionResult.SUCCESS
54 | }
55 | }
--------------------------------------------------------------------------------
/xkcd/src/proprietary/java/xyz/jienan/xkcd/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.base
2 |
3 | import android.content.SharedPreferences
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.preference.PreferenceManager
7 | import com.google.firebase.analytics.FirebaseAnalytics
8 | import xyz.jienan.xkcd.Const.FIRE_UX_ACTION
9 | import xyz.jienan.xkcd.Const.PREF_FONT
10 | import xyz.jienan.xkcd.R
11 | import xyz.jienan.xkcd.comics.activity.ImageDetailPageActivity
12 | import xyz.jienan.xkcd.home.MainActivity
13 |
14 | /**
15 | * Created by Jienan on 2018/3/9.
16 | */
17 |
18 | abstract class BaseActivity : AppCompatActivity() {
19 |
20 | private val mFirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(this) }
21 |
22 | protected val sharedPreferences: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | setTheme()
26 | super.onCreate(savedInstanceState)
27 | }
28 |
29 | protected fun logUXEvent(event: String, bundle: Bundle? = null) {
30 | mFirebaseAnalytics.logEvent(FIRE_UX_ACTION, (bundle ?: Bundle()).apply {
31 | putString(FIRE_UX_ACTION, event)
32 | })
33 | }
34 |
35 | private fun setTheme() {
36 | val fontPref = sharedPreferences.getBoolean(PREF_FONT, false)
37 | if (fontPref) {
38 | when (this) {
39 | is MainActivity -> setTheme(R.style.CustomActionBarTheme)
40 | is ImageDetailPageActivity -> setTheme(R.style.TransparentBackgroundTheme)
41 | else -> setTheme(R.style.AppBarTheme)
42 | }
43 | } else {
44 | when (this) {
45 | is MainActivity -> setTheme(R.style.CustomActionBarFontTheme)
46 | is ImageDetailPageActivity -> setTheme(R.style.TransparentBackgroundTheme)
47 | else -> setTheme(R.style.AppBarFontTheme)
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/xkcd/src/proprietary/java/xyz/jienan/xkcd/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.base
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.annotation.LayoutRes
8 | import androidx.fragment.app.Fragment
9 | import com.google.firebase.analytics.FirebaseAnalytics
10 | import xyz.jienan.xkcd.Const.FIRE_UX_ACTION
11 |
12 | abstract class BaseFragment : Fragment() {
13 |
14 | private val mFirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(requireContext()) }
15 |
16 | @get:LayoutRes
17 | protected abstract val layoutResId: Int
18 |
19 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
20 | inflater.inflate(layoutResId, container, false)
21 |
22 | protected fun logUXEvent(event: String, params: Map? = null) {
23 | val bundle = Bundle()
24 | bundle.putString(FIRE_UX_ACTION, event)
25 | if (!params.isNullOrEmpty()) {
26 | for (key in params.keys) {
27 | val value = params[key]
28 | if (value!!.matches("-?\\d+".toRegex())) {
29 | bundle.putInt(key, Integer.valueOf(value))
30 | } else {
31 | bundle.putString(key, params[key])
32 | }
33 | }
34 | }
35 | mFirebaseAnalytics.logEvent(FIRE_UX_ACTION, bundle)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/xkcd/src/release/java/xyz/jienan/xkcd/DebugUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd;
2 |
3 | import android.content.Context;
4 |
5 | import timber.log.Timber;
6 |
7 | public class DebugUtils {
8 | static boolean init() {
9 | if (BuildConfig.DEBUG) {
10 | Timber.plant(new Timber.DebugTree());
11 | }
12 | return true;
13 | }
14 |
15 | static void debugDB(Context context) {
16 | // no-ops
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/xkcd/src/release/res/raw/keep.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/xkcd/src/release/res/values/debug_placeholder.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/xkcd/src/test/java/xyz/jienan/xkcd/comics/activity/ImageWebViewActivityTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.comics.activity
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | class ImageWebViewActivityTest {
7 |
8 | private val url1663 = "https://zjn0505.github.io/xkcd-undressed/1663/#d23aed20-d830-11e9-8008-42010a8e0003"
9 |
10 | @Test
11 | fun check1663UrlParse() {
12 | val uuid = ImageWebViewActivity().extractUuidFrom1663url(url1663)
13 | assertEquals("d23aed20-d830-11e9-8008-42010a8e0003", uuid)
14 | }
15 | }
--------------------------------------------------------------------------------
/xkcd/src/test/java/xyz/jienan/xkcd/model/util/WhatIfArticleUtilTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.jienan.xkcd.model.util
2 |
3 | import okhttp3.ResponseBody
4 | import org.junit.Test
5 |
6 | class WhatIfArticleUtilTest {
7 |
8 | @Test(expected = NullPointerException::class)
9 | fun `get articles from null response body`() {
10 | WhatIfArticleUtil.getArticlesFromArchive(ResponseBody.create(null, ""))
11 | }
12 | }
--------------------------------------------------------------------------------