Add Start/Finish date support for MAL (#2672)

* Started working on MAL support

* Added date picker UI

* Replaced Date with Calendar

* Added MAL remote update functionality

* Join url methods listEntryUrl and editUrl

* Removed unused methods

* Renamed mangaEditPayload to mangaEditPostBody

* Moved code to separate method

* Uniformed code to project conventions

* Removed wildcard import

* Moved MyAnimeListTrack to private class

* Improved MyAnimeListTrack name

* Removed redundant code

* Add start/finish date in local database

* Fixed format and improved codestyle

* Fixed typo and fixed TrackHolder's format

* Improved code style

* Ran linter

* Add database updating methods

* Change date format to fit new layout

* Review Commits

* Improve SetTrackReadingDatesDialog readability

* Move private methods after public ones

* Fixed SQL error

* Fixed remove date button

* Updated MaterialDesign methods to latest version

* Replaced dismissDialog() with dialog.Dismiss()

* Fixed wrong string resource usage.
This commit is contained in:
Hawk of the Death 2020-04-23 03:23:23 +02:00 committed by GitHub
parent c967308859
commit f7c139030f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 550 additions and 18 deletions

View File

@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = 8 const val DATABASE_VERSION = 9
} }
override fun onCreate(db: SupportSQLiteDatabase) = with(db) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -69,6 +69,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(MangaTable.createLibraryIndexQuery) db.execSQL(MangaTable.createLibraryIndexQuery)
db.execSQL(ChapterTable.createUnreadChaptersIndexQuery) db.execSQL(ChapterTable.createUnreadChaptersIndexQuery)
} }
if (oldVersion < 9) {
db.execSQL(TrackTable.addStartDate)
db.execSQL(TrackTable.addFinishDate)
}
} }
override fun onConfigure(db: SupportSQLiteDatabase) { override fun onConfigure(db: SupportSQLiteDatabase) {

View File

@ -11,12 +11,14 @@ import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_FINISH_DATE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_START_DATE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
@ -54,6 +56,8 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
put(COL_STATUS, obj.status) put(COL_STATUS, obj.status)
put(COL_TRACKING_URL, obj.tracking_url) put(COL_TRACKING_URL, obj.tracking_url)
put(COL_SCORE, obj.score) put(COL_SCORE, obj.score)
put(COL_START_DATE, obj.started_reading_date)
put(COL_FINISH_DATE, obj.finished_reading_date)
} }
} }
@ -71,6 +75,8 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS)) status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE)) score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL)) tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
} }
} }

View File

