ReaderActivity.kt 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. package eu.kanade.tachiyomi.ui.reader
  2. import android.app.Dialog
  3. import android.content.Context
  4. import android.content.Intent
  5. import android.content.pm.ActivityInfo
  6. import android.content.res.Configuration
  7. import android.graphics.Color
  8. import android.os.Build
  9. import android.os.Bundle
  10. import android.support.v4.content.ContextCompat
  11. import android.view.*
  12. import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
  13. import android.view.animation.Animation
  14. import android.view.animation.AnimationUtils
  15. import android.widget.SeekBar
  16. import com.afollestad.materialdialogs.MaterialDialog
  17. import eu.kanade.tachiyomi.R
  18. import eu.kanade.tachiyomi.data.database.models.Chapter
  19. import eu.kanade.tachiyomi.data.database.models.Manga
  20. import eu.kanade.tachiyomi.data.preference.PreferencesHelper
  21. import eu.kanade.tachiyomi.data.preference.getOrDefault
  22. import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
  23. import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
  24. import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
  25. import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
  26. import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader
  27. import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonReader
  28. import eu.kanade.tachiyomi.util.GLUtil
  29. import eu.kanade.tachiyomi.util.SharedData
  30. import eu.kanade.tachiyomi.util.plusAssign
  31. import eu.kanade.tachiyomi.util.toast
  32. import eu.kanade.tachiyomi.widget.SimpleAnimationListener
  33. import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
  34. import kotlinx.android.synthetic.main.activity_reader.*
  35. import kotlinx.android.synthetic.main.reader_menu.*
  36. import nucleus.factory.RequiresPresenter
  37. import rx.Subscription
  38. import rx.subscriptions.CompositeSubscription
  39. import timber.log.Timber
  40. import java.text.DecimalFormat
  41. @RequiresPresenter(ReaderPresenter::class)
  42. class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
  43. companion object {
  44. @Suppress("unused")
  45. const val LEFT_TO_RIGHT = 1
  46. const val RIGHT_TO_LEFT = 2
  47. const val VERTICAL = 3
  48. const val WEBTOON = 4
  49. const val BLACK_THEME = 1
  50. const val MENU_VISIBLE = "menu_visible"
  51. fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
  52. SharedData.put(ReaderEvent(manga, chapter))
  53. return Intent(context, ReaderActivity::class.java)
  54. }
  55. }
  56. private var viewer: BaseReader? = null
  57. private var uiFlags: Int = 0
  58. lateinit var subscriptions: CompositeSubscription
  59. private set
  60. private var customBrightnessSubscription: Subscription? = null
  61. var readerTheme: Int = 0
  62. private set
  63. var maxBitmapSize: Int = 0
  64. private set
  65. private val decimalFormat = DecimalFormat("#.###")
  66. private var popupMenu: ReaderPopupMenu? = null
  67. private var nextChapterBtn: MenuItem? = null
  68. private var prevChapterBtn: MenuItem? = null
  69. private val volumeKeysEnabled by lazy { preferences.readWithVolumeKeys().getOrDefault() }
  70. val preferences: PreferencesHelper
  71. get() = presenter.prefs
  72. override fun onCreate(savedState: Bundle?) {
  73. super.onCreate(savedState)
  74. setContentView(R.layout.activity_reader)
  75. if (savedState == null && SharedData.get(ReaderEvent::class.java) == null) {
  76. finish()
  77. return
  78. }
  79. setupToolbar(toolbar)
  80. subscriptions = CompositeSubscription()
  81. initializeMenu()
  82. initializeSettings()
  83. if (savedState != null) {
  84. setMenuVisibility(savedState.getBoolean(MENU_VISIBLE), animate = false)
  85. }
  86. maxBitmapSize = GLUtil.getMaxTextureSize()
  87. }
  88. override fun onResume() {
  89. super.onResume()
  90. setSystemUiVisibility()
  91. }
  92. override fun onDestroy() {
  93. subscriptions.unsubscribe()
  94. popupMenu?.dismiss()
  95. viewer = null
  96. super.onDestroy()
  97. }
  98. override fun onCreateOptionsMenu(menu: Menu): Boolean {
  99. menuInflater.inflate(R.menu.reader, menu)
  100. nextChapterBtn = menu.findItem(R.id.action_next_chapter)
  101. prevChapterBtn = menu.findItem(R.id.action_previous_chapter)
  102. setAdjacentChaptersVisibility()
  103. return true
  104. }
  105. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  106. when (item.itemId) {
  107. R.id.action_previous_chapter -> requestPreviousChapter()
  108. R.id.action_next_chapter -> requestNextChapter()
  109. else -> return super.onOptionsItemSelected(item)
  110. }
  111. return true
  112. }
  113. override fun onSaveInstanceState(outState: Bundle) {
  114. outState.putBoolean(MENU_VISIBLE, reader_menu.visibility == View.VISIBLE)
  115. super.onSaveInstanceState(outState)
  116. }
  117. override fun onBackPressed() {
  118. presenter.onChapterLeft()
  119. val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
  120. if (chapterToUpdate > 0) {
  121. if (presenter.prefs.askUpdateMangaSync()) {
  122. MaterialDialog.Builder(this)
  123. .content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
  124. .positiveText(android.R.string.yes)
  125. .negativeText(android.R.string.no)
  126. .onPositive { dialog, which -> presenter.updateMangaSyncLastChapterRead() }
  127. .onAny { dialog1, which1 -> super.onBackPressed() }
  128. .show()
  129. } else {
  130. presenter.updateMangaSyncLastChapterRead()
  131. super.onBackPressed()
  132. }
  133. } else {
  134. super.onBackPressed()
  135. }
  136. }
  137. override fun onWindowFocusChanged(hasFocus: Boolean) {
  138. super.onWindowFocusChanged(hasFocus)
  139. if (hasFocus) {
  140. setSystemUiVisibility()
  141. }
  142. }
  143. override fun dispatchKeyEvent(event: KeyEvent): Boolean {
  144. if (!isFinishing) {
  145. when (event.keyCode) {
  146. KeyEvent.KEYCODE_VOLUME_DOWN -> {
  147. if (volumeKeysEnabled) {
  148. if (event.action == KeyEvent.ACTION_UP) {
  149. viewer?.moveToNext()
  150. }
  151. return true
  152. }
  153. }
  154. KeyEvent.KEYCODE_VOLUME_UP -> {
  155. if (volumeKeysEnabled) {
  156. if (event.action == KeyEvent.ACTION_UP) {
  157. viewer?.moveToPrevious()
  158. }
  159. return true
  160. }
  161. }
  162. }
  163. }
  164. return super.dispatchKeyEvent(event)
  165. }
  166. override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
  167. if (!isFinishing) {
  168. when (keyCode) {
  169. KeyEvent.KEYCODE_DPAD_RIGHT -> viewer?.moveToNext()
  170. KeyEvent.KEYCODE_DPAD_LEFT -> viewer?.moveToPrevious()
  171. KeyEvent.KEYCODE_MENU -> toggleMenu()
  172. }
  173. }
  174. return super.onKeyUp(keyCode, event)
  175. }
  176. fun onChapterError(error: Throwable) {
  177. Timber.e(error, error.message)
  178. finish()
  179. toast(error.message)
  180. }
  181. fun onChapterAppendError() {
  182. // Ignore
  183. }
  184. /**
  185. * Called from the presenter at startup, allowing to prepare the selected reader.
  186. */
  187. fun onMangaOpen(manga: Manga) {
  188. if (viewer == null) {
  189. viewer = getOrCreateViewer(manga)
  190. }
  191. if (viewer is RightToLeftReader && page_seekbar.rotation != 180f) {
  192. // Invert the seekbar for the right to left reader
  193. page_seekbar.rotation = 180f
  194. }
  195. setToolbarTitle(manga.title)
  196. please_wait.visibility = View.VISIBLE
  197. please_wait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
  198. }
  199. fun onChapterReady(chapter: ReaderChapter) {
  200. please_wait.visibility = View.GONE
  201. val pages = chapter.pages ?: run { onChapterError(Exception("Null pages")); return }
  202. val activePage = pages.getOrElse(chapter.requestedPage) { pages.first() }
  203. viewer?.onPageListReady(chapter, activePage)
  204. setActiveChapter(chapter, activePage.pageNumber)
  205. }
  206. fun onEnterChapter(chapter: ReaderChapter, currentPage: Int) {
  207. val activePage = if (currentPage == -1) chapter.pages!!.lastIndex else currentPage
  208. presenter.setActiveChapter(chapter)
  209. setActiveChapter(chapter, activePage)
  210. }
  211. fun setActiveChapter(chapter: ReaderChapter, currentPage: Int) {
  212. val numPages = chapter.pages!!.size
  213. if (page_seekbar.rotation != 180f) {
  214. right_page_text.text = "$numPages"
  215. left_page_text.text = "${currentPage + 1}"
  216. } else {
  217. left_page_text.text = "$numPages"
  218. right_page_text.text = "${currentPage + 1}"
  219. }
  220. page_seekbar.max = numPages - 1
  221. page_seekbar.progress = currentPage
  222. setToolbarSubtitle(if (chapter.isRecognizedNumber)
  223. getString(R.string.chapter_subtitle, decimalFormat.format(chapter.chapter_number.toDouble()))
  224. else
  225. chapter.name)
  226. }
  227. fun onAppendChapter(chapter: ReaderChapter) {
  228. viewer?.onPageListAppendReady(chapter)
  229. }
  230. @Suppress("UNUSED_PARAMETER")
  231. fun onAdjacentChapters(previous: Chapter?, next: Chapter?) {
  232. setAdjacentChaptersVisibility()
  233. }
  234. private fun setAdjacentChaptersVisibility() {
  235. prevChapterBtn?.isVisible = presenter.hasPreviousChapter()
  236. nextChapterBtn?.isVisible = presenter.hasNextChapter()
  237. }
  238. private fun getOrCreateViewer(manga: Manga): BaseReader {
  239. val mangaViewer = if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
  240. // Try to reuse the viewer using its tag
  241. var fragment: BaseReader? = supportFragmentManager.findFragmentByTag(manga.viewer.toString()) as? BaseReader
  242. if (fragment == null) {
  243. // Create a new viewer
  244. when (mangaViewer) {
  245. RIGHT_TO_LEFT -> fragment = RightToLeftReader()
  246. VERTICAL -> fragment = VerticalReader()
  247. WEBTOON -> fragment = WebtoonReader()
  248. else -> fragment = LeftToRightReader()
  249. }
  250. supportFragmentManager.beginTransaction().replace(R.id.reader, fragment, manga.viewer.toString()).commit()
  251. }
  252. return fragment
  253. }
  254. fun onPageChanged(currentPageIndex: Int, totalPages: Int) {
  255. val page = currentPageIndex + 1
  256. page_number.text = "$page/$totalPages"
  257. if (page_seekbar.rotation != 180f) {
  258. left_page_text.text = "$page"
  259. } else {
  260. right_page_text.text = "$page"
  261. }
  262. page_seekbar.progress = currentPageIndex
  263. }
  264. fun gotoPageInCurrentChapter(pageIndex: Int) {
  265. viewer?.let {
  266. val activePage = it.getActivePage()
  267. if (activePage != null) {
  268. val requestedPage = activePage.chapter.pages!![pageIndex]
  269. it.setActivePage(requestedPage)
  270. }
  271. }
  272. }
  273. fun toggleMenu() {
  274. setMenuVisibility(reader_menu.visibility == View.GONE)
  275. }
  276. fun requestNextChapter() {
  277. if (!presenter.loadNextChapter()) {
  278. toast(R.string.no_next_chapter)
  279. }
  280. }
  281. fun requestPreviousChapter() {
  282. if (!presenter.loadPreviousChapter()) {
  283. toast(R.string.no_previous_chapter)
  284. }
  285. }
  286. private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
  287. if (visible) {
  288. reader_menu.visibility = View.VISIBLE
  289. if (animate) {
  290. val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
  291. toolbar.startAnimation(toolbarAnimation)
  292. val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
  293. reader_menu_bottom.startAnimation(bottomMenuAnimation)
  294. }
  295. } else {
  296. val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
  297. toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
  298. override fun onAnimationEnd(animation: Animation) {
  299. reader_menu.visibility = View.GONE
  300. }
  301. })
  302. toolbar.startAnimation(toolbarAnimation)
  303. val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
  304. reader_menu_bottom.startAnimation(bottomMenuAnimation)
  305. popupMenu?.dismiss()
  306. }
  307. }
  308. private fun initializeMenu() {
  309. // Intercept all events in this layout
  310. reader_menu_bottom.setOnTouchListener { v, event -> true }
  311. page_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
  312. override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
  313. if (fromUser) {
  314. gotoPageInCurrentChapter(progress)
  315. }
  316. }
  317. })
  318. lock_orientation.setOnClickListener { v ->
  319. showImmersiveDialog(MaterialDialog.Builder(this)
  320. .title(R.string.pref_rotation_type)
  321. .items(R.array.rotation_type)
  322. .itemsCallbackSingleChoice(preferences.rotation().getOrDefault() - 1,
  323. { d, itemView, which, text ->
  324. preferences.rotation().set(which + 1)
  325. true
  326. })
  327. .build())
  328. }
  329. reader_zoom_selector.setOnClickListener { v ->
  330. showImmersiveDialog(MaterialDialog.Builder(this)
  331. .title(R.string.pref_zoom_start)
  332. .items(R.array.zoom_start)
  333. .itemsCallbackSingleChoice(preferences.zoomStart().getOrDefault() - 1,
  334. { d, itemView, which, text ->
  335. preferences.zoomStart().set(which + 1)
  336. true
  337. })
  338. .build())
  339. }
  340. reader_scale_type_selector.setOnClickListener { v ->
  341. showImmersiveDialog(MaterialDialog.Builder(this)
  342. .title(R.string.pref_image_scale_type)
  343. .items(R.array.image_scale_type)
  344. .itemsCallbackSingleChoice(preferences.imageScaleType().getOrDefault() - 1,
  345. { d, itemView, which, text ->
  346. preferences.imageScaleType().set(which + 1)
  347. true
  348. })
  349. .build())
  350. }
  351. reader_selector.setOnClickListener { v ->
  352. showImmersiveDialog(MaterialDialog.Builder(this)
  353. .title(R.string.pref_viewer_type)
  354. .items(R.array.viewers_selector)
  355. .itemsCallbackSingleChoice(presenter.manga.viewer,
  356. { d, itemView, which, text ->
  357. presenter.updateMangaViewer(which)
  358. recreate()
  359. true
  360. })
  361. .build())
  362. }
  363. val popupView = layoutInflater.inflate(R.layout.reader_popup, null)
  364. popupMenu = ReaderPopupMenu(this, popupView)
  365. reader_extra_settings.setOnClickListener {
  366. popupMenu?.let {
  367. if (!it.isShowing)
  368. it.showAtLocation(reader_extra_settings,
  369. Gravity.BOTTOM or Gravity.RIGHT, 0, reader_menu_bottom.height)
  370. else
  371. it.dismiss()
  372. }
  373. }
  374. }
  375. private fun initializeSettings() {
  376. subscriptions += preferences.showPageNumber().asObservable()
  377. .subscribe { setPageNumberVisibility(it) }
  378. subscriptions += preferences.rotation().asObservable()
  379. .subscribe {
  380. setRotation(it)
  381. val isPortrait = resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
  382. val resourceId = if (it == 1)
  383. R.drawable.ic_screen_rotation_white_24dp
  384. else if (isPortrait)
  385. R.drawable.ic_screen_lock_portrait_white_24dp
  386. else
  387. R.drawable.ic_screen_lock_landscape_white_24dp
  388. lock_orientation.setImageResource(resourceId)
  389. }
  390. subscriptions += preferences.hideStatusBar().asObservable()
  391. .subscribe { setStatusBarVisibility(it) }
  392. subscriptions += preferences.keepScreenOn().asObservable()
  393. .subscribe { setKeepScreenOn(it) }
  394. subscriptions += preferences.customBrightness().asObservable()
  395. .subscribe { setCustomBrightness(it) }
  396. subscriptions += preferences.readerTheme().asObservable()
  397. .distinctUntilChanged()
  398. .subscribe { applyTheme(it) }
  399. }
  400. private fun setRotation(rotation: Int) {
  401. when (rotation) {
  402. // Rotation free
  403. 1 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
  404. // Lock in current rotation
  405. 2 -> {
  406. val currentOrientation = resources.configuration.orientation
  407. setRotation(if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) 3 else 4)
  408. }
  409. // Lock in portrait
  410. 3 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
  411. // Lock in landscape
  412. 4 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
  413. }
  414. }
  415. private fun setPageNumberVisibility(visible: Boolean) {
  416. page_number.visibility = if (visible) View.VISIBLE else View.INVISIBLE
  417. }
  418. private fun setKeepScreenOn(enabled: Boolean) {
  419. if (enabled) {
  420. window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
  421. } else {
  422. window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
  423. }
  424. }
  425. private fun setCustomBrightness(enabled: Boolean) {
  426. if (enabled) {
  427. customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
  428. .map { Math.max(0.01f, it) }
  429. .subscribe { setCustomBrightnessValue(it) }
  430. subscriptions.add(customBrightnessSubscription)
  431. } else {
  432. if (customBrightnessSubscription != null) {
  433. subscriptions.remove(customBrightnessSubscription)
  434. }
  435. setCustomBrightnessValue(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE)
  436. }
  437. }
  438. private fun setCustomBrightnessValue(value: Float) {
  439. window.attributes = window.attributes.apply { screenBrightness = value }
  440. }
  441. private fun setStatusBarVisibility(hidden: Boolean) {
  442. createUiHideFlags(hidden)
  443. setSystemUiVisibility()
  444. }
  445. private fun createUiHideFlags(statusBarHidden: Boolean) {
  446. uiFlags = 0
  447. uiFlags = uiFlags or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
  448. if (statusBarHidden) {
  449. uiFlags = uiFlags or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
  450. View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
  451. View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
  452. View.SYSTEM_UI_FLAG_FULLSCREEN
  453. }
  454. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
  455. uiFlags = uiFlags or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
  456. }
  457. }
  458. fun setSystemUiVisibility() {
  459. window.decorView.systemUiVisibility = uiFlags
  460. }
  461. private fun applyTheme(theme: Int) {
  462. readerTheme = theme
  463. val rootView = window.decorView.rootView
  464. if (theme == BLACK_THEME) {
  465. rootView.setBackgroundColor(Color.BLACK)
  466. page_number.setTextColor(ContextCompat.getColor(this, R.color.textColorPrimaryDark))
  467. page_number.setBackgroundColor(ContextCompat.getColor(this, R.color.pageNumberBackgroundDark))
  468. } else {
  469. rootView.setBackgroundColor(Color.WHITE)
  470. page_number.setTextColor(ContextCompat.getColor(this, R.color.textColorPrimaryLight))
  471. page_number.setBackgroundColor(ContextCompat.getColor(this, R.color.pageNumberBackgroundLight))
  472. }
  473. }
  474. private fun showImmersiveDialog(dialog: Dialog) {
  475. // Hack to not leave immersive mode
  476. dialog.window.setFlags(FLAG_NOT_FOCUSABLE, FLAG_NOT_FOCUSABLE)
  477. dialog.show()
  478. dialog.window.decorView.systemUiVisibility = window.decorView.systemUiVisibility
  479. dialog.window.clearFlags(FLAG_NOT_FOCUSABLE)
  480. }
  481. }