Download added

This commit is contained in:
tom5079
2019-05-18 10:37:58 +09:00
parent ba37740c6e
commit e787428e6f
19 changed files with 566 additions and 159 deletions

View File

@@ -29,6 +29,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0"

View File

@@ -25,7 +25,8 @@
</provider>
<activity android:name=".ReaderActivity"
android:configChanges="keyboardHidden|orientation|screenSize"/>
android:parentActivityName=".MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize"/>
<activity
android:name=".SettingsActivity"
android:label="@string/settings_title" />

View File

@@ -218,7 +218,9 @@ class MainActivity : AppCompatActivity() {
private fun setupRecyclerView() {
with(main_recyclerview) {
adapter = GalleryBlockAdapter(galleries)
adapter = GalleryBlockAdapter(galleries).apply {
}
addOnScrollListener(
object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@@ -235,8 +237,7 @@ class MainActivity : AppCompatActivity() {
ItemClickSupport.addTo(this).setOnItemClickListener { _, position, _ ->
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
val gallery = galleries[position].first
intent.putExtra("GALLERY_ID", gallery.id)
intent.putExtra("GALLERY_TITLE", gallery.title)
intent.putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), gallery))
//TODO: Maybe sprinke some transitions will be nice :D
startActivity(intent)

View File

@@ -1,21 +1,40 @@
package xyz.quaver.pupil
import android.content.Intent
import android.os.Process
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.preference.PreferenceManager
import android.util.SparseArray
import com.finotes.android.finotescore.Fn
import com.finotes.android.finotescore.ObservableApplication
import com.finotes.android.finotescore.Severity
import kotlinx.coroutines.Job
class Pupil : ObservableApplication() {
override fun onCreate() {
val preference = PreferenceManager.getDefaultSharedPreferences(this)
super.onCreate()
Fn.init(this)
Fn.enableFrameDetection()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
Fn.reportException(t, Exception(e), Severity.FATAL)
if (!preference.getBoolean("channel_created", false)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
description = getString(R.string.channel_download_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
}
manager.createNotificationChannel(channel)
}
preference.edit().putBoolean("channel_created", true).apply()
}
}

View File

@@ -1,7 +1,7 @@
package xyz.quaver.pupil
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import android.view.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
@@ -10,37 +10,33 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.coroutines.*
import kotlinx.io.IOException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.ReaderItem
import xyz.quaver.hitomi.getReader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.ItemClickSupport
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import javax.net.ssl.HttpsURLConnection
class ReaderActivity : AppCompatActivity() {
private val images = ArrayList<String>()
private var galleryID = 0
private var gallerySize: Int = 0
private var currentPage: Int = 0
private lateinit var reader: Deferred<Reader>
private var loadJob: Job? = null
private lateinit var galleryBlock: GalleryBlock
private var gallerySize = 0
private var currentPage = 0
private var isScroll = true
private var isFullscreen = false
private lateinit var downloader: GalleryDownloader
private val snapHelper = PagerSnapHelper()
private var menu: Menu? = null
@@ -54,54 +50,20 @@ class ReaderActivity : AppCompatActivity() {
setContentView(R.layout.activity_reader)
supportActionBar?.title = intent.getStringExtra("GALLERY_TITLE")
galleryBlock = Json(JsonConfiguration.Stable).parse(
GalleryBlock.serializer(),
intent.getStringExtra("galleryblock")
)
galleryID = intent.getIntExtra("GALLERY_ID", 0)
reader = CoroutineScope(Dispatchers.IO).async {
val json = Json(JsonConfiguration.Stable)
val serializer = ReaderItem.serializer().list
val preference = PreferenceManager.getDefaultSharedPreferences(this@ReaderActivity)
val isHiyobi = preference.getBoolean("use_hiyobi", false)
supportActionBar?.title = galleryBlock.title
supportActionBar?.setDisplayHomeAsUpEnabled(false)
val cache = when {
isHiyobi -> File(cacheDir, "imageCache/$galleryID/reader-hiyobi.json")
else -> File(cacheDir, "imageCache/$galleryID/reader.json")
}
if (cache.exists()) {
val cached = json.parse(serializer, cache.readText())
if (cached.isNotEmpty())
return@async cached
}
val reader = when {
isHiyobi -> {
xyz.quaver.hiyobi.getReader(galleryID).let {
when {
it.isEmpty() -> getReader(galleryID)
else -> it
}
}
}
else -> {
getReader(galleryID)
}
}
if (reader.isEmpty())
finish()
if (!cache.parentFile.exists())
cache.parentFile.mkdirs()
cache.writeText(json.stringify(serializer, reader))
reader
}
initDownloader()
initView()
loadImages()
if (!downloader.notify)
downloader.start()
}
override fun onResume() {
@@ -149,6 +111,9 @@ class ReaderActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
if (!downloader.notify)
downloader.cancel()
}
override fun onBackPressed() {
@@ -166,6 +131,85 @@ class ReaderActivity : AppCompatActivity() {
}
}
private fun initDownloader() {
var d: GalleryDownloader? = GalleryDownloader.get(galleryBlock.id)
if (d == null) {
d = GalleryDownloader(this, galleryBlock)
}
downloader = d.apply {
onReaderLoadedHandler = {
CoroutineScope(Dispatchers.Main).launch {
with(reader_progressbar) {
max = it.size
progress = 0
visibility = View.VISIBLE
}
gallerySize = it.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.size}"
}
}
onProgressHandler = {
CoroutineScope(Dispatchers.Main).launch {
reader_progressbar.progress = it
}
}
onDownloadedHandler = {
CoroutineScope(Dispatchers.Main).launch {
if (images.isEmpty()) {
images.addAll(it)
reader_recyclerview.adapter?.notifyDataSetChanged()
} else {
images.add(it.last())
reader_recyclerview.adapter?.notifyItemInserted(images.size-1)
}
}
}
onErrorHandler = {
downloader.notify = false
}
onCompleteHandler = {
CoroutineScope(Dispatchers.Main).launch {
reader_progressbar.visibility = View.GONE
}
}
onNotifyChangedHandler = { notify ->
val fab = reader_fab_download
if (notify) {
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
if (downloader.notify)
fab.post {
icon.start()
fab.labelText = getString(R.string.reader_fab_download_cancel)
}
else
fab.post {
fab.setImageResource(R.drawable.ic_download)
fab.labelText = getString(R.string.reader_fab_download)
}
}
})
fab.setImageDrawable(icon)
icon?.start()
} else {
fab.setImageResource(R.drawable.ic_download)
}
}
}
if (downloader.notify) {
downloader.invokeOnReaderLoaded()
downloader.invokeOnNotifyChanged()
}
}
private fun initView() {
with(reader_recyclerview) {
adapter = ReaderAdapter(images)
@@ -208,6 +252,13 @@ class ReaderActivity : AppCompatActivity() {
reader_fab.close(true)
}
reader_fab_download.setOnClickListener {
downloader.notify = !downloader.notify
if (!downloader.notify)
downloader.clearNotification()
}
}
private fun fullscreen(isFullscreen: Boolean) {
@@ -237,68 +288,4 @@ class ReaderActivity : AppCompatActivity() {
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
}
private fun loadImages() {
fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
loadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader.await()
launch(Dispatchers.Main) {
with(reader_progressbar) {
max = reader.size
progress = 0
visibility = View.VISIBLE
}
gallerySize = reader.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize"
}
reader.chunked(4).forEach { chunked ->
chunked.map {
async(Dispatchers.IO) {
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
val fileName: String
with(url) {
fileName = substring(lastIndexOf('/')+1)
}
val cache = File(cacheDir, "/imageCache/$galleryID/$fileName")
if (!cache.exists())
try {
with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("Referer", getReferer(galleryID))
if (!cache.parentFile.exists())
cache.parentFile.mkdirs()
inputStream.copyTo(FileOutputStream(cache))
}
} catch (e: Exception) {
cache.delete()
}
cache.absolutePath
}
}.forEach {
val cache = it.await()
launch(Dispatchers.Main) {
images.add(cache)
reader_recyclerview.adapter?.notifyItemInserted(images.size - 1)
reader_progressbar.progress++
}
}
}
launch(Dispatchers.Main) {
reader_progressbar.visibility = View.GONE
}
}
}
}