@ -24,12 +24,18 @@ interface Track : Serializable {
var status: Int var status: Int
var started_reading_date: Long
var finished_reading_date: Long
var tracking_url: String var tracking_url: String
fun copyPersonalFrom(other: Track) { fun copyPersonalFrom(other: Track) {
last_chapter_read = other.last_chapter_read last_chapter_read = other.last_chapter_read
score = other.score score = other.score
status = other.status status = other.status
started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date
} }
companion object { companion object {

View File

@ -22,6 +22,10 @@ class TrackImpl : Track {
override var status: Int = 0 override var status: Int = 0
override var started_reading_date: Long = 0
override var finished_reading_date: Long = 0
override var tracking_url: String = "" override var tracking_url: String = ""
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@ -26,6 +26,10 @@ object TrackTable {
const val COL_TRACKING_URL = "remote_url" const val COL_TRACKING_URL = "remote_url"
const val COL_START_DATE = "start_date"
const val COL_FINISH_DATE = "finish_date"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
@ -39,6 +43,8 @@ object TrackTable {
$COL_STATUS INTEGER NOT NULL, $COL_STATUS INTEGER NOT NULL,
$COL_SCORE FLOAT NOT NULL, $COL_SCORE FLOAT NOT NULL,
$COL_TRACKING_URL TEXT NOT NULL, $COL_TRACKING_URL TEXT NOT NULL,
$COL_START_DATE LONG NOT NULL,
$COL_FINISH_DATE LONG NOT NULL,
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE, UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE ON DELETE CASCADE
@ -49,4 +55,10 @@ object TrackTable {
val addLibraryId: String val addLibraryId: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL" get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
val addStartDate: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_START_DATE LONG NOT NULL DEFAULT 0"
val addFinishDate: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
} }

View File

@ -22,6 +22,9 @@ abstract class TrackService(val id: Int) {
// Name of the manga sync service to display // Name of the manga sync service to display
abstract val name: String abstract val name: String
// Application and remote support for reading dates
open val supportsReadingDates: Boolean = false
@DrawableRes @DrawableRes
abstract fun getLogo(): Int abstract fun getLogo(): Int

View File

@ -24,6 +24,10 @@ class TrackSearch : Track {
override var status: Int = 0 override var status: Int = 0
override var started_reading_date: Long = 0
override var finished_reading_date: Long = 0
override lateinit var tracking_url: String override lateinit var tracking_url: String
var cover_url: String = "" var cover_url: String = ""

View File

@ -34,6 +34,8 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
override val name: String override val name: String
get() = "MyAnimeList" get() = "MyAnimeList"
override val supportsReadingDates: Boolean = true
override fun getLogo() = R.drawable.ic_tracker_mal override fun getLogo() = R.drawable.ic_tracker_mal
override fun getLogoColor() = Color.rgb(46, 81, 162) override fun getLogoColor() = Color.rgb(46, 81, 162)

View File

@ -8,10 +8,15 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.lang.toCalendar
import eu.kanade.tachiyomi.util.selectInt import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText import eu.kanade.tachiyomi.util.selectText
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.Locale
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -76,14 +81,29 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
fun updateLibManga(track: Track): Observable<Track> { fun updateLibManga(track: Track): Observable<Track> {
return Observable.defer { return Observable.defer {
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track))) // Get track data
val response = authClient.newCall(GET(url = editPageUrl(track.media_id))).execute()
val editData = response.use {
val page = Jsoup.parse(it.consumeBody())
// Extract track data from MAL page
extractDataFromEditPage(page).apply {
// Apply changes to the just fetched data
copyPersonalFrom(track)
}
}
// Update remote
authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData)))
.asObservableSuccess() .asObservableSuccess()
.map { track } .map {
track
}
} }
} }
fun findLibManga(track: Track): Observable<Track?> { fun findLibManga(track: Track): Observable<Track?> {
return authClient.newCall(GET(url = listEntryUrl(track.media_id))) return authClient.newCall(GET(url = editPageUrl(track.media_id)))
.asObservable() .asObservable()
.map { response -> .map { response ->
var libTrack: Track? = null var libTrack: Track? = null
@ -97,6 +117,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt() status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull()
?: 0f ?: 0f
started_reading_date = trackForm.searchDatePicker("#add_manga_start_date")
finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date")
} }
} }
} }
@ -150,6 +172,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
score = it.selectInt("my_score").toFloat() score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("manga_chapters") total_chapters = it.selectInt("manga_chapters")
tracking_url = mangaUrl(media_id) tracking_url = mangaUrl(media_id)
started_reading_date = it.searchDateXml("my_start_date")
finished_reading_date = it.searchDateXml("my_finish_date")
} }
} }
.toList() .toList()
@ -194,6 +218,35 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
} }
private fun extractDataFromEditPage(page: Document): MyAnimeListEditData {
val tables = page.select("form#main-form table")
return MyAnimeListEditData(
entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0
manga_id = tables[0].select("#manga_id").`val`(),
status = tables[0].select("#add_manga_status > option[selected]").`val`(),
num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(),
last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty
num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(),
score = tables[0].select("#add_manga_score > option[selected]").`val`(),
start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(),
start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(),
start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(),
finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(),
finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(),
finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(),
tags = tables[1].select("#add_manga_tags").`val`(),
priority = tables[1].select("#add_manga_priority > option[selected]").`val`(),
storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(),
num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(),
num_read_times = tables[1].select("#add_manga_num_read_times").`val`(),
reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(),
comments = tables[1].select("#add_manga_comments").`val`(),
is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(),
sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`()
)
}
companion object { companion object {
const val CSRF = "csrf_token" const val CSRF = "csrf_token"
@ -228,19 +281,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendQueryParameter("go", "export") .appendQueryParameter("go", "export")
.toString() .toString()
private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon() private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath("edit.json") .appendPath(mediaId.toString())
.appendPath("edit")
.toString() .toString()
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon() private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath("add.json") .appendPath("add.json")
.toString() .toString()
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath(mediaId.toString())
.appendPath("edit")
.toString()
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody { private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
return FormBody.Builder() return FormBody.Builder()
.add("user_name", username) .add("user_name", username)
@ -269,6 +318,53 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
} }
private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody {
return FormBody.Builder()
.add("entry_id", track.entry_id)
.add("manga_id", track.manga_id)
.add("add_manga[status]", track.status)
.add("add_manga[num_read_volumes]", track.num_read_volumes)
.add("last_completed_vol", track.last_completed_vol)
.add("add_manga[num_read_chapters]", track.num_read_chapters)
.add("add_manga[score]", track.score)
.add("add_manga[start_date][month]", track.start_date_month)
.add("add_manga[start_date][day]", track.start_date_day)
.add("add_manga[start_date][year]", track.start_date_year)
.add("add_manga[finish_date][month]", track.finish_date_month)
.add("add_manga[finish_date][day]", track.finish_date_day)
.add("add_manga[finish_date][year]", track.finish_date_year)
.add("add_manga[tags]", track.tags)
.add("add_manga[priority]", track.priority)
.add("add_manga[storage_type]", track.storage_type)
.add("add_manga[num_retail_volumes]", track.num_retail_volumes)
.add("add_manga[num_read_times]", track.num_read_chapters)
.add("add_manga[reread_value]", track.reread_value)
.add("add_manga[comments]", track.comments)
.add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss)
.add("add_manga[sns_post_type]", track.sns_post_type)
.add("submitIt", track.submitIt)
.build()
}
private fun Element.searchDateXml(field: String): Long {
val text = selectText(field, "0000-00-00")!!
// MAL sets the data to 0000-00-00 when date is invalid or missing
if (text == "0000-00-00")
return 0L
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(text)?.time ?: 0L
}
private fun Element.searchDatePicker(id: String): Long {
val month = select(id + "_month > option[selected]").`val`().toIntOrNull()
val day = select(id + "_day > option[selected]").`val`().toIntOrNull()
val year = select(id + "_year > option[selected]").`val`().toIntOrNull()
if (year == null || month == null || day == null)
return 0L
return GregorianCalendar(year, month - 1, day).timeInMillis
}
private fun Element.searchTitle() = select("strong").text()!! private fun Element.searchTitle() = select("strong").text()!!
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt() private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
@ -302,4 +398,103 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
else -> 1 else -> 1
} }
} }
private class MyAnimeListEditData(
// entry_id
var entry_id: String,
// manga_id
var manga_id: String,
// add_manga[status]
var status: String,
// add_manga[num_read_volumes]
var num_read_volumes: String,
// last_completed_vol
var last_completed_vol: String,
// add_manga[num_read_chapters]
var num_read_chapters: String,
// add_manga[score]
var score: String,
// add_manga[start_date][month]
var start_date_month: String, // [1-12]
// add_manga[start_date][day]
var start_date_day: String,
// add_manga[start_date][year]
var start_date_year: String,
// add_manga[finish_date][month]
var finish_date_month: String, // [1-12]
// add_manga[finish_date][day]
var finish_date_day: String,
// add_manga[finish_date][year]
var finish_date_year: String,
// add_manga[tags]
var tags: String,
// add_manga[priority]
var priority: String,
// add_manga[storage_type]
var storage_type: String,
// add_manga[num_retail_volumes]
var num_retail_volumes: String,
// add_manga[num_read_times]
var num_read_times: String,
// add_manga[reread_value]
var reread_value: String,
// add_manga[comments]
var comments: String,
// add_manga[is_asked_to_discuss]
var is_asked_to_discuss: String,
// add_manga[sns_post_type]
var sns_post_type: String,
// submitIt
val submitIt: String = "0"
) {
fun copyPersonalFrom(track: Track) {
num_read_chapters = track.last_chapter_read.toString()
val numScore = track.score.toInt()
if (numScore in 1..9)
score = numScore.toString()
status = track.status.toString()
if (track.started_reading_date == 0L) {
start_date_month = ""
start_date_day = ""
start_date_year = ""
}
if (track.finished_reading_date == 0L) {
finish_date_month = ""
finish_date_day = ""
finish_date_year = ""
}
track.started_reading_date.toCalendar()?.let { cal ->
start_date_month = (cal[Calendar.MONTH] + 1).toString()
start_date_day = cal[Calendar.DAY_OF_MONTH].toString()
start_date_year = cal[Calendar.YEAR].toString()
}
track.finished_reading_date.toCalendar()?.let { cal ->
finish_date_month = (cal[Calendar.MONTH] + 1).toString()
finish_date_day = cal[Calendar.DAY_OF_MONTH].toString()
finish_date_year = cal[Calendar.YEAR].toString()
}
}
}
} }

View File

@ -0,0 +1,127 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.app.Dialog
import android.os.Bundle
import android.widget.NumberPicker
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.system.toast
import java.text.DateFormatSymbols
import java.util.Calendar
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SetTrackReadingDatesDialog<T> : DialogController
where T : Controller, T : SetTrackReadingDatesDialog.Listener {
private val item: TrackItem
private val dateToUpdate: ReadingDate
constructor(target: T, dateToUpdate: ReadingDate, item: TrackItem) : super(Bundle().apply {
putSerializable(SetTrackReadingDatesDialog.KEY_ITEM_TRACK, item.track)
}) {
targetController = target
this.item = item
this.dateToUpdate = dateToUpdate
}
@Suppress("unused")
constructor(bundle: Bundle) : super(bundle) {
val track = bundle.getSerializable(SetTrackReadingDatesDialog.KEY_ITEM_TRACK) as Track
val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
item = TrackItem(track, service)
dateToUpdate = ReadingDate.Start
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val item = item
val dialog = MaterialDialog(activity!!)
.title(when (dateToUpdate) {
ReadingDate.Start -> R.string.track_started_reading_date
ReadingDate.Finish -> R.string.track_finished_reading_date
})
.customView(R.layout.track_date_dialog, dialogWrapContent = false)
.positiveButton(android.R.string.ok) { dialog ->
onDialogConfirm(dialog)
}
.negativeButton(android.R.string.cancel) { dialog ->
dialog.dismiss()
}
.neutralButton(R.string.action_remove) { dialog ->
val listener = (targetController as? Listener)
listener?.setReadingDate(item, dateToUpdate, 0L)
dialog.dismiss()
}
.noAutoDismiss()
onDialogCreated(dialog)
return dialog
}
private fun onDialogCreated(dialog: MaterialDialog) {
val view = dialog.getCustomView()
val dayPicker: NumberPicker = view.findViewById(R.id.day_picker)
val monthPicker: NumberPicker = view.findViewById(R.id.month_picker)
val yearPicker: NumberPicker = view.findViewById(R.id.year_picker)
val monthNames: Array<String> = DateFormatSymbols().months
monthPicker.displayedValues = monthNames
val calendar = Calendar.getInstance()
item.track?.let {
val date = when (dateToUpdate) {
ReadingDate.Start -> it.started_reading_date
ReadingDate.Finish -> it.finished_reading_date
}
if (date != 0L)
calendar.timeInMillis = date
}
dayPicker.value = calendar[Calendar.DAY_OF_MONTH]
monthPicker.value = calendar[Calendar.MONTH]
yearPicker.maxValue = calendar[Calendar.YEAR]
yearPicker.value = calendar[Calendar.YEAR]
}
private fun onDialogConfirm(dialog: MaterialDialog) {
val view = dialog.getCustomView()
val dayPicker: NumberPicker = view.findViewById(R.id.day_picker)
val monthPicker: NumberPicker = view.findViewById(R.id.month_picker)
val yearPicker: NumberPicker = view.findViewById(R.id.year_picker)
try {
val calendar = Calendar.getInstance().apply { isLenient = false }
calendar.set(yearPicker.value, monthPicker.value, dayPicker.value)
calendar.time = calendar.time // Throws if invalid
val listener = (targetController as? Listener)
listener?.setReadingDate(item, dateToUpdate, calendar.timeInMillis)
dialog.dismiss()
} catch (e: Exception) {
activity?.toast(R.string.error_invalid_date_supplied)
}
}
interface Listener {
fun setReadingDate(item: TrackItem, type: ReadingDate, date: Long)
}
enum class ReadingDate {
Start,
Finish
}
companion object {
private const val KEY_ITEM_TRACK = "SetTrackReadingDatesDialog.item.track"
}
}

View File

@ -40,5 +40,7 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHold
fun onStatusClick(position: Int) fun onStatusClick(position: Int)
fun onChaptersClick(position: Int) fun onChaptersClick(position: Int)
fun onScoreClick(position: Int) fun onScoreClick(position: Int)
fun onStartDateClick(position: Int)
fun onFinishDateClick(position: Int)
} }
} }

View File

@ -21,7 +21,8 @@ class TrackController : NucleusController<TrackControllerBinding, TrackPresenter
TrackAdapter.OnClickListener, TrackAdapter.OnClickListener,
SetTrackStatusDialog.Listener, SetTrackStatusDialog.Listener,
SetTrackChaptersDialog.Listener, SetTrackChaptersDialog.Listener,
SetTrackScoreDialog.Listener { SetTrackScoreDialog.Listener,
SetTrackReadingDatesDialog.Listener {
private var adapter: TrackAdapter? = null private var adapter: TrackAdapter? = null
@ -123,6 +124,20 @@ class TrackController : NucleusController<TrackControllerBinding, TrackPresenter
SetTrackScoreDialog(this, item).showDialog(router) SetTrackScoreDialog(this, item).showDialog(router)
} }
override fun onStartDateClick(position: Int) {
val item = adapter?.getItem(position) ?: return
if (item.track == null) return
SetTrackReadingDatesDialog(this, SetTrackReadingDatesDialog.ReadingDate.Start, item).showDialog(router)
}
override fun onFinishDateClick(position: Int) {
val item = adapter?.getItem(position) ?: return
if (item.track == null) return
SetTrackReadingDatesDialog(this, SetTrackReadingDatesDialog.ReadingDate.Finish, item).showDialog(router)
}
override fun setStatus(item: TrackItem, selection: Int) { override fun setStatus(item: TrackItem, selection: Int) {
presenter.setStatus(item, selection) presenter.setStatus(item, selection)
binding.swipeRefresh.isRefreshing = true binding.swipeRefresh.isRefreshing = true
@ -138,6 +153,14 @@ class TrackController : NucleusController<TrackControllerBinding, TrackPresenter
binding.swipeRefresh.isRefreshing = true binding.swipeRefresh.isRefreshing = true
} }
override fun setReadingDate(item: TrackItem, type: SetTrackReadingDatesDialog.ReadingDate, date: Long) {
when (type) {
SetTrackReadingDatesDialog.ReadingDate.Start -> presenter.setStartDate(item, date)
SetTrackReadingDatesDialog.ReadingDate.Finish -> presenter.setFinishDate(item, date)
}
binding.swipeRefresh.isRefreshing = true
}
private companion object { private companion object {
const val TAG_SEARCH_CONTROLLER = "track_search_controller" const val TAG_SEARCH_CONTROLLER = "track_search_controller"
} }

View File

@ -2,19 +2,34 @@ package eu.kanade.tachiyomi.ui.manga.track
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.View import android.view.View
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.util.view.visibleIf
import java.text.DateFormat
import kotlinx.android.synthetic.main.track_item.bottom_divider
import kotlinx.android.synthetic.main.track_item.logo_container import kotlinx.android.synthetic.main.track_item.logo_container
import kotlinx.android.synthetic.main.track_item.track_chapters import kotlinx.android.synthetic.main.track_item.track_chapters
import kotlinx.android.synthetic.main.track_item.track_details import kotlinx.android.synthetic.main.track_item.track_details
import kotlinx.android.synthetic.main.track_item.track_finish_date
import kotlinx.android.synthetic.main.track_item.track_logo import kotlinx.android.synthetic.main.track_item.track_logo
import kotlinx.android.synthetic.main.track_item.track_score import kotlinx.android.synthetic.main.track_item.track_score
import kotlinx.android.synthetic.main.track_item.track_set import kotlinx.android.synthetic.main.track_item.track_set
import kotlinx.android.synthetic.main.track_item.track_start_date
import kotlinx.android.synthetic.main.track_item.track_status import kotlinx.android.synthetic.main.track_item.track_status
import kotlinx.android.synthetic.main.track_item.track_title import kotlinx.android.synthetic.main.track_item.track_title
import kotlinx.android.synthetic.main.track_item.vert_divider_3
import uy.kohesive.injekt.injectLazy
class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
private val preferences: PreferencesHelper by injectLazy()
private val dateFormat: DateFormat by lazy {
preferences.dateFormat().getOrDefault()
}
init { init {
val listener = adapter.rowClickListener val listener = adapter.rowClickListener
@ -24,6 +39,8 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
track_status.setOnClickListener { listener.onStatusClick(bindingAdapterPosition) } track_status.setOnClickListener { listener.onStatusClick(bindingAdapterPosition) }
track_chapters.setOnClickListener { listener.onChaptersClick(bindingAdapterPosition) } track_chapters.setOnClickListener { listener.onChaptersClick(bindingAdapterPosition) }
track_score.setOnClickListener { listener.onScoreClick(bindingAdapterPosition) } track_score.setOnClickListener { listener.onScoreClick(bindingAdapterPosition) }
track_start_date.setOnClickListener { listener.onStartDateClick(bindingAdapterPosition) }
track_finish_date.setOnClickListener { listener.onFinishDateClick(bindingAdapterPosition) }
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -42,6 +59,18 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
if (track.total_chapters > 0) track.total_chapters else "-" if (track.total_chapters > 0) track.total_chapters else "-"
track_status.text = item.service.getStatus(track.status) track_status.text = item.service.getStatus(track.status)
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
if (item.service.supportsReadingDates) {
track_start_date.text =
if (track.started_reading_date != 0L) dateFormat.format(track.started_reading_date) else "-"
track_finish_date.text =
if (track.finished_reading_date != 0L) dateFormat.format(track.finished_reading_date) else "-"
} else {
bottom_divider.gone()
vert_divider_3.gone()
track_start_date.gone()
track_finish_date.gone()
}
} }
} }
} }

View File

@ -135,4 +135,16 @@ class TrackPresenter(
} }
updateRemote(track, item.service) updateRemote(track, item.service)
} }
fun setStartDate(item: TrackItem, date: Long) {
val track = item.track!!
track.started_reading_date = date
updateRemote(track, item.service)
}
fun setFinishDate(item: TrackItem, date: Long) {
val track = item.track!!
track.finished_reading_date = date
updateRemote(track, item.service)
}
} }

View File

@ -29,3 +29,16 @@ fun Long.toDateKey(): Date {
cal[Calendar.MILLISECOND] = 0 cal[Calendar.MILLISECOND] = 0
return cal.time return cal.time
} }
/**
* Convert epoch long to Calendar instance
*
* @return Calendar instance at supplied epoch time. Null if epoch was 0.
*/
fun Long.toCalendar(): Calendar? {
if (this == 0L)
return null
val cal = Calendar.getInstance()
cal.timeInMillis = this
return cal
}

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<eu.kanade.tachiyomi.widget.MinMaxNumberPicker
android:id="@+id/day_picker"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:descendantFocusability="blocksDescendants"
app:max="31"
app:min="1" />
<eu.kanade.tachiyomi.widget.MinMaxNumberPicker
android:id="@+id/month_picker"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:descendantFocusability="blocksDescendants"
app:max="11"
app:min="0" />
<eu.kanade.tachiyomi.widget.MinMaxNumberPicker
android:id="@+id/year_picker"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:descendantFocusability="blocksDescendants"
app:min="1900" />
</LinearLayout>

View File

@ -92,7 +92,6 @@
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:padding="16dp" android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/vert_divider_1" app:layout_constraintEnd_toStartOf="@+id/vert_divider_1"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_divider" app:layout_constraintTop_toBottomOf="@+id/top_divider"
@ -106,7 +105,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:alpha="0.25" android:alpha="0.25"
android:background="?android:attr/textColorHint" android:background="?android:attr/textColorHint"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/bottom_divider"
app:layout_constraintEnd_toStartOf="@+id/track_chapters" app:layout_constraintEnd_toStartOf="@+id/track_chapters"
app:layout_constraintStart_toEndOf="@+id/track_status" app:layout_constraintStart_toEndOf="@+id/track_status"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -121,7 +120,6 @@
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:padding="16dp" android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/vert_divider_2" app:layout_constraintEnd_toStartOf="@+id/vert_divider_2"
app:layout_constraintStart_toEndOf="@+id/vert_divider_1" app:layout_constraintStart_toEndOf="@+id/vert_divider_1"
app:layout_constraintTop_toBottomOf="@+id/top_divider" app:layout_constraintTop_toBottomOf="@+id/top_divider"
@ -135,7 +133,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:alpha="0.25" android:alpha="0.25"
android:background="?android:attr/textColorHint" android:background="?android:attr/textColorHint"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/bottom_divider"
app:layout_constraintEnd_toStartOf="@+id/track_score" app:layout_constraintEnd_toStartOf="@+id/track_score"
app:layout_constraintStart_toEndOf="@+id/track_chapters" app:layout_constraintStart_toEndOf="@+id/track_chapters"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -150,12 +148,64 @@
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:padding="16dp" android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/vert_divider_2" app:layout_constraintStart_toEndOf="@+id/vert_divider_2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_divider" app:layout_constraintTop_toBottomOf="@+id/top_divider"
tools:text="10" /> tools:text="10" />
<View
android:id="@+id/bottom_divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:alpha="0.25"
android:background="?android:attr/textColorHint"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/track_score" />
<TextView
android:id="@+id/track_start_date"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/list_item_selector"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:padding="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/vert_divider_3"
app:layout_constraintTop_toBottomOf="@+id/bottom_divider"
tools:text="4/16/2020" />
<View
android:id="@+id/vert_divider_3"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:alpha="0.25"
android:background="?android:attr/textColorHint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/track_start_date"
app:layout_constraintEnd_toStartOf="@+id/track_finish_date"
app:layout_constraintTop_toTopOf="@+id/bottom_divider" />
<TextView
android:id="@+id/track_finish_date"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/list_item_selector"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:padding="16dp"
app:layout_constraintStart_toEndOf="@+id/vert_divider_3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_divider"
tools:text="4/16/2020" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>

View File

@ -475,8 +475,11 @@
<string name="status">Status</string> <string name="status">Status</string>
<string name="track_status">Status</string> <string name="track_status">Status</string>
<string name="track_start_date">Started</string> <string name="track_start_date">Started</string>
<string name="track_started_reading_date">Started reading date</string>
<string name="track_finished_reading_date">Finished reading date</string>
<string name="track_type">Type</string> <string name="track_type">Type</string>
<string name="track_author">Author</string> <string name="track_author">Author</string>
<string name="error_invalid_date_supplied">Invalid date supplied</string>
<string name="url_not_set">Manga URL not set, please click title and select manga again</string> <string name="url_not_set">Manga URL not set, please click title and select manga again</string>
<!-- Category activity --> <!-- Category activity -->