From f7c139030f343a0758f380c2ee7c19864c8d6be9 Mon Sep 17 00:00:00 2001 From: Hawk of the Death Date: Thu, 23 Apr 2020 03:23:23 +0200 Subject: [PATCH] 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. --- .../tachiyomi/data/database/DbOpenCallback.kt | 6 +- .../data/database/mappers/TrackTypeMapping.kt | 6 + .../tachiyomi/data/database/models/Track.kt | 6 + .../data/database/models/TrackImpl.kt | 4 + .../data/database/tables/TrackTable.kt | 12 + .../tachiyomi/data/track/TrackService.kt | 3 + .../tachiyomi/data/track/model/TrackSearch.kt | 4 + .../data/track/myanimelist/MyAnimeList.kt | 2 + .../data/track/myanimelist/MyAnimeListApi.kt | 215 +++++++++++++++++- .../manga/track/SetTrackReadingDatesDialog.kt | 127 +++++++++++ .../tachiyomi/ui/manga/track/TrackAdapter.kt | 2 + .../ui/manga/track/TrackController.kt | 25 +- .../tachiyomi/ui/manga/track/TrackHolder.kt | 29 +++ .../ui/manga/track/TrackPresenter.kt | 12 + .../tachiyomi/util/lang/DateExtensions.kt | 13 ++ app/src/main/res/layout/track_date_dialog.xml | 37 +++ app/src/main/res/layout/track_item.xml | 62 ++++- app/src/main/res/values/strings.xml | 3 + 18 files changed, 550 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackReadingDatesDialog.kt create mode 100644 app/src/main/res/layout/track_date_dialog.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index 60f534a28b..949275a9a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { /** * Version of the database. */ - const val DATABASE_VERSION = 8 + const val DATABASE_VERSION = 9 } override fun onCreate(db: SupportSQLiteDatabase) = with(db) { @@ -69,6 +69,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { db.execSQL(MangaTable.createLibraryIndexQuery) db.execSQL(ChapterTable.createUnreadChaptersIndexQuery) } + if (oldVersion < 9) { + db.execSQL(TrackTable.addStartDate) + db.execSQL(TrackTable.addFinishDate) + } } override fun onConfigure(db: SupportSQLiteDatabase) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt index 797c6a2524..ea04f88df0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt @@ -11,12 +11,14 @@ import com.pushtorefresh.storio.sqlite.queries.InsertQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery import eu.kanade.tachiyomi.data.database.models.Track 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_LAST_CHAPTER_READ 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_MEDIA_ID 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_SYNC_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE @@ -54,6 +56,8 @@ class TrackPutResolver : DefaultPutResolver() { put(COL_STATUS, obj.status) put(COL_TRACKING_URL, obj.tracking_url) 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() { status = cursor.getInt(cursor.getColumnIndex(COL_STATUS)) score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE)) 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)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt index c64363efa0..0f3815c544 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt @@ -24,12 +24,18 @@ interface Track : Serializable { var status: Int + var started_reading_date: Long + + var finished_reading_date: Long + var tracking_url: String fun copyPersonalFrom(other: Track) { last_chapter_read = other.last_chapter_read score = other.score status = other.status + started_reading_date = other.started_reading_date + finished_reading_date = other.finished_reading_date } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt index 03a878e146..6f5991133c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt @@ -22,6 +22,10 @@ class TrackImpl : Track { 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 fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt index 82c863fb90..fc30bafa6c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt @@ -26,6 +26,10 @@ object TrackTable { const val COL_TRACKING_URL = "remote_url" + const val COL_START_DATE = "start_date" + + const val COL_FINISH_DATE = "finish_date" + val createTableQuery: String get() = """CREATE TABLE $TABLE( $COL_ID INTEGER NOT NULL PRIMARY KEY, @@ -39,6 +43,8 @@ object TrackTable { $COL_STATUS INTEGER NOT NULL, $COL_SCORE FLOAT 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, FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) ON DELETE CASCADE @@ -49,4 +55,10 @@ object TrackTable { val addLibraryId: String 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" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt index 3bc560f418..3fd39018d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt @@ -22,6 +22,9 @@ abstract class TrackService(val id: Int) { // Name of the manga sync service to display abstract val name: String + // Application and remote support for reading dates + open val supportsReadingDates: Boolean = false + @DrawableRes abstract fun getLogo(): Int diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt index ee1100b763..9035f55502 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt @@ -24,6 +24,10 @@ class TrackSearch : Track { 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 var cover_url: String = "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 90363b0d25..b98caef18c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -34,6 +34,8 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { override val name: String get() = "MyAnimeList" + override val supportsReadingDates: Boolean = true + override fun getLogo() = R.drawable.ic_tracker_mal override fun getLogoColor() = Color.rgb(46, 81, 162) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index dd3d56341a..53256c5873 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -8,10 +8,15 @@ import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservable import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.lang.toCalendar import eu.kanade.tachiyomi.util.selectInt import eu.kanade.tachiyomi.util.selectText import java.io.BufferedReader 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 okhttp3.FormBody import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -76,14 +81,29 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI fun updateLibManga(track: Track): Observable { 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() - .map { track } + .map { + track + } } } fun findLibManga(track: Track): Observable { - return authClient.newCall(GET(url = listEntryUrl(track.media_id))) + return authClient.newCall(GET(url = editPageUrl(track.media_id))) .asObservable() .map { response -> 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() score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() ?: 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() total_chapters = it.selectInt("manga_chapters") tracking_url = mangaUrl(media_id) + started_reading_date = it.searchDateXml("my_start_date") + finished_reading_date = it.searchDateXml("my_finish_date") } } .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 { const val CSRF = "csrf_token" @@ -228,19 +281,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendQueryParameter("go", "export") .toString() - private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon() - .appendPath("edit.json") + private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon() + .appendPath(mediaId.toString()) + .appendPath("edit") .toString() private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon() .appendPath("add.json") .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 { return FormBody.Builder() .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()) } + 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.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 } } + + 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() + } + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackReadingDatesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackReadingDatesDialog.kt new file mode 100644 index 0000000000..988d29868a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackReadingDatesDialog.kt @@ -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 : 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().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 = 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" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt index 597e21a704..9f25a46b1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt @@ -40,5 +40,7 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter presenter.setStartDate(item, date) + SetTrackReadingDatesDialog.ReadingDate.Finish -> presenter.setFinishDate(item, date) + } + binding.swipeRefresh.isRefreshing = true + } + private companion object { const val TAG_SEARCH_CONTROLLER = "track_search_controller" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt index 4a8a59ad99..71dc0b8ceb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -2,19 +2,34 @@ package eu.kanade.tachiyomi.ui.manga.track import android.annotation.SuppressLint 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.util.view.gone 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.track_chapters 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_score 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_title +import kotlinx.android.synthetic.main.track_item.vert_divider_3 +import uy.kohesive.injekt.injectLazy class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { + private val preferences: PreferencesHelper by injectLazy() + + private val dateFormat: DateFormat by lazy { + preferences.dateFormat().getOrDefault() + } + init { val listener = adapter.rowClickListener @@ -24,6 +39,8 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { track_status.setOnClickListener { listener.onStatusClick(bindingAdapterPosition) } track_chapters.setOnClickListener { listener.onChaptersClick(bindingAdapterPosition) } track_score.setOnClickListener { listener.onScoreClick(bindingAdapterPosition) } + track_start_date.setOnClickListener { listener.onStartDateClick(bindingAdapterPosition) } + track_finish_date.setOnClickListener { listener.onFinishDateClick(bindingAdapterPosition) } } @SuppressLint("SetTextI18n") @@ -42,6 +59,18 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { if (track.total_chapters > 0) track.total_chapters else "-" track_status.text = item.service.getStatus(track.status) 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() + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt index 7ebe963610..3d5f94c780 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt @@ -135,4 +135,16 @@ class TrackPresenter( } 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) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt index c03d8bba36..1f1aa6c9ab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt @@ -29,3 +29,16 @@ fun Long.toDateKey(): Date { cal[Calendar.MILLISECOND] = 0 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 +} diff --git a/app/src/main/res/layout/track_date_dialog.xml b/app/src/main/res/layout/track_date_dialog.xml new file mode 100644 index 0000000000..6ff1195417 --- /dev/null +++ b/app/src/main/res/layout/track_date_dialog.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/track_item.xml b/app/src/main/res/layout/track_item.xml index 3fba74c9ab..47b2f94717 100644 --- a/app/src/main/res/layout/track_item.xml +++ b/app/src/main/res/layout/track_item.xml @@ -92,7 +92,6 @@ android:gravity="center" android:maxLines="1" android:padding="16dp" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/vert_divider_1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/top_divider" @@ -106,7 +105,7 @@ android:layout_marginBottom="8dp" android:alpha="0.25" 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_constraintStart_toEndOf="@+id/track_status" app:layout_constraintTop_toTopOf="parent" /> @@ -121,7 +120,6 @@ android:gravity="center" android:maxLines="1" android:padding="16dp" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/vert_divider_2" app:layout_constraintStart_toEndOf="@+id/vert_divider_1" app:layout_constraintTop_toBottomOf="@+id/top_divider" @@ -135,7 +133,7 @@ android:layout_marginBottom="8dp" android:alpha="0.25" 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_constraintStart_toEndOf="@+id/track_chapters" app:layout_constraintTop_toTopOf="parent" /> @@ -150,12 +148,64 @@ android:gravity="center" android:maxLines="1" android:padding="16dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/vert_divider_2" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/top_divider" tools:text="10" /> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 477c2ead5c..1b59b0c5d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -475,8 +475,11 @@ Status Status Started + Started reading date + Finished reading date Type Author + Invalid date supplied Manga URL not set, please click title and select manga again