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.
- package/README.md +93 -9
- package/android/build.gradle +32 -29
- package/android/gradle.properties +3 -3
- package/android/src/main/java/com/reactnativereadium/ReadiumView.kt +88 -28
- package/android/src/main/java/com/reactnativereadium/ReadiumViewManager.kt +20 -19
- package/android/src/main/java/com/reactnativereadium/reader/BaseReaderFragment.kt +27 -6
- package/android/src/main/java/com/reactnativereadium/reader/EpubReaderFragment.kt +53 -69
- package/android/src/main/java/com/reactnativereadium/reader/PositionLabelManager.kt +132 -0
- package/android/src/main/java/com/reactnativereadium/reader/ReaderService.kt +64 -65
- package/android/src/main/java/com/reactnativereadium/reader/ReaderViewModel.kt +6 -85
- package/android/src/main/java/com/reactnativereadium/reader/VisualReaderFragment.kt +32 -2
- package/android/src/main/java/com/reactnativereadium/utils/FragmentFactory.kt +2 -3
- package/android/src/main/java/com/reactnativereadium/utils/JsonExtensions.kt +61 -0
- package/android/src/main/java/com/reactnativereadium/utils/MetadataNormalizer.kt +183 -0
- package/android/src/main/java/com/reactnativereadium/utils/NormalizedMetadata.kt +179 -0
- package/android/src/main/java/com/reactnativereadium/utils/extensions/InputStream.kt +0 -9
- package/ios/App/AppModule.swift +3 -9
- package/ios/Common/Toolkit/Extensions/Locator.swift +1 -1
- package/ios/Data/Bookmark.swift +1 -1
- package/ios/Reader/Common/ReaderViewController.swift +118 -21
- package/ios/Reader/EPUB/AssociatedColors.swift +1 -1
- package/ios/Reader/EPUB/EPUBHTTPServer.swift +13 -0
- package/ios/Reader/EPUB/EPUBModule.swift +1 -1
- package/ios/Reader/EPUB/EPUBViewController.swift +3 -4
- package/ios/Reader/ReaderFormatModule.swift +1 -1
- package/ios/Reader/ReaderModule.swift +1 -1
- package/ios/Reader/ReaderService.swift +70 -35
- package/ios/ReadiumView.swift +62 -25
- package/ios/ReadiumViewManager.m +1 -1
- package/lib/src/ReadiumViewNativeComponent.d.ts +19 -0
- package/lib/src/ReadiumViewNativeComponent.js +10 -0
- package/lib/src/components/BaseReadiumView.d.ts +1 -2
- package/lib/src/components/BaseReadiumView.js +3 -7
- package/lib/src/components/ReadiumView.js +15 -15
- package/lib/src/components/ReadiumView.web.js +100 -21
- package/lib/src/interfaces/BaseReadiumViewProps.d.ts +2 -1
- package/lib/src/interfaces/Preferences.d.ts +3 -2
- package/lib/src/interfaces/PublicationMetadata.d.ts +114 -0
- package/lib/src/interfaces/PublicationMetadata.js +1 -0
- package/lib/src/interfaces/PublicationReady.d.ts +15 -0
- package/lib/src/interfaces/PublicationReady.js +1 -0
- package/lib/src/interfaces/index.d.ts +2 -0
- package/lib/src/interfaces/index.js +2 -0
- package/lib/src/utils/index.d.ts +0 -1
- package/lib/src/utils/index.js +0 -1
- package/lib/web/hooks/index.d.ts +3 -2
- package/lib/web/hooks/index.js +3 -2
- package/lib/web/hooks/useLocationObserver.d.ts +2 -1
- package/lib/web/hooks/useLocationObserver.js +18 -11
- package/lib/web/hooks/useNavigator.d.ts +12 -0
- package/lib/web/hooks/useNavigator.js +87 -0
- package/lib/web/hooks/usePositionLabel.d.ts +9 -0
- package/lib/web/hooks/usePositionLabel.js +33 -0
- package/lib/web/hooks/usePreferencesObserver.d.ts +2 -0
- package/lib/web/hooks/usePreferencesObserver.js +54 -0
- package/lib/web/utils/manifestFetcher.d.ts +8 -0
- package/lib/web/utils/manifestFetcher.js +28 -0
- package/lib/web/utils/manifestNormalizer.d.ts +8 -0
- package/lib/web/utils/manifestNormalizer.js +70 -0
- package/lib/web/utils/metadataNormalizer.d.ts +53 -0
- package/lib/web/utils/metadataNormalizer.js +220 -0
- package/lib/web/utils/navigatorListeners.d.ts +6 -0
- package/lib/web/utils/navigatorListeners.js +50 -0
- package/lib/web/utils/publicationUtils.d.ts +15 -0
- package/lib/web/utils/publicationUtils.js +39 -0
- package/package.json +24 -14
- package/react-native-readium.podspec +7 -5
- package/src/ReadiumViewNativeComponent.ts +35 -0
- package/src/components/BaseReadiumView.tsx +3 -10
- package/src/components/ReadiumView.tsx +15 -15
- package/src/components/ReadiumView.web.tsx +120 -27
- package/src/interfaces/BaseReadiumViewProps.ts +2 -1
- package/src/interfaces/Preferences.ts +3 -2
- package/src/interfaces/PublicationMetadata.ts +141 -0
- package/src/interfaces/PublicationReady.ts +18 -0
- package/src/interfaces/index.ts +2 -0
- package/src/utils/index.ts +0 -1
- package/web/hooks/index.ts +3 -2
- package/web/hooks/useLocationObserver.ts +24 -11
- package/web/hooks/useNavigator.ts +146 -0
- package/web/hooks/usePositionLabel.ts +51 -0
- package/web/hooks/usePreferencesObserver.ts +69 -0
- package/web/utils/manifestFetcher.ts +38 -0
- package/web/utils/manifestNormalizer.ts +74 -0
- package/web/utils/metadataNormalizer.ts +238 -0
- package/web/utils/navigatorListeners.ts +60 -0
- package/web/utils/publicationUtils.ts +47 -0
- package/android/src/main/java/com/reactnativereadium/search/SearchFragment.kt +0 -100
- package/android/src/main/java/com/reactnativereadium/search/SearchPagingSource.kt +0 -44
- package/android/src/main/java/com/reactnativereadium/search/SearchResultAdapter.kt +0 -68
- package/android/src/main/java/com/reactnativereadium/utils/R2DispatcherActivity.kt +0 -45
- package/android/src/main/java/com/reactnativereadium/utils/SectionDecoration.kt +0 -98
- package/android/src/main/java/com/reactnativereadium/utils/SingleClickListener.kt +0 -32
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Bitmap.kt +0 -23
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Context.kt +0 -16
- package/android/src/main/java/com/reactnativereadium/utils/extensions/File.kt +0 -22
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Link.kt +0 -6
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Metadata.kt +0 -6
- package/android/src/main/java/com/reactnativereadium/utils/extensions/URL.kt +0 -29
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Uri.kt +0 -17
- package/android/src/main/res/layout/fragment_search.xml +0 -39
- package/android/src/main/res/layout/item_recycle_search.xml +0 -14
- package/ios/Common/EPUBPreferences.swift +0 -8
- package/ios/Common/Paths.swift +0 -52
- package/ios/Common/Publication.swift +0 -15
- package/ios/Common/Toolkit/Extensions/HTTPClient.swift +0 -65
- package/ios/Common/Toolkit/Extensions/UIImage.swift +0 -12
- package/ios/Common/Toolkit/Extensions/UIViewController.swift +0 -19
- package/ios/Common/Toolkit/ScreenOrientation.swift +0 -13
- package/lib/src/utils/createFragment.d.ts +0 -1
- package/lib/src/utils/createFragment.js +0 -10
- package/lib/web/hooks/useReaderRef.d.ts +0 -3
- package/lib/web/hooks/useReaderRef.js +0 -85
- package/lib/web/hooks/useSettingsObserver.d.ts +0 -2
- package/lib/web/hooks/useSettingsObserver.js +0 -44
- package/src/utils/createFragment.ts +0 -15
- package/web/hooks/useReaderRef.ts +0 -109
- 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.
|
|
23
|
+
import org.readium.r2.navigator.preferences.Theme
|
|
30
24
|
|
|
31
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
145
|
-
EpubPreferences(scroll = true)
|
|
146
|
-
)
|
|
148
|
+
userPreferences = if (isExploreByTouchEnabled) {
|
|
149
|
+
userPreferences.plus(EpubPreferences(scroll = true))
|
|
147
150
|
} else {
|
|
148
|
-
|
|
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.
|
|
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.
|
|
17
|
-
import org.readium.r2.
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
}
|