mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	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:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							c967308859
						
					
				
				
					commit
					f7c139030f
				
			| @@ -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) { | ||||
|   | ||||
| @@ -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<Track>() { | ||||
|         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<Track>() { | ||||
|         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)) | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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" | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 = "" | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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<Track> { | ||||
|         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<Track?> { | ||||
|         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() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
|     } | ||||
| } | ||||
| @@ -40,5 +40,7 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHold | ||||
|         fun onStatusClick(position: Int) | ||||
|         fun onChaptersClick(position: Int) | ||||
|         fun onScoreClick(position: Int) | ||||
|         fun onStartDateClick(position: Int) | ||||
|         fun onFinishDateClick(position: Int) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,7 +21,8 @@ class TrackController : NucleusController<TrackControllerBinding, TrackPresenter | ||||
|         TrackAdapter.OnClickListener, | ||||
|         SetTrackStatusDialog.Listener, | ||||
|         SetTrackChaptersDialog.Listener, | ||||
|         SetTrackScoreDialog.Listener { | ||||
|         SetTrackScoreDialog.Listener, | ||||
|         SetTrackReadingDatesDialog.Listener { | ||||
|  | ||||
|     private var adapter: TrackAdapter? = null | ||||
|  | ||||
| @@ -123,6 +124,20 @@ class TrackController : NucleusController<TrackControllerBinding, TrackPresenter | ||||
|         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) { | ||||
|         presenter.setStatus(item, selection) | ||||
|         binding.swipeRefresh.isRefreshing = true | ||||
| @@ -138,6 +153,14 @@ class TrackController : NucleusController<TrackControllerBinding, TrackPresenter | ||||
|         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 { | ||||
|         const val TAG_SEARCH_CONTROLLER = "track_search_controller" | ||||
|     } | ||||
|   | ||||
| @@ -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() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user