react-native-readium 4.0.1 → 5.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +93 -9
  2. package/android/build.gradle +32 -29
  3. package/android/gradle.properties +3 -3
  4. package/android/src/main/java/com/reactnativereadium/ReadiumView.kt +88 -28
  5. package/android/src/main/java/com/reactnativereadium/ReadiumViewManager.kt +20 -19
  6. package/android/src/main/java/com/reactnativereadium/reader/BaseReaderFragment.kt +27 -6
  7. package/android/src/main/java/com/reactnativereadium/reader/EpubReaderFragment.kt +53 -69
  8. package/android/src/main/java/com/reactnativereadium/reader/PositionLabelManager.kt +132 -0
  9. package/android/src/main/java/com/reactnativereadium/reader/ReaderService.kt +64 -65
  10. package/android/src/main/java/com/reactnativereadium/reader/ReaderViewModel.kt +6 -85
  11. package/android/src/main/java/com/reactnativereadium/reader/VisualReaderFragment.kt +32 -2
  12. package/android/src/main/java/com/reactnativereadium/utils/FragmentFactory.kt +2 -3
  13. package/android/src/main/java/com/reactnativereadium/utils/JsonExtensions.kt +61 -0
  14. package/android/src/main/java/com/reactnativereadium/utils/MetadataNormalizer.kt +183 -0
  15. package/android/src/main/java/com/reactnativereadium/utils/NormalizedMetadata.kt +179 -0
  16. package/android/src/main/java/com/reactnativereadium/utils/extensions/InputStream.kt +0 -9
  17. package/ios/App/AppModule.swift +3 -9
  18. package/ios/Common/Toolkit/Extensions/Locator.swift +1 -1
  19. package/ios/Data/Bookmark.swift +1 -1
  20. package/ios/Reader/Common/ReaderViewController.swift +118 -21
  21. package/ios/Reader/EPUB/AssociatedColors.swift +1 -1
  22. package/ios/Reader/EPUB/EPUBHTTPServer.swift +13 -0
  23. package/ios/Reader/EPUB/EPUBModule.swift +1 -1
  24. package/ios/Reader/EPUB/EPUBViewController.swift +3 -4
  25. package/ios/Reader/ReaderFormatModule.swift +1 -1
  26. package/ios/Reader/ReaderModule.swift +1 -1
  27. package/ios/Reader/ReaderService.swift +70 -35
  28. package/ios/ReadiumView.swift +62 -25
  29. package/ios/ReadiumViewManager.m +1 -1
  30. package/lib/src/ReadiumViewNativeComponent.d.ts +19 -0
  31. package/lib/src/ReadiumViewNativeComponent.js +10 -0
  32. package/lib/src/components/BaseReadiumView.d.ts +1 -2
  33. package/lib/src/components/BaseReadiumView.js +3 -7
  34. package/lib/src/components/ReadiumView.js +15 -15
  35. package/lib/src/components/ReadiumView.web.js +100 -21
  36. package/lib/src/interfaces/BaseReadiumViewProps.d.ts +2 -1
  37. package/lib/src/interfaces/Preferences.d.ts +3 -2
  38. package/lib/src/interfaces/PublicationMetadata.d.ts +114 -0
  39. package/lib/src/interfaces/PublicationMetadata.js +1 -0
  40. package/lib/src/interfaces/PublicationReady.d.ts +15 -0
  41. package/lib/src/interfaces/PublicationReady.js +1 -0
  42. package/lib/src/interfaces/index.d.ts +2 -0
  43. package/lib/src/interfaces/index.js +2 -0
  44. package/lib/src/utils/index.d.ts +0 -1
  45. package/lib/src/utils/index.js +0 -1
  46. package/lib/web/hooks/index.d.ts +3 -2
  47. package/lib/web/hooks/index.js +3 -2
  48. package/lib/web/hooks/useLocationObserver.d.ts +2 -1
  49. package/lib/web/hooks/useLocationObserver.js +18 -11
  50. package/lib/web/hooks/useNavigator.d.ts +12 -0
  51. package/lib/web/hooks/useNavigator.js +87 -0
  52. package/lib/web/hooks/usePositionLabel.d.ts +9 -0
  53. package/lib/web/hooks/usePositionLabel.js +33 -0
  54. package/lib/web/hooks/usePreferencesObserver.d.ts +2 -0
  55. package/lib/web/hooks/usePreferencesObserver.js +54 -0
  56. package/lib/web/utils/manifestFetcher.d.ts +8 -0
  57. package/lib/web/utils/manifestFetcher.js +28 -0
  58. package/lib/web/utils/manifestNormalizer.d.ts +8 -0
  59. package/lib/web/utils/manifestNormalizer.js +70 -0
  60. package/lib/web/utils/metadataNormalizer.d.ts +53 -0
  61. package/lib/web/utils/metadataNormalizer.js +220 -0
  62. package/lib/web/utils/navigatorListeners.d.ts +6 -0
  63. package/lib/web/utils/navigatorListeners.js +50 -0
  64. package/lib/web/utils/publicationUtils.d.ts +15 -0
  65. package/lib/web/utils/publicationUtils.js +39 -0
  66. package/package.json +24 -14
  67. package/react-native-readium.podspec +7 -5
  68. package/src/ReadiumViewNativeComponent.ts +35 -0
  69. package/src/components/BaseReadiumView.tsx +3 -10
  70. package/src/components/ReadiumView.tsx +15 -15
  71. package/src/components/ReadiumView.web.tsx +120 -27
  72. package/src/interfaces/BaseReadiumViewProps.ts +2 -1
  73. package/src/interfaces/Preferences.ts +3 -2
  74. package/src/interfaces/PublicationMetadata.ts +141 -0
  75. package/src/interfaces/PublicationReady.ts +18 -0
  76. package/src/interfaces/index.ts +2 -0
  77. package/src/utils/index.ts +0 -1
  78. package/web/hooks/index.ts +3 -2
  79. package/web/hooks/useLocationObserver.ts +24 -11
  80. package/web/hooks/useNavigator.ts +146 -0
  81. package/web/hooks/usePositionLabel.ts +51 -0
  82. package/web/hooks/usePreferencesObserver.ts +69 -0
  83. package/web/utils/manifestFetcher.ts +38 -0
  84. package/web/utils/manifestNormalizer.ts +74 -0
  85. package/web/utils/metadataNormalizer.ts +238 -0
  86. package/web/utils/navigatorListeners.ts +60 -0
  87. package/web/utils/publicationUtils.ts +47 -0
  88. package/android/src/main/java/com/reactnativereadium/search/SearchFragment.kt +0 -100
  89. package/android/src/main/java/com/reactnativereadium/search/SearchPagingSource.kt +0 -44
  90. package/android/src/main/java/com/reactnativereadium/search/SearchResultAdapter.kt +0 -68
  91. package/android/src/main/java/com/reactnativereadium/utils/R2DispatcherActivity.kt +0 -45
  92. package/android/src/main/java/com/reactnativereadium/utils/SectionDecoration.kt +0 -98
  93. package/android/src/main/java/com/reactnativereadium/utils/SingleClickListener.kt +0 -32
  94. package/android/src/main/java/com/reactnativereadium/utils/extensions/Bitmap.kt +0 -23
  95. package/android/src/main/java/com/reactnativereadium/utils/extensions/Context.kt +0 -16
  96. package/android/src/main/java/com/reactnativereadium/utils/extensions/File.kt +0 -22
  97. package/android/src/main/java/com/reactnativereadium/utils/extensions/Link.kt +0 -6
  98. package/android/src/main/java/com/reactnativereadium/utils/extensions/Metadata.kt +0 -6
  99. package/android/src/main/java/com/reactnativereadium/utils/extensions/URL.kt +0 -29
  100. package/android/src/main/java/com/reactnativereadium/utils/extensions/Uri.kt +0 -17
  101. package/android/src/main/res/layout/fragment_search.xml +0 -39
  102. package/android/src/main/res/layout/item_recycle_search.xml +0 -14
  103. package/ios/Common/EPUBPreferences.swift +0 -8
  104. package/ios/Common/Paths.swift +0 -52
  105. package/ios/Common/Publication.swift +0 -15
  106. package/ios/Common/Toolkit/Extensions/HTTPClient.swift +0 -65
  107. package/ios/Common/Toolkit/Extensions/UIImage.swift +0 -12
  108. package/ios/Common/Toolkit/Extensions/UIViewController.swift +0 -19
  109. package/ios/Common/Toolkit/ScreenOrientation.swift +0 -13
  110. package/lib/src/utils/createFragment.d.ts +0 -1
  111. package/lib/src/utils/createFragment.js +0 -10
  112. package/lib/web/hooks/useReaderRef.d.ts +0 -3
  113. package/lib/web/hooks/useReaderRef.js +0 -85
  114. package/lib/web/hooks/useSettingsObserver.d.ts +0 -2
  115. package/lib/web/hooks/useSettingsObserver.js +0 -44
  116. package/src/utils/createFragment.ts +0 -15
  117. package/web/hooks/useReaderRef.ts +0 -109
  118. package/web/hooks/useSettingsObserver.ts +0 -61
