commit 618420f22ace326d58e63858fd927bbdb8bdc5bd Author: Josh Sharp Date: Sun Jun 21 15:18:26 2020 +1000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..78c31e6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 29 + + defaultConfig { + applicationId "au.com.joshsharp.wishkobone" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation "com.squareup.okhttp3:okhttp:4.7.2" + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation "io.coil-kt:coil:0.11.0" + implementation 'com.quiph.ui:recyclerviewfastscroller:0.1.3' + implementation 'com.google.android.material:material:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +} \ No newline at end of file diff --git a/proguard-rules.pro b/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/src/androidTest/java/au/com/joshsharp/wishkobone/ExampleInstrumentedTest.kt b/src/androidTest/java/au/com/joshsharp/wishkobone/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..87718b7 --- /dev/null +++ b/src/androidTest/java/au/com/joshsharp/wishkobone/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package au.com.joshsharp.wishkobone + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("au.com.joshsharp.wishkobone", appContext.packageName) + } +} \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e56e335 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/au/com/joshsharp/wishkobone/APIClient.java b/src/main/java/au/com/joshsharp/wishkobone/APIClient.java new file mode 100644 index 0000000..5d2b377 --- /dev/null +++ b/src/main/java/au/com/joshsharp/wishkobone/APIClient.java @@ -0,0 +1,158 @@ +package au.com.joshsharp.wishkobone; + +import android.content.Context; +import android.util.Log; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.jetbrains.annotations.NotNull; + +import okhttp3.Callback; +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.FormBody; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + + +/** + * Created by Josh on 2/09/2014. + */ +public class APIClient { + private static final String BASE_URL = "https://www.kobo.com/"; + private static OkHttpClient client; + private static String agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0"; + private String cookieValue; + + public APIClient() { + client = new OkHttpClient.Builder().retryOnConnectionFailure(false) + .build(); + + } + + public void setCookie(String value){ + cookieValue = value; + } + + public void get(String url, HashMap params, Callback responseHandler) { + HttpUrl.Builder urlBuilder = HttpUrl.parse(getAbsoluteUrl(url)).newBuilder(); + if (params != null) { + for (String key : params.keySet()) { + urlBuilder = urlBuilder.setQueryParameter(key, params.get(key)); + } + } + + Request request = new Request.Builder().url(urlBuilder.build()) + .get() + .addHeader("User-Agent", agent) + .addHeader("Referer", "https://www.kobo.com/au/en/account/wishlist") + .addHeader("Accept", "application / json, text / javascript, * / *; q = 0.01") + .addHeader("Accept-Language", "en-GB,en-AU;q=0.7,en;q=0.3") + .addHeader("X-Requested-With", "XMLHttpRequest") + .addHeader("Cookie", "KoboSession=" + cookieValue) + .build(); + client.newCall(request).enqueue(responseHandler); + } + + public void post(String url, @NotNull HashMap params, Callback responseHandler) { + this.post(url, params, "application/x-www-form-urlencoded", responseHandler); + } + + public void post(String url, @NotNull HashMap params, String contentType, Callback responseHandler) { + FormBody.Builder builder = new FormBody.Builder(); + for (String key : params.keySet()) { + builder = builder.add(key, params.get(key)); + } + + FormBody formBody = builder.build(); + + Request request = new Request.Builder().url(getAbsoluteUrl(url)).post(formBody) + .addHeader("User-Agent", agent) + .addHeader("Referer", "https://www.kobo.com/au/en/account/wishlist") + .addHeader("Accept", "application / json, text / javascript, * / *; q = 0.01") + .addHeader("Accept-Language", "en-GB,en-AU;q=0.7,en;q=0.3") + .addHeader("X-Requested-With", "XMLHttpRequest") + .addHeader("Cookie", "KoboSession=" + cookieValue) + .addHeader("Content-type", contentType) + .build(); + client.newCall(request).enqueue(responseHandler); + + } + + public void post(String url, @NotNull String payload, String contentType, Callback responseHandler){ + + RequestBody body = RequestBody.create(payload, MediaType.get(contentType)); + + Request request = new Request.Builder().url(getAbsoluteUrl(url)).post(body) + .addHeader("User-Agent", agent) + .addHeader("Referer", "https://www.kobo.com/au/en/account/wishlist") + .addHeader("Accept", "application/json, text/javascript, */*; q=0.01") + .addHeader("Accept-Language", "en-GB,en-AU;q=0.7,en;q=0.3") + .addHeader("X-Requested-With", "XMLHttpRequest") + .addHeader("Origin","https://www.kobo.com") + .addHeader("Cookie", "KoboSession=" + cookieValue) + .addHeader("Content-type", contentType) + .build(); + client.newCall(request).enqueue(responseHandler); + } + + public Response syncGet(String url, HashMap params) throws IOException { + HttpUrl.Builder urlBuilder = HttpUrl.parse(getAbsoluteUrl(url)).newBuilder(); + if (params != null) { + for (String key : params.keySet()) { + urlBuilder = urlBuilder.setQueryParameter(key, params.get(key)); + } + } + + Request request = new Request.Builder().url(urlBuilder.build()) + .get() + .addHeader("User-agent", agent) + .addHeader("Referer", "https://www.kobo.com/au/en/account/wishlist") + .addHeader("Accept", "application / json, text / javascript, * / *; q = 0.01") + .addHeader("Accept-Language", "en-GB,en-AU;q=0.7,en;q=0.3") + .addHeader("X-Requested-With", "XMLHttpRequest") + .addHeader("Cookie", "KoboSession=" + cookieValue) + .build(); + return client.newCall(request).execute(); + } + + public Response syncPost(String url, @NotNull String payload, String contentType) throws IOException { + + + RequestBody body = RequestBody.create(payload, MediaType.get(contentType)); + + Request request = new Request.Builder().url(getAbsoluteUrl(url)).post(body) + .addHeader("User-agent", agent) + .addHeader("Referer", "https://www.kobo.com/au/en/account/wishlist") + .addHeader("Accept", "application/json, text/javascript, */*; q=0.01") + .addHeader("Accept-Language", "en-GB,en-AU;q=0.7,en;q=0.3") + .addHeader("X-Requested-With", "XMLHttpRequest") + .addHeader("Cookie", String.format("KoboSession=%s;", cookieValue)) + .addHeader("Content-type", contentType) + .build(); + return client.newCall(request).execute(); + } + + public Response syncPost(String url, @NotNull HashMap params) throws IOException { + FormBody.Builder builder = new FormBody.Builder(); + for (String key : params.keySet()) { + builder = builder.add(key, params.get(key)); + } + + FormBody formBody = builder.build(); + + return this.syncPost(url, formBody.toString(), "application/x-www-form-urlencoded"); + } + + private static String getAbsoluteUrl(String relativeUrl) { + return BASE_URL + relativeUrl; + } +} diff --git a/src/main/java/au/com/joshsharp/wishkobone/Book.kt b/src/main/java/au/com/joshsharp/wishkobone/Book.kt new file mode 100644 index 0000000..d50c3dc --- /dev/null +++ b/src/main/java/au/com/joshsharp/wishkobone/Book.kt @@ -0,0 +1,19 @@ +package au.com.joshsharp.wishkobone + +class Book( + var authors: ArrayList, + var title: String, + var imageUrl: String, + var url: String, + var series: String?, + var synopsis: String, + var price: Float? = 0.0f +){ + + fun formattedPrice(): String { + if (this.price != null){ + return String.format("$%.2f", this.price) + } + return "N/A" + } +} \ No newline at end of file diff --git a/src/main/java/au/com/joshsharp/wishkobone/JsonCallback.java b/src/main/java/au/com/joshsharp/wishkobone/JsonCallback.java new file mode 100644 index 0000000..f4787c4 --- /dev/null +++ b/src/main/java/au/com/joshsharp/wishkobone/JsonCallback.java @@ -0,0 +1,63 @@ +package au.com.joshsharp.wishkobone; + +import android.util.Log; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Response; + +public abstract class JsonCallback implements Callback { + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + Object json; + try { + json = new JSONTokener(response.body().string()).nextValue(); + response.body().close(); + } catch (JSONException | NullPointerException e) { + throw new IOException(e); + } + + if (json instanceof JSONArray) { + if (response.isSuccessful()) { + this.onResponse(call, response, ((JSONArray) json)); + } else { + this.onFailureResponse(call, response, ((JSONArray) json)); + } + } else if (json instanceof JSONObject) { + if (response.isSuccessful()) { + this.onResponse(call, response, ((JSONObject) json)); + } else { + this.onFailureResponse(call, response, ((JSONObject) json)); + } + } else { + this.onFailure(call, new IOException("Couldn't parse json")); + } + } + + public void onResponse(@NotNull Call call, @NotNull Response r, @NotNull JSONObject response) { + // pass + Log.d("okhttp", "empty callback for JSONObject called"); + } + + public void onResponse(@NotNull Call call, @NotNull Response r, @NotNull JSONArray response) { + // pass + Log.d("okhttp", "empty callback for JSONArray called"); + } + + public void onFailureResponse(@NotNull Call call, @NotNull Response r, @NotNull JSONObject response) { + Log.d("okhttp", "empty failure callback for JSONObject called"); + } + + public void onFailureResponse(@NotNull Call call, @NotNull Response r, @NotNull JSONArray response) { + Log.d("okhttp", "empty failure callback for JSONArray called"); + } +} diff --git a/src/main/java/au/com/joshsharp/wishkobone/KoboCookieJar.kt b/src/main/java/au/com/joshsharp/wishkobone/KoboCookieJar.kt new file mode 100644 index 0000000..f50d8d9 --- /dev/null +++ b/src/main/java/au/com/joshsharp/wishkobone/KoboCookieJar.kt @@ -0,0 +1,31 @@ +package au.com.joshsharp.wishkobone + +import android.util.Log +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl +import java.util.ArrayList + +class KoboCookieJar: CookieJar { + + var list = ArrayList(); + + override fun loadForRequest(url: HttpUrl): List { + Log.d("cookies", "returning some cookies: ${list.joinToString(", ")}") + return list; + } + + fun setCookie(value: String){ + list.clear(); + list.add(Cookie.Builder() + .name("KoboSession") + .domain("kobo.com") + .expiresAt(2092109000000) + .value(value) + .build()); + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + // pass + } +} \ No newline at end of file diff --git a/src/main/java/au/com/joshsharp/wishkobone/LoginActivity.kt b/src/main/java/au/com/joshsharp/wishkobone/LoginActivity.kt new file mode 100644 index 0000000..b571509 --- /dev/null +++ b/src/main/java/au/com/joshsharp/wishkobone/LoginActivity.kt @@ -0,0 +1,62 @@ +package au.com.joshsharp.wishkobone + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.webkit.CookieManager +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.app.AppCompatActivity +import kotlinx.android.synthetic.main.activity_login.* + +class LoginActivity : AppCompatActivity() { + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + supportActionBar?.title = "Login to Kobo to continue" + + val cookieManager = CookieManager.getInstance() + cookieManager.setAcceptCookie(true) + cookieManager.setAcceptThirdPartyCookies(web, true) + + web.settings.javaScriptEnabled = true; + web.loadUrl("https://www.kobo.com/account/wishlist") + web.webViewClient = object: WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + + Log.d("cookie", "now at ${url}") + Log.d("cookie",getCookie(".kobo.com","KoboSession") ?: "null"); + + val cookieVal = getCookie(".kobo.com","KoboSession") + if (cookieVal != null){ + val app = application as WishkoboneApplication + app.setCookie(cookieVal) + this@LoginActivity.finish() + } + } + } + } + + fun getCookie(siteName: String, name: String): String? { + + val manager = CookieManager.getInstance() + manager.getCookie(siteName)?.let {cookies -> + val typedArray = cookies.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (element in typedArray) { + val split = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + + if(split.size >= 2) { + if (split[0].trim() == name) { + return split[1] + } + } + + } + } + + return null + } +} \ No newline at end of file diff --git a/src/main/java/au/com/joshsharp/wishkobone/MainActivity.kt b/src/main/java/au/com/joshsharp/wishkobone/MainActivity.kt new file mode 100644 index 0000000..b3f4157 --- /dev/null +++ b/src/main/java/au/com/joshsharp/wishkobone/MainActivity.kt @@ -0,0 +1,328 @@ +package au.com.joshsharp.wishkobone + +import android.annotation.SuppressLint +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.net.Uri +import android.os.Bundle +import android.text.Html +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.core.content.ContextCompat.startActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.api.load +import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_SHORT +import com.google.android.material.snackbar.Snackbar +import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller +import kotlinx.android.synthetic.main.activity_main.* +import okhttp3.Call +import okhttp3.Response +import org.json.JSONObject +import java.io.IOException +import java.util.* +import kotlin.collections.ArrayList + +class MainActivity : AppCompatActivity() { + + var hasCookie = false + val viewAdapter = BookAdapter(ArrayList()) + var totalPages = 1 + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val viewManager = LinearLayoutManager(this) + + status_text.text = "Loading..." + + list.apply { + // use this setting to improve performance if you know that changes + // in content do not change the layout size of the RecyclerView + setHasFixedSize(true) + + // use a linear layout manager + layoutManager = viewManager + + // specify an viewAdapter (see also next example) + adapter = viewAdapter + } + + + val app = application as WishkoboneApplication + + val cookie = app.getCookie() + if (cookie != null) { + hasCookie = true + loadBooks() + } else { + doLogin() + } + + + } + + fun doLogin(){ + val login = Intent(this, LoginActivity::class.java) + startActivity(login); + } + + override fun onResume() { + super.onResume() + + if (!hasCookie) { + val app = application as WishkoboneApplication + val cookie = app.getCookie() + if (cookie != null) { // we just got this via login + + loadBooks() + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the options menu from XML + val inflater = menuInflater + inflater.inflate(R.menu.menu_main, menu) + + // Get the SearchView and set the searchable configuration + val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager + (menu.findItem(R.id.menu_search).actionView as SearchView).apply { + val view = this + // Assumes current activity is the searchable activity + setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + view.clearFocus() + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + viewAdapter.filter(newText) + return true + } + + }) + setIconifiedByDefault(true) + + setOnCloseListener { + viewAdapter.filter(null) // reset + return@setOnCloseListener false + } + } + + return true + } + + + @SuppressLint("SetTextI18n") + private fun loadBooks(page: Int = 1) { + runOnUiThread { + status.visibility = View.VISIBLE + status_text.text = "Loading page $page of $totalPages" + } + + val app = application as WishkoboneApplication + app.client.post( + "account/wishlist/fetch", + "{\"pageIndex\":$page}", + "application/json", + object : + JsonCallback() { + override fun onFailure(call: Call, e: IOException) { + hasCookie = false + } + + override fun onResponse(call: Call, r: Response, response: JSONObject) { + if (!response.has("TotalNumPages")){ + // must be unauthenticated + hasCookie = false + runOnUiThread { + app.invalidateCookie() + doLogin() + } + return + } + totalPages = response.getInt("TotalNumPages") + val array = response.getJSONArray("Items") + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + + val authors = obj.getJSONArray("Authors") + val authorList = ArrayList() + for (j in 0 until authors.length()) { + authorList.add(authors.getJSONObject(j).getString("Name")) + } + + val price = obj.getString("Price").substring(1).toFloatOrNull() + var series: String? = null + if (obj.has("Series") && !obj.isNull("Series")) { + series = obj.getString("Series") + if (obj.has("SeriesNumber") && !obj.isNull("SeriesNumber")) { + series += " ${obj.getString("SeriesNumber")}" + } + } + + runOnUiThread { + viewAdapter.books.add( + Book( + authorList, + obj.getString("Title"), + "https:" + obj.getString("ImageUrl"), + "http://www.kobo.com" + obj.getString("ProductUrl"), + series, + obj.getString("Synopsis"), + price + ) + ) + } + + } + hasCookie = true + runOnUiThread { + viewAdapter.sortBooks() + } + + if (array.length() == 12) { + // full page, probably worth going again + loadBooks(page + 1) + } else { + // the end! + runOnUiThread { + status_text.text = + "Load complete! ${viewAdapter.books.size} items shown." + status.postDelayed({ + status.visibility = View.GONE + }, 3000) + } + } + } + + override fun onFailureResponse(call: Call, r: Response, response: JSONObject) { + hasCookie = false + } + }); + } + + class BookAdapter(val books: ArrayList) : + RecyclerView.Adapter(), + RecyclerViewFastScroller.OnPopupTextUpdate { + // Provide a reference to the views for each data item + // Complex data items may need more than one view per item, and + // you provide access to all the views for a data item in a view holder. + // Each data item is just a string in this case that is shown in a TextView. + private var filteredBooks: ArrayList = ArrayList() + + init { + filteredBooks.addAll(books) + } + + class BookViewHolder( + val title: TextView, + val authors: TextView, + val price: TextView, + val series: TextView, + val synopsis: TextView, + val cover: ImageView, + itemView: View + ) : RecyclerView.ViewHolder( + itemView + ) + + fun filter(query: String?) { + if (query == null) { + filteredBooks.clear() + filteredBooks.addAll(books) + + } else { + + Log.d("filter", "looking for $query") + + filteredBooks = books.filter { + it.title.contains(query, true) + || it.authors.joinToString(", ").contains(query, true) + || it.series?.contains(query, true) ?: false + } as ArrayList + } + notifyDataSetChanged() + } + + fun sortBooks() { + books.sortBy { + if (it.price != null) { + it.price + } else { + 9999f + } + } + filteredBooks.clear() + filteredBooks.addAll(books) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookViewHolder { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.book_item, parent, false) as ViewGroup + // set the view's size, margins, paddings and layout parameters + + return BookViewHolder( + layout.findViewById(R.id.title), layout.findViewById(R.id.authors), + layout.findViewById(R.id.price), layout.findViewById(R.id.series), + layout.findViewById(R.id.synopsis), layout.findViewById(R.id.cover), + layout + ) + + } + + override fun onBindViewHolder(holder: BookViewHolder, position: Int) { + val book = filteredBooks[position] + holder.title.text = book.title + holder.authors.text = book.authors.joinToString(", ") + if (book.price != null) { + holder.price.text = String.format("$%.2f", book.price) + } else { + holder.price.text = "N/A" + } + + holder.synopsis.text = Html.fromHtml(book.synopsis).toString().trim() + if (book.series != null) { + holder.series.visibility = View.VISIBLE + holder.series.text = book.series + } else { + holder.series.visibility = View.GONE + } + + holder.cover.load(book.imageUrl) { + crossfade(true) + } + + holder.itemView.setOnClickListener { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(book.url)) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK) + startActivity(it.context, intent, null) + } + } + + override fun getItemCount(): Int { + return filteredBooks.size + } + + override fun onChange(position: Int): CharSequence { + return filteredBooks[position].formattedPrice() + } + + } +} + + diff --git a/src/main/java/au/com/joshsharp/wishkobone/WishkoboneApplication.kt b/src/main/java/au/com/joshsharp/wishkobone/WishkoboneApplication.kt new file mode 100644 index 0000000..98b9dba --- /dev/null +++ b/src/main/java/au/com/joshsharp/wishkobone/WishkoboneApplication.kt @@ -0,0 +1,37 @@ +package au.com.joshsharp.wishkobone + +import android.app.Application +import android.content.SharedPreferences + +class WishkoboneApplication: Application() { + + var client = APIClient() + + override fun onCreate() { + super.onCreate() + if (getCookie() != null){ + client.setCookie(getCookie()) + } + } + + fun getCookie(): String? { + val prefs = getSharedPreferences(getString(R.string.app_name), 0) + return prefs.getString("cookie",null) + } + + fun setCookie(cookie: String) { + val editor = getSharedPreferences(getString(R.string.app_name), 0).edit() + editor.putString("cookie", cookie) + editor.apply() + client.setCookie(cookie) + + } + + fun invalidateCookie() { + val editor = getSharedPreferences(getString(R.string.app_name), 0).edit() + editor.clear() + editor.apply() + client = APIClient() // remove cookie + + } +} \ No newline at end of file diff --git a/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/fast_scroll_thumb.xml b/src/main/res/drawable/fast_scroll_thumb.xml new file mode 100644 index 0000000..71241bf --- /dev/null +++ b/src/main/res/drawable/fast_scroll_thumb.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/fast_scroll_track.xml b/src/main/res/drawable/fast_scroll_track.xml new file mode 100644 index 0000000..d58a8d4 --- /dev/null +++ b/src/main/res/drawable/fast_scroll_track.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_launcher_background.xml b/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..12578c6 --- /dev/null +++ b/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,3 @@ + + diff --git a/src/main/res/drawable/scroller_popup.xml b/src/main/res/drawable/scroller_popup.xml new file mode 100644 index 0000000..f7eef4c --- /dev/null +++ b/src/main/res/drawable/scroller_popup.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/scroller_thumb.xml b/src/main/res/drawable/scroller_thumb.xml new file mode 100644 index 0000000..e1f24ea --- /dev/null +++ b/src/main/res/drawable/scroller_thumb.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/layout/activity_login.xml b/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..9be7df8 --- /dev/null +++ b/src/main/res/layout/activity_login.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/activity_main.xml b/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..9ce8318 --- /dev/null +++ b/src/main/res/layout/activity_main.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/book_item.xml b/src/main/res/layout/book_item.xml new file mode 100644 index 0000000..97fc336 --- /dev/null +++ b/src/main/res/layout/book_item.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/menu/menu_main.xml b/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..4ef67c6 --- /dev/null +++ b/src/main/res/menu/menu_main.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..791f197 --- /dev/null +++ b/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..791f197 --- /dev/null +++ b/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/mipmap-hdpi/ic_launcher.png b/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/main/res/mipmap-hdpi/ic_launcher_round.png b/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src/main/res/mipmap-hdpi/logo.png b/src/main/res/mipmap-hdpi/logo.png new file mode 100644 index 0000000..cee0dd0 Binary files /dev/null and b/src/main/res/mipmap-hdpi/logo.png differ diff --git a/src/main/res/mipmap-mdpi/ic_launcher.png b/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/main/res/mipmap-mdpi/ic_launcher_round.png b/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src/main/res/mipmap-mdpi/logo.png b/src/main/res/mipmap-mdpi/logo.png new file mode 100644 index 0000000..cee0dd0 Binary files /dev/null and b/src/main/res/mipmap-mdpi/logo.png differ diff --git a/src/main/res/mipmap-xhdpi/ic_launcher.png b/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src/main/res/mipmap-xhdpi/logo.png b/src/main/res/mipmap-xhdpi/logo.png new file mode 100644 index 0000000..cee0dd0 Binary files /dev/null and b/src/main/res/mipmap-xhdpi/logo.png differ diff --git a/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src/main/res/mipmap-xxhdpi/logo.png b/src/main/res/mipmap-xxhdpi/logo.png new file mode 100644 index 0000000..cee0dd0 Binary files /dev/null and b/src/main/res/mipmap-xxhdpi/logo.png differ diff --git a/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src/main/res/mipmap-xxxhdpi/logo.png b/src/main/res/mipmap-xxxhdpi/logo.png new file mode 100644 index 0000000..84f8766 Binary files /dev/null and b/src/main/res/mipmap-xxxhdpi/logo.png differ diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml new file mode 100644 index 0000000..f4eda99 --- /dev/null +++ b/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #bf2026 + #ab1d22 + #4218bb + #e0e0e0 + #888888 + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml new file mode 100644 index 0000000..d706f96 --- /dev/null +++ b/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Wishkobone + \ No newline at end of file diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml new file mode 100644 index 0000000..b2cfe55 --- /dev/null +++ b/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/src/test/java/au/com/joshsharp/wishkobone/ExampleUnitTest.kt b/src/test/java/au/com/joshsharp/wishkobone/ExampleUnitTest.kt new file mode 100644 index 0000000..628b2e3 --- /dev/null +++ b/src/test/java/au/com/joshsharp/wishkobone/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package au.com.joshsharp.wishkobone + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file