commit 2a7814798bab8a20d149d3cbb15d3e72b0b432f5 Author: tom5079 Date: Sat May 11 22:59:12 2019 +0900 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2b75303a --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..cb22ebb7 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..15a15b21 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..3e352b83 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..146ab09b --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..7631aec3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..7f68460d --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..bf27adc7 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,44 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "xyz.quaver.pupil" + minSdkVersion 15 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.preference:preference:1.1.0-alpha05' + implementation 'com.google.android.material:material:1.0.0' + implementation 'com.github.arimorty:floatingsearchview:2.1.1' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation project(path: ':libpupil') +} + +androidExtensions { + experimental = true +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/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/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..73714252 --- /dev/null +++ b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt @@ -0,0 +1,53 @@ +package xyz.quaver.pupil + +import android.graphics.BitmapFactory +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File + +/** + * 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("xyz.quaver.pupil", appContext.packageName) + } + + @Test + fun checkCacheDir() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + val file = File(appContext.cacheDir, "imageCache/1412251/01.jpg.webp") + + val bitmap = BitmapFactory.decodeFile(file.absolutePath) + + Log.d("Pupil", bitmap.byteCount.toString()) + } + + @Test + @ExperimentalUnsignedTypes + fun test_doSearch() { + Log.d("TEST", "Starting...") + + runBlocking { + CoroutineScope(Dispatchers.Main).launch { + Log.d("TEST", "This is started! wow") + }.join() + } + + Log.d("TEST", "Finished! ...Really?") + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ae7692db --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 00000000..18fdf67b Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ diff --git a/app/src/main/java/xyz/quaver/pupil/GalleryActivity.kt b/app/src/main/java/xyz/quaver/pupil/GalleryActivity.kt new file mode 100644 index 00000000..8b397b2d --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/GalleryActivity.kt @@ -0,0 +1,127 @@ +package xyz.quaver.pupil + +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import androidx.appcompat.app.AppCompatActivity +import kotlinx.android.synthetic.main.activity_gallery.* +import kotlinx.coroutines.* +import xyz.quaver.hitomi.Reader +import xyz.quaver.hitomi.getReader +import xyz.quaver.hitomi.getReferer +import xyz.quaver.pupil.adapters.GalleryAdapter +import java.io.File +import java.io.FileOutputStream +import java.net.URL +import javax.net.ssl.HttpsURLConnection + +class GalleryActivity : AppCompatActivity() { + + private val images = ArrayList() + private var galleryID = 0 + private lateinit var reader: Deferred + private var loadJob: Job? = null + private var screenMode = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_gallery) + + galleryID = intent.getIntExtra("GALLERY_ID", 0) + CoroutineScope(Dispatchers.Unconfined).launch { + reader = async(Dispatchers.IO) { + getReader(galleryID) + } + } + + initView() + loadImages() + } + + override fun onDestroy() { + super.onDestroy() + loadJob?.cancel() + } + + private fun initView() { + gallery_recyclerview.adapter = GalleryAdapter(images).apply { + setOnClick { + val attrs = window.attributes + + screenMode = (screenMode+1)%2 + + when(screenMode) { + 0 -> { + attrs.flags = attrs.flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv() + supportActionBar?.show() + } + 1 -> { + attrs.flags = attrs.flags or WindowManager.LayoutParams.FLAG_FULLSCREEN + supportActionBar?.hide() + } + } + window.attributes = attrs + } + } + } + + private fun loadImages() { + + fun webpUrlFromUrl(url: URL) = URL(url.toString().replace("/galleries/", "/webp/") + ".webp") + + loadJob = CoroutineScope(Dispatchers.Default).launch { + val reader = reader.await() + + launch(Dispatchers.Main) { + supportActionBar?.title = reader.title + + with(gallery_progressbar) { + max = reader.images.size + progress = 0 + + visibility = View.VISIBLE + } + } + + reader.images.chunked(8).forEach { chunked -> + chunked.map { + async(Dispatchers.IO) { + val url = if (it.second?.haswebp == 1) webpUrlFromUrl(it.first) else it.first + + val fileName: String + + with(url.path) { + fileName = substring(lastIndexOf('/')+1) + } + + val cache = File(cacheDir, "/imageCache/$galleryID/$fileName") + + if (!cache.exists()) + with(url.openConnection() as HttpsURLConnection) { + setRequestProperty("Referer", getReferer(galleryID)) + + if (!cache.parentFile.exists()) + cache.parentFile.mkdirs() + + inputStream.copyTo(FileOutputStream(cache)) + } + + cache.absolutePath + } + }.forEach { + val cache = it.await() + + launch(Dispatchers.Main) { + images.add(cache) + gallery_recyclerview.adapter?.notifyItemInserted(images.size - 1) + gallery_progressbar.progress++ + } + } + } + + launch(Dispatchers.Main) { + gallery_progressbar.visibility = View.GONE + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/MainActivity.kt new file mode 100644 index 00000000..f06503d8 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/MainActivity.kt @@ -0,0 +1,296 @@ +package xyz.quaver.pupil + +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Bundle +import android.preference.PreferenceManager +import android.text.* +import android.text.style.AlignmentSpan +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.arlib.floatingsearchview.FloatingSearchView +import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion +import com.arlib.floatingsearchview.util.view.SearchInputView +import com.google.android.material.appbar.AppBarLayout +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.coroutines.* +import xyz.quaver.hitomi.* +import xyz.quaver.pupil.adapters.GalleryBlockAdapter +import xyz.quaver.pupil.types.TagSuggestion +import xyz.quaver.pupil.util.SetLineOverlap +import javax.net.ssl.HttpsURLConnection + +class MainActivity : AppCompatActivity() { + + private val galleries = ArrayList>() + + private var isLoading = false + private var query = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + main_appbar_layout.addOnOffsetChangedListener( + AppBarLayout.OnOffsetChangedListener { _, p1 -> + main_searchview.translationY = p1.toFloat() + main_recyclerview.translationY = p1.toFloat() + } + ) + + with(main_swipe_layout) { + setProgressViewOffset(false, 0, resources.getDimensionPixelSize(R.dimen.progress_view_offset)) + + setOnRefreshListener { + runBlocking { + cleanJob?.join() + } + fetchGalleries(query, true) + } + } + + setupRecyclerView() + setupSearchBar() + fetchGalleries(query) + } + + private fun setupRecyclerView() { + with(main_recyclerview) { + adapter = GalleryBlockAdapter(galleries).apply { + setClickListener { + val intent = Intent(this@MainActivity, GalleryActivity::class.java) + intent.putExtra("GALLERY_ID", it) + + //TODO: Maybe sprinke some transitions will be nice :D + startActivity(intent) + } + } + addOnScrollListener( + object: RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + + if (!isLoading) + if (layoutManager.findLastCompletelyVisibleItemPosition() == galleries.size) + fetchGalleries(query) + } + } + ) + } + } + + private var suggestionJob : Job? = null + private fun setupSearchBar() { + val searchInputView = findViewById(R.id.search_bar_text) + //Change upper case letters to lower case + searchInputView.addTextChangedListener(object: TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + + } + + override fun afterTextChanged(s: Editable?) { + s ?: return + + if (s.any { it.isUpperCase() }) + s.replace(0, s.length, s.toString().toLowerCase()) + } + }) + + with(main_searchview as FloatingSearchView) { + setOnMenuItemClickListener { + when(it.itemId) { + R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) + R.id.main_menu_search -> setSearchFocused(true) + } + } + + setOnQueryChangeListener { _, query -> + clearSuggestions() + + if (query.isEmpty() or query.endsWith(' ')) + return@setOnQueryChangeListener + + val currentQuery = query.split(" ").last().replace('_', ' ') + + suggestionJob?.cancel() + + suggestionJob = CoroutineScope(Dispatchers.IO).launch { + val suggestions = getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) } + + withContext(Dispatchers.Main) { + swapSuggestions(suggestions) + } + } + } + + setOnBindSuggestionCallback { _, leftIcon, textView, item, _ -> + val suggestion = item as TagSuggestion + + leftIcon.setImageDrawable( + ResourcesCompat.getDrawable( + resources, + when(suggestion.n) { + "female" -> R.drawable.ic_gender_female + "male" -> R.drawable.ic_gender_male + "language" -> R.drawable.ic_translate + "group" -> R.drawable.ic_account_group + "character" -> R.drawable.ic_account_star + "series" -> R.drawable.ic_book_open + "artist" -> R.drawable.ic_brush + else -> R.drawable.ic_tag + }, + null) + ) + + val text = "${suggestion.s}\n ${suggestion.t}" + + val len = text.length + val left = suggestion.s.length + + textView.text = SpannableString(text).apply { + val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE) + setSpan(s, left, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + setSpan(SetLineOverlap(true), 1, len-2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + setSpan(SetLineOverlap(false), len-1, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + setOnSearchListener(object : FloatingSearchView.OnSearchListener { + override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) { + val suggestion = searchSuggestion as TagSuggestion + + with(searchInputView.text) { + delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length) + append("${suggestion.n}:${suggestion.s.replace(Regex("\\s"), "_")} ") + } + + clearSuggestions() + } + + override fun onSearchAction(currentQuery: String?) { + //Do search on onFocusCleared() + } + }) + + setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener { + override fun onFocus() { + //Do Nothing + } + + override fun onFocusCleared() { + suggestionJob?.cancel() + + val query = searchInputView.text.toString() + + if (query != this@MainActivity.query) { + this@MainActivity.query = query + + fetchGalleries(query, true) + } + } + }) + } + } + + private val cache = ArrayList() + private var currentFetchingJob: Job? = null + private var cleanJob: Job? = null + + private fun cancelFetch() { + isLoading = false + + runBlocking { + cleanJob?.join() + currentFetchingJob?.cancelAndJoin() + } + } + + private fun fetchGalleries(query: String, clear: Boolean = false) { + val preference = PreferenceManager.getDefaultSharedPreferences(this) + val perPage = preference.getString("per_page", "25")?.toInt() ?: 25 + val defaultQuery = preference.getString("default_query", "")!! + + if (clear) { + cancelFetch() + cleanJob = CoroutineScope(Dispatchers.Main).launch { + cache.clear() + galleries.clear() + + main_recyclerview.adapter?.notifyDataSetChanged() + + main_noresult.visibility = View.INVISIBLE + main_progressbar.show() + main_swipe_layout.isRefreshing = false + } + } + + if (isLoading) + return + + isLoading = true + + currentFetchingJob = CoroutineScope(Dispatchers.IO).launch { + try { + val galleryIDs: List + + cleanJob?.join() + + if (query.isEmpty() && defaultQuery.isEmpty()) + galleryIDs = fetchNozomi(start = galleries.size, count = perPage) + else { + if (cache.isEmpty()) + cache.addAll(doSearch("$defaultQuery $query")) + + galleryIDs = cache.slice(galleries.size until Math.min(galleries.size + perPage, cache.size)) + + with(main_recyclerview.adapter as GalleryBlockAdapter) { + noMore = galleries.size + perPage >= cache.size + } + } + + if (query.isNotEmpty() and defaultQuery.isNotEmpty() and cache.isNullOrEmpty()) { + withContext(Dispatchers.Main) { + main_noresult.visibility = View.VISIBLE + main_progressbar.hide() + } + } + + galleryIDs.chunked(4).forEach { chunked -> + chunked.map { + async { + val galleryBlock = getGalleryBlock(it) + val thumbnail: Bitmap + + with(galleryBlock.thumbnails[0].openConnection() as HttpsURLConnection) { + thumbnail = BitmapFactory.decodeStream(inputStream) + } + + Pair(galleryBlock, thumbnail) + } + }.forEach { + val galleryBlock = it.await() + + withContext(Dispatchers.Main) { + main_progressbar.hide() + + galleries.add(galleryBlock) + main_recyclerview.adapter?.notifyItemInserted(galleries.size - 1) + } + } + } + } finally { + isLoading = false + } + } + } +} diff --git a/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt b/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt new file mode 100644 index 00000000..f179742e --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/SettingsActivity.kt @@ -0,0 +1,80 @@ +package xyz.quaver.pupil + +import android.os.Bundle +import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat + +class SettingsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.settings_activity) + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, SettingsFragment()) + .commit() + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + class SettingsFragment : PreferenceFragmentCompat() { + + private val suffix = listOf( + "B", + "kB", + "MB", + "GB", + "TB" //really? + ) + + private fun getCacheSize() : String { + var size = context!!.cacheDir.walk().map { it.length() }.sum() + var suffixIndex = 0 + + while (size >= 1024) { + size /= 1024 + suffixIndex++ + } + + return getString(R.string.settings_delete_cache_summary, size, suffix[suffixIndex]) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.root_preferences, rootKey) + + with(findPreference("delete_cache")) { + this ?: return@with + + summary = getCacheSize() + + setOnPreferenceClickListener { + AlertDialog.Builder(context).apply { + setTitle(getString(R.string.settings_delete_cache_alert_title)) + setMessage(getString(R.string.settings_delete_cache_alert_message)) + setPositiveButton(android.R.string.yes) { _, _ -> + with(context.cacheDir) { + if (exists()) + deleteRecursively() + } + + summary = getCacheSize() + } + setNegativeButton(android.R.string.no) { _, _ -> } + }.show() + + true + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + android.R.id.home -> onBackPressed() + } + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryAdapter.kt new file mode 100644 index 00000000..5d1febdc --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryAdapter.kt @@ -0,0 +1,59 @@ +package xyz.quaver.pupil.adapters + +import android.graphics.BitmapFactory +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import xyz.quaver.pupil.R + +class GalleryAdapter(private val images: List) : RecyclerView.Adapter() { + + class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view) + + private var onClick: (() -> Unit)? = null + fun setOnClick(callback: (() -> Unit)?) { + this.onClick = callback + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + LayoutInflater.from(parent.context).inflate( + R.layout.item_gallery, parent, false + ).let { + return ViewHolder(it as ImageView) + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + with(holder.view) { + setOnClickListener { + onClick?.invoke() + } + + CoroutineScope(Dispatchers.Default).launch { + val options = BitmapFactory.Options() + + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(images[position], options) + + options.inSampleSize = options.outWidth / + context.resources.displayMetrics.widthPixels + + options.inJustDecodeBounds = false + + val image = BitmapFactory.decodeFile(images[position], options) + + launch(Dispatchers.Main) { + setImageBitmap(image) + } + } + } + } + + override fun getItemCount() = images.size + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt new file mode 100644 index 00000000..b94db781 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt @@ -0,0 +1,109 @@ +package xyz.quaver.pupil.adapters + +import android.graphics.Bitmap +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip +import kotlinx.android.synthetic.main.item_galleryblock.view.* +import xyz.quaver.hitomi.GalleryBlock +import xyz.quaver.hitomi.toTag +import xyz.quaver.pupil.R + +class GalleryBlockAdapter(private val galleries: List>) : RecyclerView.Adapter() { + + private enum class ViewType { + VIEW_ITEM, + VIEW_PROG + } + + private fun String.wordCapitalize() : String { + val result = ArrayList() + + for (word in this.split(" ")) + result.add(word.capitalize()) + + return result.joinToString(" ") + } + + var noMore = false + + class ViewHolder(val view: CardView) : RecyclerView.ViewHolder(view) + class ProgressViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view) + + private var callback: ((Int) -> Unit)? = null + fun setClickListener(callback: ((Int) -> Unit)?) { + this.callback = callback + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + when(viewType) { + ViewType.VIEW_ITEM.ordinal -> { + val view = LayoutInflater.from(parent.context).inflate( + R.layout.item_galleryblock, parent, false + ) as CardView + + return ViewHolder(view) + } + ViewType.VIEW_PROG.ordinal -> { + val view = LayoutInflater.from(parent.context).inflate( + R.layout.item_progressbar, parent, false + ) as LinearLayout + + return ProgressViewHolder(view) + } + } + + throw Exception("Unexpected ViewType") + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is ViewHolder) { + with(holder.view) { + val resources = context.resources + val languages = resources.getStringArray(R.array.languages).map { + it.split("|").let { split -> + Pair(split[0], split[1]) + } + }.toMap() + val (gallery, thumbnail) = galleries[position] + + val artists = gallery.artists.ifEmpty { listOf("N/A") } + val series = gallery.series.ifEmpty { listOf("N/A") } + + setOnClickListener { + callback?.invoke(gallery.id) + } + + galleryblock_thumbnail.setImageBitmap(thumbnail) + galleryblock_title.text = gallery.title + galleryblock_artist.text = artists.joinToString(", ") { it.wordCapitalize() } + galleryblock_series.text = + resources.getString(R.string.galleryblock_series, series.joinToString(", ") { it.wordCapitalize() }) + galleryblock_type.text = resources.getString(R.string.galleryblock_type, gallery.type).wordCapitalize() + galleryblock_language.text = + resources.getString(R.string.galleryblock_language, languages[gallery.language]) + + galleryblock_tag_group.removeAllViews() + gallery.relatedTags.forEach { + galleryblock_tag_group.addView( + Chip(context).apply { + text = it.toTag().wordCapitalize() + } + ) + } + } + } + } + + override fun getItemCount() = if (galleries.isEmpty()) 0 else galleries.size+(if (noMore) 0 else 1) + + override fun getItemViewType(position: Int): Int { + return when { + galleries.getOrNull(position) == null -> ViewType.VIEW_PROG.ordinal + else -> ViewType.VIEW_ITEM.ordinal + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/types/TagSuggestion.kt b/app/src/main/java/xyz/quaver/pupil/types/TagSuggestion.kt new file mode 100644 index 00000000..2b39db9f --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/types/TagSuggestion.kt @@ -0,0 +1,14 @@ +package xyz.quaver.pupil.types + +import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion +import kotlinx.android.parcel.Parcelize +import xyz.quaver.hitomi.Suggestion + +@Parcelize +data class TagSuggestion constructor(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion { + constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n) + + override fun getBody(): String { + return s + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/SetLineOverlap.kt b/app/src/main/java/xyz/quaver/pupil/util/SetLineOverlap.kt new file mode 100644 index 00000000..8c1d5b4b --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/util/SetLineOverlap.kt @@ -0,0 +1,37 @@ +package xyz.quaver.pupil.util + +import android.graphics.Paint +import android.text.style.LineHeightSpan + +class SetLineOverlap(private val overlap: Boolean) : LineHeightSpan { + companion object { + private var originalBottom = 15 + private var originalDescent = 13 + private var overlapSaved = false + } + + override fun chooseHeight( + text: CharSequence?, + start: Int, + end: Int, + spanstartv: Int, + lineHeight: Int, + fm: Paint.FontMetricsInt? + ) { + fm ?: return + + if (overlap) { + if (overlapSaved) { + originalBottom = fm.bottom + originalDescent = fm.descent + overlapSaved = true + } + fm.bottom += fm.top + fm.descent += fm.top + } else { + fm.bottom = originalBottom + fm.descent = originalDescent + overlapSaved = false + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_expand.xml b/app/src/main/res/drawable-anydpi/ic_expand.xml new file mode 100644 index 00000000..46ffbc0f --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_expand.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_search.xml b/app/src/main/res/drawable-anydpi/ic_search.xml new file mode 100644 index 00000000..4da90631 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_settings.xml b/app/src/main/res/drawable-anydpi/ic_settings.xml new file mode 100644 index 00000000..994eaacb --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-hdpi/ic_account_group.png b/app/src/main/res/drawable-hdpi/ic_account_group.png new file mode 100644 index 00000000..2e8279db Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_account_group.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_account_star.png b/app/src/main/res/drawable-hdpi/ic_account_star.png new file mode 100644 index 00000000..53b16e5d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_account_star.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_book_open.png b/app/src/main/res/drawable-hdpi/ic_book_open.png new file mode 100644 index 00000000..144febb1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_book_open.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_brush.png b/app/src/main/res/drawable-hdpi/ic_brush.png new file mode 100644 index 00000000..5316c732 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_brush.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_expand.png b/app/src/main/res/drawable-hdpi/ic_expand.png new file mode 100644 index 00000000..87c8ec07 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_expand.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_gender_female.png b/app/src/main/res/drawable-hdpi/ic_gender_female.png new file mode 100644 index 00000000..76a7d4c4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_gender_female.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_gender_male.png b/app/src/main/res/drawable-hdpi/ic_gender_male.png new file mode 100644 index 00000000..4ee6cd07 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_gender_male.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_search.png b/app/src/main/res/drawable-hdpi/ic_search.png new file mode 100644 index 00000000..7ca567fe Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_search.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings.png b/app/src/main/res/drawable-hdpi/ic_settings.png new file mode 100644 index 00000000..24ba874a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_settings.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_tag.png b/app/src/main/res/drawable-hdpi/ic_tag.png new file mode 100644 index 00000000..aa3b8750 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_tag.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_translate.png b/app/src/main/res/drawable-hdpi/ic_translate.png new file mode 100644 index 00000000..1a8f4365 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_translate.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_account_group.png b/app/src/main/res/drawable-ldpi/ic_account_group.png new file mode 100644 index 00000000..c7cbb8cb Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_account_group.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_account_star.png b/app/src/main/res/drawable-ldpi/ic_account_star.png new file mode 100644 index 00000000..4373f45f Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_account_star.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_book_open.png b/app/src/main/res/drawable-ldpi/ic_book_open.png new file mode 100644 index 00000000..1b1c6020 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_book_open.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_brush.png b/app/src/main/res/drawable-ldpi/ic_brush.png new file mode 100644 index 00000000..48baadd4 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_brush.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_gender_female.png b/app/src/main/res/drawable-ldpi/ic_gender_female.png new file mode 100644 index 00000000..1070b7d4 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_gender_female.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_gender_male.png b/app/src/main/res/drawable-ldpi/ic_gender_male.png new file mode 100644 index 00000000..fd054d81 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_gender_male.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_tag.png b/app/src/main/res/drawable-ldpi/ic_tag.png new file mode 100644 index 00000000..39459491 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_tag.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_translate.png b/app/src/main/res/drawable-ldpi/ic_translate.png new file mode 100644 index 00000000..ffe6baf7 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_translate.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_account_group.png b/app/src/main/res/drawable-mdpi/ic_account_group.png new file mode 100644 index 00000000..314120c4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_account_group.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_account_star.png b/app/src/main/res/drawable-mdpi/ic_account_star.png new file mode 100644 index 00000000..48b75fa6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_account_star.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_book_open.png b/app/src/main/res/drawable-mdpi/ic_book_open.png new file mode 100644 index 00000000..b76cd538 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_book_open.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_brush.png b/app/src/main/res/drawable-mdpi/ic_brush.png new file mode 100644 index 00000000..48ec2743 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_brush.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_expand.png b/app/src/main/res/drawable-mdpi/ic_expand.png new file mode 100644 index 00000000..4da33c07 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_expand.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_gender_female.png b/app/src/main/res/drawable-mdpi/ic_gender_female.png new file mode 100644 index 00000000..7000159c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_gender_female.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_gender_male.png b/app/src/main/res/drawable-mdpi/ic_gender_male.png new file mode 100644 index 00000000..211cee7a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_gender_male.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_search.png b/app/src/main/res/drawable-mdpi/ic_search.png new file mode 100644 index 00000000..fd0d5163 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_search.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings.png b/app/src/main/res/drawable-mdpi/ic_settings.png new file mode 100644 index 00000000..c891ad02 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_settings.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_tag.png b/app/src/main/res/drawable-mdpi/ic_tag.png new file mode 100644 index 00000000..42e8255f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_tag.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_translate.png b/app/src/main/res/drawable-mdpi/ic_translate.png new file mode 100644 index 00000000..1d4c32d6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_translate.png differ diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..6348baae --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/ic_account_group.png b/app/src/main/res/drawable-xhdpi/ic_account_group.png new file mode 100644 index 00000000..4e6389bc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_account_group.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_account_star.png b/app/src/main/res/drawable-xhdpi/ic_account_star.png new file mode 100644 index 00000000..a8dc7cc7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_account_star.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_book_open.png b/app/src/main/res/drawable-xhdpi/ic_book_open.png new file mode 100644 index 00000000..dc925c77 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_book_open.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_brush.png b/app/src/main/res/drawable-xhdpi/ic_brush.png new file mode 100644 index 00000000..01fde729 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_brush.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand.png b/app/src/main/res/drawable-xhdpi/ic_expand.png new file mode 100644 index 00000000..b2baf41d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_expand.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_gender_female.png b/app/src/main/res/drawable-xhdpi/ic_gender_female.png new file mode 100644 index 00000000..734e0d99 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_gender_female.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_gender_male.png b/app/src/main/res/drawable-xhdpi/ic_gender_male.png new file mode 100644 index 00000000..89a1948d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_gender_male.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_search.png b/app/src/main/res/drawable-xhdpi/ic_search.png new file mode 100644 index 00000000..f4b5524d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_search.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings.png b/app/src/main/res/drawable-xhdpi/ic_settings.png new file mode 100644 index 00000000..92d74d24 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_tag.png b/app/src/main/res/drawable-xhdpi/ic_tag.png new file mode 100644 index 00000000..23658b82 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_tag.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_translate.png b/app/src/main/res/drawable-xhdpi/ic_translate.png new file mode 100644 index 00000000..73a5393b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_translate.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_account_group.png b/app/src/main/res/drawable-xxhdpi/ic_account_group.png new file mode 100644 index 00000000..2080d107 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_account_group.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_account_star.png b/app/src/main/res/drawable-xxhdpi/ic_account_star.png new file mode 100644 index 00000000..ca802350 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_account_star.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_book_open.png b/app/src/main/res/drawable-xxhdpi/ic_book_open.png new file mode 100644 index 00000000..fc7ea0d5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_book_open.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_brush.png b/app/src/main/res/drawable-xxhdpi/ic_brush.png new file mode 100644 index 00000000..2a5a71a2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_brush.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand.png b/app/src/main/res/drawable-xxhdpi/ic_expand.png new file mode 100644 index 00000000..913492e6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_expand.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_gender_female.png b/app/src/main/res/drawable-xxhdpi/ic_gender_female.png new file mode 100644 index 00000000..f2cb8663 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_gender_female.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_gender_male.png b/app/src/main/res/drawable-xxhdpi/ic_gender_male.png new file mode 100644 index 00000000..3ba80a52 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_gender_male.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search.png b/app/src/main/res/drawable-xxhdpi/ic_search.png new file mode 100644 index 00000000..187f763e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_search.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings.png b/app/src/main/res/drawable-xxhdpi/ic_settings.png new file mode 100644 index 00000000..2a50df97 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_tag.png b/app/src/main/res/drawable-xxhdpi/ic_tag.png new file mode 100644 index 00000000..10494383 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tag.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_translate.png b/app/src/main/res/drawable-xxhdpi/ic_translate.png new file mode 100644 index 00000000..49d105a1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_translate.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_account_group.png b/app/src/main/res/drawable-xxxhdpi/ic_account_group.png new file mode 100644 index 00000000..7ad2921a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_account_group.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_account_star.png b/app/src/main/res/drawable-xxxhdpi/ic_account_star.png new file mode 100644 index 00000000..2fa56e16 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_account_star.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_book_open.png b/app/src/main/res/drawable-xxxhdpi/ic_book_open.png new file mode 100644 index 00000000..db934527 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_book_open.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_brush.png b/app/src/main/res/drawable-xxxhdpi/ic_brush.png new file mode 100644 index 00000000..fa47de51 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_brush.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_gender_female.png b/app/src/main/res/drawable-xxxhdpi/ic_gender_female.png new file mode 100644 index 00000000..6b0c3dcd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_gender_female.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_gender_male.png b/app/src/main/res/drawable-xxxhdpi/ic_gender_male.png new file mode 100644 index 00000000..d7251b2b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_gender_male.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_tag.png b/app/src/main/res/drawable-xxxhdpi/ic_tag.png new file mode 100644 index 00000000..d6804b45 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_tag.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_translate.png b/app/src/main/res/drawable-xxxhdpi/ic_translate.png new file mode 100644 index 00000000..078e846a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_translate.png differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..a0ad202f --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..5b8f761d --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/img_thumbnail.jpg b/app/src/main/res/drawable/img_thumbnail.jpg new file mode 100644 index 00000000..c6959911 Binary files /dev/null and b/app/src/main/res/drawable/img_thumbnail.jpg differ diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml new file mode 100644 index 00000000..34130f3e --- /dev/null +++ b/app/src/main/res/layout/activity_gallery.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..77677f3d --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_gallery.xml b/app/src/main/res/layout/item_gallery.xml new file mode 100644 index 00000000..2e6f7755 --- /dev/null +++ b/app/src/main/res/layout/item_gallery.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_galleryblock.xml b/app/src/main/res/layout/item_galleryblock.xml new file mode 100644 index 00000000..ce1571dd --- /dev/null +++ b/app/src/main/res/layout/item_galleryblock.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_progressbar.xml b/app/src/main/res/layout/item_progressbar.xml new file mode 100644 index 00000000..0488c59c --- /dev/null +++ b/app/src/main/res/layout/item_progressbar.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml new file mode 100644 index 00000000..cd820e81 --- /dev/null +++ b/app/src/main/res/layout/settings_activity.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml new file mode 100644 index 00000000..a204ac61 --- /dev/null +++ b/app/src/main/res/menu/main.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..c49df0d1 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..ce94cf37 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..36a51807 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..8c5a0b9d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..23570cef Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..a0a07dfc Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..a930e8f7 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..06c2c989 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..6cd7fb20 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..df94340b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml new file mode 100644 index 00000000..9c3f02d2 --- /dev/null +++ b/app/src/main/res/values-v23/styles.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 00000000..99daa28e --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,60 @@ + + + + + 10 + 25 + 50 + + + + Reply + Reply to all + + + + reply + reply_all + + + + indonesian|Bahasa Indonesia + catalan|català + cebuano|Cebuano + czech|Čeština + danish|Dansk + german|Deutsch + estonian|eesti + english|English + spanish|Español + esperanto|Esperanto + french|Français + italian|Italiano + latin|Latina + hungarian|magyar + dutch|Nederlands + norwegian|norsk + polish|polski + portuguese|Português + romanian|română + albanian|shqip + slovak|Slovenčina + finnish|Suomi + swedish|Svenska + tagalog|Tagalog + vietnamese|tiếng việt + turkish|Türkçe + greek|Ελληνικά + mongolian|Монгол + russian|Русский + ukrainian|Українська + hebrew|עברית + arabic|العربية + persian|فارسی + thai|ไทย + korean|한국어 + chinese|中文 + japanese|日本語 + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..fcd0a08d --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #4fc3f7 + #0093c4 + #D81B60 + #FFFFFF + diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml new file mode 100644 index 00000000..ccfb842f --- /dev/null +++ b/app/src/main/res/values/dimen.xml @@ -0,0 +1,5 @@ + + + 64dp + 80dp + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..c5d5899f --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..f8f2a91e --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,32 @@ + + Pupil + + Settings + Search + No result + + Search galleries + + Thumbnail + + Series: %1$s + Type: %1$s + Language: %1$s + + Content ImageView //No need to translate + Fullscreen + + + Settings + + Search Settings + Galleries per page + + Default query + + Delete Cache + Currently using %1$d%2$s of cache + Warning + Deleting cache can affect image loading speed. Do you want to continue? + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..448d30a8 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml new file mode 100644 index 00000000..86a4e5d2 --- /dev/null +++ b/app/src/main/res/xml/root_preferences.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt new file mode 100644 index 00000000..d5e91efa --- /dev/null +++ b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt @@ -0,0 +1,11 @@ +package xyz.quaver.pupil + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ + +class ExampleUnitTest { + +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..c136f825 --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.3.31' + repositories { + google() + jcenter() + + } + dependencies { + classpath 'com.android.tools.build:gradle:3.4.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..65efb57f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official + +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f6b961fd Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..2518566d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Apr 25 10:57:40 KST 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..cccdd3d5 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/libpupil/.gitignore b/libpupil/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/libpupil/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libpupil/build.gradle b/libpupil/build.gradle new file mode 100644 index 00000000..17a6560d --- /dev/null +++ b/libpupil/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'java-library' +apply plugin: 'kotlin' +apply plugin: 'kotlinx-serialization' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' + implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0" + implementation 'org.jsoup:jsoup:1.11.3' + testImplementation 'junit:junit:4.12' +} + +sourceCompatibility = "7" +targetCompatibility = "7" +buildscript { + ext.kotlin_version = '1.3.31' + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" + } +} +repositories { + mavenCentral() +} +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental' + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/common.kt b/libpupil/src/main/java/xyz/quaver/hitomi/common.kt new file mode 100644 index 00000000..23b705da --- /dev/null +++ b/libpupil/src/main/java/xyz/quaver/hitomi/common.kt @@ -0,0 +1,62 @@ +package xyz.quaver.hitomi + +const val protocol = "https:" + +fun String.toTag() : String { + if (this.indexOf(':') > -1) { + val split = this.split(':') + + val field = split[0] + val term = split[1] + + when(field) { + "male" -> return "$term ♂" + "female" -> return "$term ♀" + } + } + + return this +} + +//common.js +var adapose = false +const val numberOfFrontends = 2 +const val domain = "ltn.hitomi.la" +const val galleryblockdir = "galleryblock" +const val nozomiextension = ".nozomi" + +fun subdomainFromGalleryID(g: Int) : String { + if (adapose) + return "0" + + val o = g % numberOfFrontends + + return (97+o).toChar().toString() +} + +fun subdomainFromURL(url: String, base: String? = null) : String { + var retval = "a" + + if (base != null) + retval = base + + val r = Regex("""/\d*(\d)/""") + val m = r.find(url) + + m ?: return retval + + var g = m.groups[1]!!.value.toIntOrNull() + + g ?: return retval + + if (g == 1) + g = 0 + + retval = subdomainFromGalleryID(g) + retval + + return retval +} + +fun urlFromURL(url: String, base: String? = null) : String { + return url.replace(Regex("//..?\\.hitomi\\.la/"), "//${subdomainFromURL(url, base)}.hitomi.la/") +} \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/download.kt b/libpupil/src/main/java/xyz/quaver/hitomi/download.kt new file mode 100644 index 00000000..df73e902 --- /dev/null +++ b/libpupil/src/main/java/xyz/quaver/hitomi/download.kt @@ -0,0 +1,3 @@ +package xyz.quaver.hitomi + +//download.js \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/galleries.kt b/libpupil/src/main/java/xyz/quaver/hitomi/galleries.kt new file mode 100644 index 00000000..ffbe2a98 --- /dev/null +++ b/libpupil/src/main/java/xyz/quaver/hitomi/galleries.kt @@ -0,0 +1,61 @@ +package xyz.quaver.hitomi + +import org.jsoup.Jsoup +import java.net.URL + +data class Gallery( + val related: List, + val langList: List>, + val cover: URL, + val title: String, + val artists: List, + val groups: List, + val type: String, + val language: String, + val series: List, + val characters: List, + val tags: List, + val thumbnails: List +) +fun getGallery(galleryID: Int) : Gallery { + val url = "https://hitomi.la/galleries/$galleryID.html" + + val doc = Jsoup.connect(url).get() + + val related = Regex("\\d+") + .findAll(doc.select("script").first().html()) + .map { + it.value.toInt() + }.toList() + + val langList = doc.select("#lang-list a").map { + Pair(it.text(), it.attr("href").replace(".html", "")) + } + + val cover = URL(protocol + doc.selectFirst(".cover img").attr("src")) + val title = doc.selectFirst(".gallery h1 a").text() + val artists = doc.select(".gallery h2 a").map { it.text() } + val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() } + val type = doc.selectFirst(".gallery-info a[href~=^/type/]").text() + + val language = { + val href = doc.select(".gallery-info a[href~=^/index-.+-1.html]").attr("href") + href.slice(7 until href.indexOf("-1")) + }.invoke() + + val series = doc.select(".gallery-info a[href~=^/series/]").map { it.text() } + val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() } + + val tags = doc.select(".gallery-info a[href~=^/tag/]").map { + val href = it.attr("href") + href.slice(5 until href.indexOf('-')) + } + + val thumbnails = Regex("'(//tn.hitomi.la/smalltn/\\d+/\\d+.+)',") + .findAll(doc.select("script").last().html()) + .map { + URL(protocol + it.groups[1]!!.value) + }.toList() + + return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails) +} \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/galleryblock.kt b/libpupil/src/main/java/xyz/quaver/hitomi/galleryblock.kt new file mode 100644 index 00000000..39dfb263 --- /dev/null +++ b/libpupil/src/main/java/xyz/quaver/hitomi/galleryblock.kt @@ -0,0 +1,79 @@ +package xyz.quaver.hitomi + +import org.jsoup.Jsoup +import java.net.URL +import java.net.URLDecoder +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.* +import javax.net.ssl.HttpsURLConnection + +//galleryblock.js +fun fetchNozomi(area: String? = null, tag: String = "index", language: String = "all", start: Int = -1, count: Int = -1) : List { + val url = + when(area) { + null -> "$protocol//$domain/$tag-$language$nozomiextension" + else -> "$protocol//$domain/$area/$tag-$language$nozomiextension" + } + + try { + with(URL(url).openConnection() as HttpsURLConnection) { + requestMethod = "GET" + + if (start != -1 && count != -1) { + val startByte = start*4 + val endByte = (start+count)*4-1 + + setRequestProperty("Range", "bytes=$startByte-$endByte") + } + + val nozomi = ArrayList() + + val arrayBuffer = ByteBuffer + .wrap(inputStream.readBytes()) + .order(ByteOrder.BIG_ENDIAN) + + while (arrayBuffer.hasRemaining()) + nozomi.add(arrayBuffer.int) + + return nozomi + } + } catch (e: Exception) { + return listOf() + } +} + +data class GalleryBlock( + val id: Int, + val thumbnails: List, + val title: String, + val artists: List, + val series: List, + val type: String, + val language: String, + val relatedTags: List +) +fun getGalleryBlock(galleryID: Int) : GalleryBlock { + val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension" + + val doc = Jsoup.connect(url).get() + + val thumbnails = doc.select("img").map { URL(protocol + it.attr("data-src")) } + + val title = doc.selectFirst("h1.lillie > a").text() + val artists = doc.select("div.artist-list a").map{ it.text() } + val series = doc.select("a[href~=^/series/]").map { it.text() } + val type = doc.selectFirst("a[href~=^/type/]").text() + + val language = { + val href = doc.select("a[href~=^/index-.+-1.html]").attr("href") + href.slice(7 until href.indexOf("-1")) + }.invoke() + + val relatedTags = doc.select(".relatedtags a").map { + val href = URLDecoder.decode(it.attr("href"), "UTF-8") + href.slice(5 until href.indexOf('-')) + } + + return GalleryBlock(galleryID, thumbnails, title, artists, series, type, language, relatedTags) +} \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/readers.kt b/libpupil/src/main/java/xyz/quaver/hitomi/readers.kt new file mode 100644 index 00000000..23d43b58 --- /dev/null +++ b/libpupil/src/main/java/xyz/quaver/hitomi/readers.kt @@ -0,0 +1,50 @@ +package xyz.quaver.hitomi + +import kotlinx.serialization.ImplicitReflectionSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.parseList +import org.jsoup.Jsoup +import java.net.URL + +fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html" + +@Serializable +data class GalleryInfo( + val width: Int, + val haswebp: Int, + val name: String, + val height: Int +) +data class Reader( + val title: String, + val images: List> +) +//Set header `Referer` to reader url to avoid 403 error +@UseExperimental(ImplicitReflectionSerializer::class) +fun getReader(galleryID: Int) : Reader { + val readerUrl = "https://hitomi.la/reader/$galleryID.html" + val galleryInfoUrl = "https://ltn.hitomi.la/galleries/$galleryID.js" + + val doc = Jsoup.connect(readerUrl).get() + + val title = doc.selectFirst("title").text() + + val images = doc.select(".img-url").map { + URL(protocol + urlFromURL(it.text())) + } + + val galleryInfo = ArrayList() + + galleryInfo.addAll(Json.parseList( + Regex("""\[.+\]""").find( + URL(galleryInfoUrl).readText() + )?.value ?: "[]" + ) + ) + + if (images.size > galleryInfo.size) + galleryInfo.addAll(arrayOfNulls(images.size - galleryInfo.size)) + + return Reader(title, images zip galleryInfo) +} \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/results.kt b/libpupil/src/main/java/xyz/quaver/hitomi/results.kt new file mode 100644 index 00000000..44895bbe --- /dev/null +++ b/libpupil/src/main/java/xyz/quaver/hitomi/results.kt @@ -0,0 +1,73 @@ +package xyz.quaver.hitomi + +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.* +import java.util.concurrent.Executors + +val searchDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() +fun doSearch(query: String) : List { + val terms = query + .trim() + .replace(Regex("""^\?"""), "") + .replace('_', ' ') + .toLowerCase() + .split(Regex("\\s+")) + + val results = ArrayList() + val positiveTerms = LinkedList() + val negativeTerms = LinkedList() + + for (term in terms) { + if (term.matches(Regex("^-.+"))) + negativeTerms.push(term.replace(Regex("^-"), "")) + else + positiveTerms.push(term) + } + + //first results + results.addAll( + if (positiveTerms.isEmpty()) + getGalleryIDsFromNozomi(null, "index", "all") + else + getGalleryIDsForQuery(positiveTerms.poll()) + ) + + runBlocking { + @Synchronized fun filterPositive(newResults: List) { + results.filter { newResults.binarySearch(it) >= 0 }.let { + results.clear() + results.addAll(it) + } + } + + @Synchronized fun filterNegative(newResults: List) { + results.filterNot { newResults.binarySearch(it) >= 0 }.let { + results.clear() + results.addAll(it) + } + } + + //positive results + positiveTerms.map { + launch(searchDispatcher) { + filterPositive(getGalleryIDsForQuery(it).reversed()) + } + }.forEach { + it.join() + } + + //negative results + negativeTerms.map { + launch(searchDispatcher) { + filterNegative(getGalleryIDsForQuery(it).reversed()) + } + }.forEach { + it.join() + } + } + + return results + +} \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/search.kt b/libpupil/src/main/java/xyz/quaver/hitomi/search.kt new file mode 100644 index 00000000..e771ec3d --- /dev/null +++ b/libpupil/src/main/java/xyz/quaver/hitomi/search.kt @@ -0,0 +1,317 @@ +package xyz.quaver.hitomi + +import java.net.URL +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.MessageDigest +import javax.net.ssl.HttpsURLConnection + +//searchlib.js +const val separator = "-" +const val extension = ".html" +const val index_dir = "tagindex" +const val galleries_index_dir = "galleriesindex" +const val max_node_size = 464 +const val B = 16 +const val compressed_nozomi_prefix = "n" + +val tag_index_version = getIndexVersion("tagindex") +val galleries_index_version = getIndexVersion("galleriesindex") + +fun sha256(data: ByteArray) : ByteArray { + return MessageDigest.getInstance("SHA-256").digest(data) +} + +@UseExperimental(ExperimentalUnsignedTypes::class) +fun hashTerm(term: String) : UByteArray { + return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4) +} + +fun sanitize(input: String) : String { + return input.replace(Regex("[/#]"), "") +} + +fun getIndexVersion(name: String) : String { + return URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}") + .readText() +} + +//search.js +fun getGalleryIDsForQuery(query: String) : List { + query.replace("_", " ").let { + if (it.indexOf(':') > -1) { + val sides = it.split(":") + val ns = sides[0] + var tag = sides[1] + + var area : String? = ns + var language = "all" + when (ns) { + "female", "male" -> { + area = "tag" + tag = it + } + "language" -> { + area = null + language = tag + tag = "index" + } + } + + return getGalleryIDsFromNozomi(area, tag, language) + } + + val key = hashTerm(it) + val field = "galleries" + + val node = getNodeAtAddress(field, 0) ?: return listOf() + + val data = bSearch(field, key, node) + + if (data != null) + return getGalleryIDsFromData(data) + + return arrayListOf() + } +} + +fun getSuggestionsForQuery(query: String) : List { + query.replace('_', ' ').let { + var field = "global" + var term = it + + if (term.indexOf(':') > -1) { + val sides = it.split(':') + field = sides[0] + term = sides[1] + } + + val key = hashTerm(term) + val node = getNodeAtAddress(field, 0) ?: return listOf() + val data = bSearch(field, key, node) + + if (data != null) + return getSuggestionsFromData(field, data) + + return listOf() + } +} + +data class Suggestion(val s: String, val t: Int, val u: String, val n: String) +fun getSuggestionsFromData(field: String, data: Pair) : List { + val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data" + val (offset, length) = data + if (length > 10000 || length <= 0) + throw Exception("length $length is too long") + + val inbuf = getURLAtRange(url, offset.until(offset+length)) ?: return listOf() + + val suggestions = ArrayList() + + val buffer = ByteBuffer + .wrap(inbuf) + .order(ByteOrder.BIG_ENDIAN) + val numberOfSuggestions = buffer.int + + if (numberOfSuggestions > 100 || numberOfSuggestions <= 0) + throw Exception("number of suggestions $numberOfSuggestions is too long") + + for (i in 0.until(numberOfSuggestions)) { + var top = buffer.int + + val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8")) + buffer.position(buffer.position()+top) + + top = buffer.int + + val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8")) + buffer.position(buffer.position()+top) + + val count = buffer.int + + val tagname = sanitize(tag) + val u = + when(ns) { + "female", "male" -> "/tag/$ns:$tagname${separator}1$extension" + "language" -> "/index-$tagname${separator}1$extension" + else -> "/$ns/$tagname${separator}all${separator}1$extension" + } + + suggestions.add(Suggestion(tag, count, u, ns)) + } + + return suggestions +} + +fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List { + val nozomiAddress = + when(area) { + null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension" + else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension" + } + + try { + with (URL(nozomiAddress).openConnection() as HttpsURLConnection) { + requestMethod = "GET" + + val nozomi = ArrayList() + + val arrayBuffer = ByteBuffer + .wrap(inputStream.readBytes()) + .order(ByteOrder.BIG_ENDIAN) + + while (arrayBuffer.hasRemaining()) + nozomi.add(arrayBuffer.int) + + return nozomi + } + } catch (e: Exception) { + return listOf() + } +} + +fun getGalleryIDsFromData(data: Pair) : List { + val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data" + val (offset, length) = data + if (length > 100000000 || length <= 0) + throw Exception("length $length is too long") + + val inbuf = getURLAtRange(url, offset.until(offset+length)) ?: return listOf() + + val galleryIDs = ArrayList() + + val buffer = ByteBuffer + .wrap(inbuf) + .order(ByteOrder.BIG_ENDIAN) + + val numberOfGalleryIDs = buffer.int + + val expectedLength = numberOfGalleryIDs*4+4 + + if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0) + throw Exception("number_of_galleryids $numberOfGalleryIDs is too long") + else if (inbuf.size != expectedLength) + throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength") + + for (i in 0.until(numberOfGalleryIDs)) + galleryIDs.add(buffer.int) + + return galleryIDs +} + +fun getNodeAtAddress(field: String, address: Long) : Node? { + val url = + when(field) { + "galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index" + else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index" + } + + val nodedata = getURLAtRange(url, address.until(address+max_node_size)) ?: return null + + return decodeNode(nodedata) +} + +fun getURLAtRange(url: String, range: LongRange) : ByteArray? { + try { + with (URL(url).openConnection() as HttpsURLConnection) { + requestMethod = "GET" + + setRequestProperty("Range", "bytes=${range.first}-${range.last}") + + return inputStream.readBytes() + } + } catch (e: Exception) { + return null + } +} + +@UseExperimental(ExperimentalUnsignedTypes::class) +data class Node(val keys: List, val datas: List>, val subNodeAddresses: List) +@UseExperimental(ExperimentalUnsignedTypes::class) +fun decodeNode(data: ByteArray) : Node { + val buffer = ByteBuffer + .wrap(data) + .order(ByteOrder.BIG_ENDIAN) + + val uData = data.toUByteArray() + + val numberOfKeys = buffer.int + val keys = ArrayList() + + for (i in 0.until(numberOfKeys)) { + val keySize = buffer.int + + if (keySize == 0 || keySize > 32) + throw Exception("fatal: !keySize || keySize > 32") + + keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize))) + buffer.position(buffer.position()+keySize) + } + + val numberOfDatas = buffer.int + val datas = ArrayList>() + + for (i in 0.until(numberOfDatas)) { + val offset = buffer.long + val length = buffer.int + + datas.add(Pair(offset, length)) + } + + val numberOfSubNodeAddresses = B+1 + val subNodeAddresses = ArrayList() + + for (i in 0.until(numberOfSubNodeAddresses)) { + val subNodeAddress = buffer.long + subNodeAddresses.add(subNodeAddress) + } + + return Node(keys, datas, subNodeAddresses) +} + +@UseExperimental(ExperimentalUnsignedTypes::class) +fun bSearch(field: String, key: UByteArray, node: Node) : Pair? { + fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int { + val top = Math.min(dv1.size, dv2.size) + + for (i in 0.until(top)) { + if (dv1[i] < dv2[i]) + return -1 + else if (dv1[i] > dv2[i]) + return 1 + } + + return 0 + } + + fun locateKey(key: UByteArray, node: Node) : Pair { + for (i in 0 until node.keys.size) { + val cmpResult = compareArrayBuffers(key, node.keys[i]) + + if (cmpResult <= 0) + return Pair(cmpResult==0, i) + } + + return Pair(false, node.keys.size) + } + + fun isLeaf(node: Node) : Boolean { + for (subnode in node.subNodeAddresses) + if (subnode != 0L) + return false + + return true + } + + if (node.keys.isEmpty()) + return null + + val (there, where) = locateKey(key, node) + if (there) + return node.datas[where] + else if (isLeaf(node)) + return null + + val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null + return bSearch(field, key, nextNode) +} \ No newline at end of file diff --git a/libpupil/src/test/java/xyz/quaver/hitomi/UnitTest.kt b/libpupil/src/test/java/xyz/quaver/hitomi/UnitTest.kt new file mode 100644 index 00000000..17ac0e39 --- /dev/null +++ b/libpupil/src/test/java/xyz/quaver/hitomi/UnitTest.kt @@ -0,0 +1,64 @@ +package xyz.quaver.hitomi + +import org.junit.Test +import java.net.URL + +class UnitTest { + @Test + fun test() { + val url = URL("https://ltn.hitomi.la/galleries/1411672.js") + + print(url.path.substring(url.path.lastIndexOf('/')+1)) + } + + @Test + fun test_nozomi() { + val nozomi = fetchNozomi(start = 0, count = 5) + + for (n in nozomi) + println(n) + } + + @Test + fun test_search() { + val ids = getGalleryIDsForQuery("female:loli").reversed() + + for (i in 0..100) + println(ids[i]) + } + + @Test + fun test_suggestions() { + val suggestions = getSuggestionsForQuery("language:g") + + print(suggestions) + } + + @Test + fun test_doSearch() { + val r = doSearch("type:artistcg language:korean female:loli female:mind_break -female:anal") + + print(r.size) + } + + @Test + fun test_getBlock() { + val galleryBlock = getGalleryBlock(1405716) + + print(galleryBlock) + } + + @Test + fun test_getGallery() { + val gallery = getGallery(1405267) + + print(gallery) + } + + @Test + fun test_getReader() { + val reader = getReader(1404693) + + print(reader) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..436dba02 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':libpupil'