@@ -6,49 +6,50 @@
6
6
 
7
7
  package com.reactnativereadium.reader
8
8
 
9
- import android.graphics.Color
10
- import android.graphics.PointF
11
9
  import android.os.Bundle
12
10
  import android.view.*
13
11
  import android.view.accessibility.AccessibilityManager
14
12
  import androidx.appcompat.app.AppCompatActivity
15
- import androidx.appcompat.widget.SearchView
16
13
  import androidx.fragment.app.commitNow
17
14
  import androidx.lifecycle.ViewModelProvider
18
15
  import com.reactnativereadium.R
19
- import com.reactnativereadium.utils.toggleSystemUi
20
- import java.net.URL
21
- import kotlinx.coroutines.delay
22
16
  import org.readium.r2.navigator.epub.EpubNavigatorFragment
23
- import org.readium.r2.navigator.ExperimentalDecorator
24
17
  import org.readium.r2.navigator.Navigator
25
18
  import org.readium.r2.navigator.epub.EpubPreferences
26
19
  import org.readium.r2.navigator.epub.EpubPreferencesSerializer
20
+ import org.readium.r2.navigator.epub.EpubNavigatorFactory
27
21
  import org.readium.r2.shared.publication.Locator
28
22
  import org.readium.r2.shared.publication.Publication