View File

@@ -1,6 +1,7 @@
package xyz.quaver.pupil.adapters
import android.graphics.BitmapFactory
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -14,9 +15,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.ReaderItem
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import kotlin.concurrent.schedule
class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@@ -35,9 +44,12 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
}
var noMore = false
private val refreshTasks = SparseArray<TimerTask>()
class ViewHolder(val view: CardView) : RecyclerView.ViewHolder(view)
class ProgressViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
private var onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
class ViewHolder(val view: CardView, var galleryID: Int? = null) : RecyclerView.ViewHolder(view)
class ProgressViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when(viewType) {
@@ -71,16 +83,62 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
}.toMap()
val (gallery, thumbnail) = galleries[position]
holder.galleryID = gallery.id
val artists = gallery.artists
val series = gallery.series
CoroutineScope(Dispatchers.Default).launch {
val cache = thumbnail.await()
if (!File(cache).exists())
return@launch
val bitmap = BitmapFactory.decodeFile(thumbnail.await())
CoroutineScope(Dispatchers.Main).launch {
galleryblock_thumbnail.setImageBitmap(bitmap)
}
}
//Check cache
val readerCache = File(context.cacheDir, "imageCache/${gallery.id}/reader.json")
val imageCache = File(context.cacheDir, "imageCache/${gallery.id}/images")
if (readerCache.exists()) {
val reader = Json(JsonConfiguration.Stable)
.parse(ReaderItem.serializer().list, readerCache.readText())
with(galleryblock_progressbar) {
max = reader.size
progress = imageCache.list()?.size ?: 0
visibility = View.VISIBLE
}
} else {
galleryblock_progressbar.visibility = View.GONE
}
if (refreshTasks.get(gallery.id) == null) {
val refresh = Timer(false).schedule(0, 1000) {
this@with.post {
val size = imageCache.list()?.size ?: return@post
with(galleryblock_progressbar) {
progress = size
if (visibility == View.GONE) {
val reader = Json(JsonConfiguration.Stable)
.parse(ReaderItem.serializer().list, readerCache.readText())
max = reader.size
visibility = View.VISIBLE
}
}
}
}
refreshTasks.put(gallery.id, refresh)
}
galleryblock_title.text = gallery.title
with(galleryblock_artist) {
text = artists.joinToString(", ") { it.wordCapitalize() }
@@ -112,8 +170,8 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
galleryblock_tag_group.removeAllViews()
gallery.relatedTags.forEach {
val tag = Tag.parse(it)
val chip = LayoutInflater
.from(context)
val chip = LayoutInflater.from(context)
.inflate(R.layout.tag_chip, holder.view, false) as Chip
val icon = when(tag.area) {
@@ -131,15 +189,37 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
}
chip.chipIcon = icon
chip.text = Tag.parse(it).tag.wordCapitalize()
chip.text = tag.tag.wordCapitalize()
chip.setOnClickListener {
for (callback in onChipClickedHandler)
callback.invoke(tag)
}
galleryblock_tag_group.addView(chip)
}
}
}
if (holder is ProgressViewHolder) {
holder.view.visibility = when(noMore) {
true -> View.GONE
false -> View.VISIBLE
}
}
}
override fun getItemCount() = if (galleries.isEmpty()) 0 else galleries.size+(if (noMore) 0 else 1)
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
super.onViewDetachedFromWindow(holder)
if (holder is ViewHolder) {
val galleryID = holder.galleryID ?: return
val task = refreshTasks.get(galleryID) ?: return
refreshTasks.remove(galleryID)
task.cancel()
}
}
override fun getItemCount() = if (galleries.isEmpty()) 0 else galleries.size+1
override fun getItemViewType(position: Int): Int {
return when {

View File

@@ -0,0 +1,232 @@
package xyz.quaver.pupil.util
import android.app.PendingIntent
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager
import kotlinx.coroutines.*
import kotlinx.io.IOException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import xyz.quaver.hitomi.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ReaderActivity
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import javax.net.ssl.HttpsURLConnection
class GalleryDownloader(
base: Context,
private val galleryBlock: GalleryBlock
) : ContextWrapper(base) {
var notify: Boolean = false
set(value) {
if (value) {
field = true
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
if (downloadJob?.isActive != true)
field = false
} else {
field = false
}
onNotifyChangedHandler?.invoke(value)
}
private val reader: Deferred<Reader>
private var downloadJob: Job? = null
private lateinit var notificationBuilder: NotificationCompat.Builder
private lateinit var notificationManager: NotificationManagerCompat
var onReaderLoadedHandler: ((Reader) -> Unit)? = null
var onProgressHandler: ((Int) -> Unit)? = null
var onDownloadedHandler: ((List<String>) -> Unit)? = null
var onErrorHandler: (() -> Unit)? = null
var onCompleteHandler: (() -> Unit)? = null
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
companion object : SparseArray<GalleryDownloader>()
init {
put(galleryBlock.id, this)
initNotification()
reader = CoroutineScope(Dispatchers.IO).async {
val json = Json(JsonConfiguration.Stable)
val serializer = ReaderItem.serializer().list
val preference = PreferenceManager.getDefaultSharedPreferences(this@GalleryDownloader)
val useHiyobi = preference.getBoolean("use_hiyobi", false)
//Check cache
val cache = File(cacheDir, "imageCache/${galleryBlock.id}/reader.json")
if (cache.exists()) {
val cached = json.parse(serializer, cache.readText())
if (cached.isNotEmpty())
return@async cached
}
//Cache doesn't exist. Load from internet
val reader = when {
useHiyobi -> {
xyz.quaver.hiyobi.getReader(galleryBlock.id).let {
when {
it.isEmpty() -> getReader(galleryBlock.id)
else -> it
}
}
}
else -> {
getReader(galleryBlock.id)
}
}
//Could not retrieve reader
if (reader.isEmpty())
throw IOException("Can't retrieve Reader")
//Save cache
if (!cache.parentFile.exists())
cache.parentFile.mkdirs()
cache.writeText(json.stringify(serializer, reader))
reader
}
}
private fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
fun start() {
downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader.await()
val list = ArrayList<String>()
onReaderLoadedHandler?.invoke(reader)
notificationBuilder
.setProgress(reader.size, 0, false)
.setContentText("0/${reader.size}")
reader.chunked(4).forEachIndexed { chunkIndex, chunked ->
chunked.mapIndexed { i, it ->
val index = chunkIndex*4+i
onProgressHandler?.invoke(index)
notificationBuilder
.setProgress(reader.size, index, false)
.setContentText("$index/${reader.size}")
if (notify)
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
async(Dispatchers.IO) {
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
val name = "$index".padStart(4, '0')
val ext = url.split('.').last()
val cache = File(cacheDir, "/imageCache/${galleryBlock.id}/images/$name.$ext")
if (!cache.exists())
try {
with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("Referer", getReferer(galleryBlock.id))
if (!cache.parentFile.exists())
cache.parentFile.mkdirs()
inputStream.copyTo(FileOutputStream(cache))
}
} catch (e: Exception) {
cache.delete()
onErrorHandler?.invoke()
notificationBuilder
.setContentTitle(galleryBlock.title)
.setContentText(getString(R.string.reader_notification_error))
.setProgress(0, 0, false)
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
}
cache.absolutePath
}
}.forEach {
list.add(it.await())
onDownloadedHandler?.invoke(list)
}
}
onCompleteHandler?.invoke()
notificationBuilder
.setContentTitle(galleryBlock.title)
.setContentText(getString(R.string.reader_notification_complete))
.setProgress(0, 0, false)
if (notify)
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
notify = false
remove(galleryBlock.id)
}
}
fun cancel() {
downloadJob?.cancel()
remove(galleryBlock.id)
}
fun invokeOnReaderLoaded() {
CoroutineScope(Dispatchers.Default).launch {
onReaderLoadedHandler?.invoke(reader.await())
}
}
fun clearNotification() {
notificationManager.cancel(galleryBlock.id)
}
fun invokeOnNotifyChanged() {
onNotifyChangedHandler?.invoke(notify)
}
private fun initNotification() {
val intent = Intent(this, ReaderActivity::class.java).apply {
putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), galleryBlock))
}
val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
notificationBuilder = NotificationCompat.Builder(this, "download").apply {
setContentTitle(galleryBlock.title)
setContentText(getString(R.string.reader_notification_text))
setSmallIcon(R.drawable.ic_download)
setContentIntent(pendingIntent)
setProgress(0, 0, true)
priority = NotificationCompat.PRIORITY_LOW
}
notificationManager = NotificationManagerCompat.from(this)
}
}