29
- import org.readium.r2.shared.ReadiumCSSName
23
+ import org.readium.r2.navigator.preferences.Theme
30
24
 
31
- @OptIn(ExperimentalDecorator::class)
32
- class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listener {
25
+ class EpubReaderFragment : VisualReaderFragment() {
33
26
 
34
27
  override lateinit var model: ReaderViewModel
35
28
  override lateinit var navigator: Navigator
36
29
  private lateinit var publication: Publication
37
30
  lateinit var navigatorFragment: EpubNavigatorFragment
38
31
  private lateinit var factory: ReaderViewModel.Factory
32
+ private lateinit var navigatorFactory: EpubNavigatorFactory
33
+ private val preferencesSerializer = EpubPreferencesSerializer()
39
34
  private var initialPreferencesJsonString: String? = null
40
35
 
41
- private lateinit var menuScreenReader: MenuItem
42
- private lateinit var menuSearch: MenuItem
43
- lateinit var menuSearchView: SearchView
44
-
45
36
  private lateinit var userPreferences: EpubPreferences
46
- private var isScreenReaderVisible = false
47
- private var isSearchViewIconified = true
48
37
 
49
38
  // Accessibility
50
39
  private var isExploreByTouchEnabled = false
51
40
 
41
+ private fun ensureUserPreferencesInitialized() {
42
+ if (this::userPreferences.isInitialized) return
43
+ userPreferences = initialPreferencesJsonString?.let {
44
+ preferencesSerializer.deserialize(it)
45
+ } ?: EpubPreferences()
46
+ }
47
+
48
+ private fun applyPendingPreferencesIfNeeded() {
49
+ if (!this::navigator.isInitialized) return
50
+ initialPreferencesJsonString?.let { updatePreferencesFromJsonString(it) }
51
+ }
52
+
52
53
  fun initFactory(
53
54
  publication: Publication,
54
55
  initialLocation: Locator?
@@ -57,29 +58,36 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene
57
58
  publication,
58
59
  initialLocation
59
60
  )
61
+ navigatorFactory = EpubNavigatorFactory(publication)
60
62
  }
61
63
 
62
64
  fun updatePreferencesFromJsonString(serialisedPreferences: String) {
63
- if (this::userPreferences.isInitialized) {
64
- val serializer = EpubPreferencesSerializer()
65
- this.userPreferences = serializer.deserialize(serialisedPreferences)
66
- if (navigator is EpubNavigatorFragment) {
67
- (navigator as EpubNavigatorFragment).submitPreferences(this.userPreferences)
68
- }
65
+ userPreferences = preferencesSerializer.deserialize(serialisedPreferences)
66
+
67
+ if (this::navigator.isInitialized && navigator is EpubNavigatorFragment) {
68
+ (navigator as EpubNavigatorFragment).submitPreferences(userPreferences)
69
69
  initialPreferencesJsonString = null
70
+
71
+ // Update position label color to match theme, similar to iOS implementation
72
+ updatePositionLabelColor()
70
73
  } else {
71
74
  initialPreferencesJsonString = serialisedPreferences
72
75
  }
73
76
  }
74
77
 
75
- override fun onCreate(savedInstanceState: Bundle?) {
76
- // FIXME: this should be checked
77
- // check(R2App.isServerStarted)
78
+ private fun updatePositionLabelColor() {
79
+ // Priority 1: Use explicit textColor if set
80
+ val color = userPreferences.textColor?.int
81
+ // Priority 2: Use theme's content color
82
+ ?: userPreferences.theme?.contentColor
83
+ // Priority 3: Default to dark gray
84
+ ?: android.graphics.Color.DKGRAY
78
85
 
79
- if (savedInstanceState != null) {
80
- isScreenReaderVisible = savedInstanceState.getBoolean(IS_SCREEN_READER_VISIBLE_KEY)
81
- isSearchViewIconified = savedInstanceState.getBoolean(IS_SEARCH_VIEW_ICONIFIED)
82
- }
86
+ setPositionLabelColor(color)
87
+ }
88
+
89
+ override fun onCreate(savedInstanceState: Bundle?) {
90
+ check(::navigatorFactory.isInitialized) { "EpubReaderFragment factory was not initialized" }
83
91
 
84
92
  ViewModelProvider(this, factory)
85
93
  .get(ReaderViewModel::class.java)
@@ -88,18 +96,12 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene
88
96
  publication = it.publication
89
97
  }
90
98
 
99
+ ensureUserPreferencesInitialized()
91
100
 
92
- childFragmentManager.fragmentFactory =
93
- EpubNavigatorFragment.createFactory(
94
- publication = publication,
95
- initialLocator = model.initialLocation,
96
- listener = this,
97
- config = EpubNavigatorFragment.Configuration().apply {
98
- // Register the HTML template for our custom [DecorationStyleAnnotationMark].
99
- // TODO: remove?
100
- /* decorationTemplates[DecorationStyleAnnotationMark::class] = annotationMarkTemplate(activity) */
101
- /* selectionActionModeCallback = customSelectionActionModeCallback */
102
- }
101
+ childFragmentManager.fragmentFactory =
102
+ navigatorFactory.createFragmentFactory(
103
+ initialLocator = model.initialLocation,
104
+ initialPreferences = userPreferences,
103
105
  )
104
106
 
105
107
  setHasOptionsMenu(true)
@@ -119,58 +121,40 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene
119
121
  navigator = childFragmentManager.findFragmentByTag(navigatorFragmentTag) as Navigator
120
122
  navigatorFragment = navigator as EpubNavigatorFragment
121
123
 
124
+ applyPendingPreferencesIfNeeded()
125
+
122
126
  return view
123
127
  }
124
128
 
125
129
  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
126
130
  super.onViewCreated(view, savedInstanceState)
131
+
132
+ // Set initial position label color based on current preferences
133
+ updatePositionLabelColor()
127
134
  }
128
135
 
129
136
  override fun onResume() {
130
137
  super.onResume()
131
138
  val activity = requireActivity()
132
139
 
133
- if (!this::userPreferences.isInitialized) {
134
- userPreferences = EpubPreferences()
135
- }
136
- initialPreferencesJsonString?.let { updatePreferencesFromJsonString(it)}
140
+ ensureUserPreferencesInitialized()
141
+ applyPendingPreferencesIfNeeded()
137
142
 
138
143
  // If TalkBack or any touch exploration service is activated we force scroll mode (and
139
144
  // override user preferences)
140
145
  val am = activity.getSystemService(AppCompatActivity.ACCESSIBILITY_SERVICE) as AccessibilityManager
141
146
  isExploreByTouchEnabled = am.isTouchExplorationEnabled
142
147
 
143
- if (isExploreByTouchEnabled) {
144
- this.userPreferences = this.userPreferences.plus(
145
- EpubPreferences(scroll = true)
146
- )
148
+ userPreferences = if (isExploreByTouchEnabled) {
149
+ userPreferences.plus(EpubPreferences(scroll = true))
147
150
  } else {
148
- if (publication.cssStyle != "cjk-vertical") {
149
- this.userPreferences = this.userPreferences.plus(
150
- EpubPreferences(scroll = null)
151
- )
152
- }
151
+ userPreferences.plus(EpubPreferences(scroll = null))
153
152
  }
154
- }
155
-
156
- override fun onSaveInstanceState(outState: Bundle) {
157
- super.onSaveInstanceState(outState)
158
- outState.putBoolean(IS_SCREEN_READER_VISIBLE_KEY, isScreenReaderVisible)
159
- outState.putBoolean(IS_SEARCH_VIEW_ICONIFIED, isSearchViewIconified)
160
- }
161
-
162
- override fun onTap(point: PointF): Boolean {
163
- return true
153
+ (navigator as? EpubNavigatorFragment)?.submitPreferences(userPreferences)
164
154
  }
165
155
 
166
156
  companion object {
167
157
 
168
- private const val SEARCH_FRAGMENT_TAG = "search"
169
-
170
- private const val IS_SCREEN_READER_VISIBLE_KEY = "isScreenReaderVisible"
171
-
172
- private const val IS_SEARCH_VIEW_ICONIFIED = "isSearchViewIconified"
173
-
174
158
  fun newInstance(): EpubReaderFragment {
175
159
  return EpubReaderFragment()
176
160
  }
@@ -0,0 +1,132 @@
1
+ package com.reactnativereadium.reader
2
+
3
+ import android.graphics.Color
4
+ import android.view.Gravity
5
+ import android.widget.FrameLayout
6
+ import android.widget.TextView
7
+ import kotlinx.coroutines.CoroutineScope
8
+ import kotlinx.coroutines.Job
9
+ import kotlinx.coroutines.launch
10
+ import org.readium.r2.shared.publication.Publication
11
+ import org.readium.r2.shared.publication.services.positions
12
+
13
+ /**
14
+ * Manages the position label display for the reader.
15
+ * Similar to iOS PositionLabelManager, this class handles the display of current position
16
+ * and total position count in a TextView overlay.
17
+ */
18
+ class PositionLabelManager(
19
+ private val containerView: FrameLayout,
20
+ private val publication: Publication,
21
+ private val lifecycleScope: CoroutineScope
22
+ ) {
23
+ private val label: TextView = TextView(containerView.context)
24
+ private var positionsCount: Int? = null
25
+ private var positionsLoadingJob: Job? = null
26
+ private var lastKnownPosition: Int? = null
27
+ private var lastKnownProgression: Double? = null
28
+
29
+ init {
30
+ setupLabel()
31
+ }
32
+
33
+ companion object {
34
+ private const val LABEL_TEXT_SIZE_SP = 12f
35
+ private const val LABEL_PADDING_HORIZONTAL_DP = 10
36
+ private const val LABEL_PADDING_VERTICAL_DP = 5
37
+ private const val LABEL_BOTTOM_MARGIN_DP = 20
38
+ private const val LABEL_ELEVATION_DP = 4f
39
+ }
40
+
41
+ private fun setupLabel() {
42
+ val density = containerView.resources.displayMetrics.density
43
+
44
+ label.apply {
45
+ textSize = LABEL_TEXT_SIZE_SP
46
+ setTextColor(Color.DKGRAY) // Default, will be updated based on theme
47
+ setBackgroundColor(Color.TRANSPARENT)
48
+ gravity = Gravity.CENTER
49
+ setPadding(
50
+ (LABEL_PADDING_HORIZONTAL_DP * density).toInt(),
51
+ (LABEL_PADDING_VERTICAL_DP * density).toInt(),
52
+ (LABEL_PADDING_HORIZONTAL_DP * density).toInt(),
53
+ (LABEL_PADDING_VERTICAL_DP * density).toInt()
54
+ )
55
+ elevation = LABEL_ELEVATION_DP * density
56
+ }
57
+
58
+ val params = FrameLayout.LayoutParams(
59
+ FrameLayout.LayoutParams.WRAP_CONTENT,
60
+ FrameLayout.LayoutParams.WRAP_CONTENT
61
+ ).apply {
62
+ gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
63
+ bottomMargin = (LABEL_BOTTOM_MARGIN_DP * density).toInt()
64
+ }
65
+
66
+ containerView.addView(label, params)
67
+ // Ensure label is above navigator fragment
68
+ label.bringToFront()
69
+ }
70
+
71
+ /**
72
+ * Update the position label with current position and progression.
73
+ * Should be called on the main thread.
74
+ */
75
+ fun update(position: Int?, totalProgression: Double?) {
76
+ lastKnownPosition = position
77
+ lastKnownProgression = totalProgression
78
+ label.text = positionLabelText(position, totalProgression)
79
+ }
80
+
81
+ private fun positionLabelText(position: Int?, totalProgression: Double?): String? {
82
+ return when {
83
+ position != null -> {
84
+ val total = positionsCount
85
+ if (total != null) {
86
+ "$position / $total"
87
+ } else {
88
+ loadPositionsCountIfNeeded()
89
+ "$position"
90
+ }
91
+ }
92
+ totalProgression != null -> {
93
+ "${(totalProgression * 100).toInt()}%"
94
+ }
95
+ else -> null
96
+ }
97
+ }
98
+
99
+ private fun loadPositionsCountIfNeeded() {
100
+ if (positionsCount != null) return
101
+ if (positionsLoadingJob?.isActive == true) return
102
+
103
+ positionsLoadingJob = lifecycleScope.launch {
104
+ try {
105
+ val positions = publication.positions()
106
+ positionsCount = positions.size
107
+
108
+ // Refresh label with the new count
109
+ label.text = positionLabelText(lastKnownPosition, lastKnownProgression)
110
+ } catch (e: Exception) {
111
+ // Failed to load positions, continue showing position without total
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Update the text color of the position label.
118
+ * Should be called on the main thread.
119
+ */
120
+ fun setTextColor(color: Int) {
121
+ label.setTextColor(color)
122
+ }
123
+
124
+ /**
125
+ * Cancel any ongoing loading operations and remove the label from the view hierarchy.
126
+ */
127
+ fun cleanup() {
128
+ positionsLoadingJob?.cancel()
129
+ positionsLoadingJob = null
130
+ containerView.removeView(label)
131
+ }
132
+ }
@@ -1,49 +1,37 @@
1
1
  package com.reactnativereadium.reader
2
2
 
3
- import android.annotation.SuppressLint
4
- import androidx.lifecycle.ViewModelStore
5
3
  import com.facebook.react.bridge.ReactApplicationContext
6
4
  import com.facebook.react.util.RNLog
7
5
  import com.reactnativereadium.utils.LinkOrLocator
8
6
  import java.io.File
9
- import java.io.IOException
10
- import java.net.ServerSocket
11
- import org.readium.r2.shared.extensions.mediaType
12
- import org.readium.r2.shared.extensions.tryOrNull
7
+ import java.util.Locale
13
8
  import org.readium.r2.shared.publication.Locator
14
- import org.readium.r2.shared.publication.asset.FileAsset
15
9
  import org.readium.r2.shared.publication.Publication
16
- import org.readium.r2.streamer.server.Server
17
- import org.readium.r2.streamer.Streamer
10
+ import org.readium.r2.shared.util.FileExtension
11
+ import org.readium.r2.shared.util.asset.AssetRetriever
12
+ import org.readium.r2.shared.util.format.FormatHints
13
+ import org.readium.r2.shared.util.http.DefaultHttpClient
14
+ import org.readium.r2.shared.util.toUrl
15
+ import org.readium.r2.streamer.PublicationOpener
16
+ import org.readium.r2.streamer.parser.DefaultPublicationParser
18
17
 
19
18
 
20
19
  class ReaderService(
21
20
  private val reactContext: ReactApplicationContext
22
21
  ) {
23
- private var streamer = Streamer(reactContext)
24
- // see R2App.onCreate
25
- private var server: Server
26
- // val channel = EventChannel(Channel<Event>(Channel.BUFFERED), viewModelScope)
27
- private var store = ViewModelStore()
28
-
29
- companion object {
30
- @SuppressLint("StaticFieldLeak")
31
- lateinit var server: Server
32
- private set
33
-
34
- lateinit var R2DIRECTORY: String
35
- private set
36
-
37
- var isServerStarted = false
38
- private set
39
- }
40
-
41
- init {
42
- val s = ServerSocket(0)
43
- s.close()
44
- server = Server(s.localPort, reactContext)
45
- this.startServer()
46
- }
22
+ private val httpClient = DefaultHttpClient()
23
+ private val assetRetriever = AssetRetriever(
24
+ reactContext.contentResolver,
25
+ httpClient
26
+ )
27
+ private val publicationOpener = PublicationOpener(
28
+ publicationParser = DefaultPublicationParser(
29
+ context = reactContext,
30
+ assetRetriever = assetRetriever,
31
+ httpClient = httpClient,
32
+ pdfFactory = null,
33
+ )
34
+ )
47
35
 
48
36
  fun locatorFromLinkOrLocator(
49
37
  location: LinkOrLocator?,
@@ -69,44 +57,55 @@ class ReaderService(
69
57
  initialLocation: LinkOrLocator?,
70
58
  callback: suspend (fragment: BaseReaderFragment) -> Unit
71
59
  ) {
72
- val file = File(fileName)
73
- val asset = FileAsset(file, file.mediaType())
60
+ val publicationFile = File(fileName).absoluteFile
61
+ if (!publicationFile.exists()) {
62
+ RNLog.e(reactContext, "Failed to open publication: File does not exist: $fileName")
63
+ return
64
+ }
65
+ val publicationUrl = runCatching {
66
+ publicationFile.toUrl()
67
+ }
68
+ .onFailure {
69
+ RNLog.e(
70
+ reactContext,
71
+ "Invalid publication path: $fileName - ${it.message}"
72
+ )
73
+ }
74
+ .getOrNull()
75
+ ?: return
74
76
 
75
- streamer.open(
76
- asset,
77
- allowUserInteraction = false,
78
- sender = reactContext
79
- )
80
- .onSuccess {
81
- val locator = locatorFromLinkOrLocator(initialLocation, it)
82
- val readerFragment = EpubReaderFragment.newInstance()
83
- readerFragment.initFactory(it, locator)
84
- callback.invoke(readerFragment)
77
+ val fileExtension = publicationFile.extension
78
+ .takeIf { it.isNotEmpty() }?.lowercase(Locale.ROOT)
85
79
 
86
- }
80
+ val asset = assetRetriever
81
+ .retrieve(
82
+ publicationUrl,
83
+ FormatHints(fileExtension = fileExtension?.let { FileExtension(it) })
84
+ )
87
85
  .onFailure {
88
- tryOrNull { asset.file.delete() }
89
- RNLog.w(reactContext, "Error executing ReaderService.openPublication")
90
- // TODO: implement failure event
86
+ RNLog.w(reactContext, "Unable to retrieve publication asset: ${it.message}")
91
87
  }
92
- }
93
-
94
- private fun startServer() {
95
- if (!server.isAlive) {
96
- try {
97
- server.start()
98
- } catch (e: IOException) {
99
- RNLog.e(reactContext, "Unable to start the Readium server.")
88
+ .getOrNull()
89
+ ?: return
90
+
91
+ publicationOpener
92
+ .open(
93
+ asset = asset,
94
+ allowUserInteraction = false
95
+ )
96
+ .onSuccess {
97
+ val locator = locatorFromLinkOrLocator(initialLocation, it)
98
+ val readerFragment = EpubReaderFragment.newInstance()
99
+ readerFragment.initFactory(it, locator)
100
+ callback.invoke(readerFragment)
100
101
  }
101
- if (server.isAlive) {
102
- // // Add your own resources here
103
- // server.loadCustomResource(assets.open("scripts/test.js"), "test.js")
104
- // server.loadCustomResource(assets.open("styles/test.css"), "test.css")
105
- // server.loadCustomFont(assets.open("fonts/test.otf"), applicationContext, "test.otf")
106
-
107
- isServerStarted = true
102
+ .onFailure {
103
+ RNLog.w(
104
+ reactContext,
105
+ "Error executing ReaderService.openPublication: ${it.message}"
106
+ )
107
+ // TODO: implement failure event
108
108
  }
109
- }
110
109
  }
111
110
 
112
111
  sealed class Event {
@@ -1,95 +1,21 @@
1
1
  package com.reactnativereadium.reader
2
2
 
3
- import android.graphics.Color
4
3
  import androidx.lifecycle.ViewModel
5
4
  import androidx.lifecycle.ViewModelProvider
6
5
  import androidx.lifecycle.viewModelScope
7
- import androidx.paging.*
8
- import com.reactnativereadium.search.SearchPagingSource
9
6
  import com.reactnativereadium.utils.EventChannel
10
7
  import kotlinx.coroutines.channels.Channel
11
- import kotlinx.coroutines.flow.*
12
- import kotlinx.coroutines.launch
13
- import org.readium.r2.navigator.Decoration
14
- import org.readium.r2.navigator.ExperimentalDecorator
15
8
  import org.readium.r2.shared.publication.Locator
16
- import org.readium.r2.shared.publication.LocatorCollection
17
9
  import org.readium.r2.shared.publication.Publication
18
- import org.readium.r2.shared.publication.services.search.search
19
- import org.readium.r2.shared.publication.services.search.SearchIterator
20
- import org.readium.r2.shared.publication.services.search.SearchTry
21
- import org.readium.r2.shared.Search
22
- import org.readium.r2.shared.UserException
23
10
  import org.readium.r2.shared.publication.Link
24
- import org.readium.r2.shared.util.Try
11
+ import org.readium.r2.shared.publication.Metadata
25
12
 
26
- @OptIn(Search::class, ExperimentalDecorator::class)
27
13
  class ReaderViewModel(
28
14
  val publication: Publication,
29
15
  val initialLocation: Locator?
30
16
  ) : ViewModel() {
31
17
  val channel = EventChannel(Channel<Event>(Channel.BUFFERED), viewModelScope)
32
18
 
33
- fun search(query: String) = viewModelScope.launch {
34
- if (query == lastSearchQuery) return@launch
35
- lastSearchQuery = query
36
- _searchLocators.value = emptyList()
37
- searchIterator = publication.search(query)
38
- .onFailure { channel.send(Event.Failure(it)) }
39
- .getOrNull()
40
- pagingSourceFactory.invalidate()
41
- channel.send(Event.StartNewSearch)
42
- }
43
-
44
- fun cancelSearch() = viewModelScope.launch {
45
- _searchLocators.value = emptyList()
46
- searchIterator?.close()
47
- searchIterator = null
48
- pagingSourceFactory.invalidate()
49
- }
50
-
51
- val searchLocators: StateFlow<List<Locator>> get() = _searchLocators
52
- private var _searchLocators = MutableStateFlow<List<Locator>>(emptyList())
53
-
54
- /**
55
- * Maps the current list of search result locators into a list of [Decoration] objects to
56
- * underline the results in the navigator.
57
- */
58
- val searchDecorations: Flow<List<Decoration>> by lazy {
59
- searchLocators.map {
60
- it.mapIndexed { index, locator ->
61
- Decoration(
62
- // The index in the search result list is a suitable Decoration ID, as long as
63
- // we clear the search decorations between two searches.
64
- id = index.toString(),
65
- locator = locator,
66
- style = Decoration.Style.Underline(tint = Color.RED)
67
- )
68
- }
69
- }
70
- }
71
-
72
- private var lastSearchQuery: String? = null
73
-
74
- private var searchIterator: SearchIterator? = null
75
-
76
- private val pagingSourceFactory = InvalidatingPagingSourceFactory {
77
- SearchPagingSource(listener = PagingSourceListener())
78
- }
79
-
80
- inner class PagingSourceListener : SearchPagingSource.Listener {
81
- override suspend fun next(): SearchTry<LocatorCollection?> {
82
- val iterator = searchIterator ?: return Try.success(null)
83
- return iterator.next().onSuccess {
84
- _searchLocators.value += (it?.locators ?: emptyList())
85
- }
86
- }
87
- }
88
-
89
- val searchResult: Flow<PagingData<Locator>> =
90
- Pager(PagingConfig(pageSize = 20), pagingSourceFactory = pagingSourceFactory)
91
- .flow.cachedIn(viewModelScope)
92
-
93
19
  class Factory(
94
20
  private val publication: Publication,
95
21
  private val initialLocation: Locator?
@@ -107,16 +33,11 @@ class ReaderViewModel(
107
33
  }
108
34
 
109
35
  sealed class Event {
110
- object OpenOutlineRequested : Event()
111
- object OpenDrmManagementRequested : Event()
112
- object StartNewSearch : Event()
113
- class Failure(val error: UserException) : Event()
114
36
  class LocatorUpdate(val locator: Locator) : Event()
115
- class TableOfContentsLoaded(val toc: List<Link>) : Event()
116
- }
117
-
118
- sealed class FeedbackEvent {
119
- object BookmarkSuccessfullyAdded : FeedbackEvent()
120
- object BookmarkFailed : FeedbackEvent()
37
+ class PublicationReady(
38
+ val tableOfContents: List<Link>,
39
+ val positions: List<Locator>,
40
+ val metadata: Metadata
41
+ ) : Event()
121
42
  }
122
43
  }