View File

@@ -1,6 +1,5 @@
package xyz.quaver.pupil.util
import kotlinx.io.IOException
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
@@ -8,7 +7,6 @@ import kotlinx.serialization.parseList
import kotlinx.serialization.stringify
import java.io.File
class Histories(private val file: File) : ArrayList<Int>() {
init {

View File

@@ -1,2 +0,0 @@
package xyz.quaver.pupil.util

View File

@@ -0,0 +1,55 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:aapt="http://schemas.android.com/aapt"
tools:ignore="NewApi">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 19 9 L 15 9 L 15 3 L 9 3 L 9 9 L 5 9 L 12 16 L 19 9 Z"
android:fillColor="#fff"
android:strokeWidth="1"/>
<path
android:name="path_2"
android:pathData="M 5 19 L 19 19"
android:fillColor="#fff"
android:strokeColor="#fff"
android:strokeWidth="2"
android:strokeLineCap="butt"/>
</vector>
</aapt:attr>
<target android:name="path_2">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:propertyName="trimPathEnd"
android:duration="500"
android:valueFrom="0"
android:valueTo="0.8"
android:valueType="floatType"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
<objectAnimator
android:propertyName="trimPathStart"
android:startOffset="500"
android:duration="500"
android:valueFrom="0"
android:valueTo="0.8"
android:valueType="floatType"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
<objectAnimator
android:propertyName="trimPathOffset"
android:duration="1000"
android:valueFrom="0"
android:valueTo="0.2"
android:valueType="floatType"
android:interpolator="@android:anim/linear_interpolator"/>
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#fff"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ff000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

View File

@@ -41,6 +41,15 @@
android:layout_gravity="bottom|end"
app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/reader_fab_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_downloading"
android:tint="@android:color/white"
app:fab_label="@string/reader_fab_download"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/reader_fab_fullscreen"
android:layout_width="wrap_content"
@@ -49,11 +58,6 @@
app:fab_label="@string/reader_fab_fullscreen"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -18,6 +18,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
@@ -25,7 +33,7 @@
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView

View File

@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
<com.google.android.material.chip.Chip
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="24dp"
app:chipIconSize="16dp"
app:chipStartPadding="8dp"
app:chipCornerRadius="100dp">
</com.google.android.material.chip.Chip>
app:chipCornerRadius="100dp"/>

View File

@@ -44,4 +44,11 @@
<string name="help_dialog_title">準備中</string>
<string name="help_dialog_message">準備中です。</string>
<string name="reader_fab_fullscreen">フルスクリーン</string>
<string name="channel_download">ダウンロード</string>
<string name="channel_download_description">ダウンロードの進行を通知</string>
<string name="reader_fab_download">バックグラウンドダウンロード</string>
<string name="reader_notification_text">ダウンロード中…</string>
<string name="reader_notification_complete">ダウンロード完了</string>
<string name="reader_notification_error">ダウンロードエラー</string>
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</string>
</resources>

View File

@@ -44,4 +44,11 @@
<string name="help_dialog_title">준비 중</string>
<string name="help_dialog_message">준비중입니다.</string>
<string name="reader_fab_fullscreen">전체 화면</string>
<string name="channel_download">다운로드</string>
<string name="channel_download_description">다운로드 상태 알림</string>
<string name="reader_fab_download">백그라운드 다운로드</string>
<string name="reader_notification_text">다운로드 중…</string>
<string name="reader_notification_complete">다운로드 완료</string>
<string name="reader_notification_error">다운로드 오류</string>
<string name="reader_fab_download_cancel">백그라운드 다운로드 취소</string>
</resources>

View File

@@ -1,5 +1,5 @@
<resources>
<string name="app_name" translatable="false">Pupil</string>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" translatable="false" tools:override="true">Pupil</string>
<string name="release_url" translatable="false">https://api.github.com/repos/tom5079/Pupil-issue/releases</string>
<string name="release_name" translatable="false">Pupil-v(\\d+\\.)+\\d+\\.apk</string>
@@ -22,6 +22,9 @@
<string name="permission_explain">Denying any permission can deactivate some functions</string>
<string name="channel_download">Download</string>
<string name="channel_download_description">Shows download status</string>
<string name="main_search">Search</string>
<string name="main_no_result">No result</string>
@@ -49,6 +52,11 @@
<string name="reader_go_to_page">Go to page</string>
<string name="reader_fab_fullscreen">Fullscreen</string>
<string name="reader_fab_download">Background download</string>
<string name="reader_fab_download_cancel">Cancel background download</string>
<string name="reader_notification_text">Downloading&#8230;</string>
<string name="reader_notification_complete">Download complete</string>
<string name="reader_notification_error">Download error</string>
<string name="settings_title">Settings</string>
<string name="settings_search_title">Search Settings</string>

View File

@@ -12,7 +12,6 @@ import org.junit.Test
class ExampleUnitTest {
@Test
@ImplicitReflectionSerializer
fun test() {
}

View File

@@ -7,9 +7,7 @@ import java.net.URL
class UnitTest {
@Test
fun test() {
val f = File("C:/Users/tom50/Workspace/Pupil/nodir/nodir/asdf.txt")
f.delete()
print(File("C:\\asdf").list()?.size ?: 0)
}
@Test