Skip to content

Commit d9573bc

Browse files
committed
fix: ttsSetVoice now works on both platforms
1 parent 442e55c commit d9573bc

File tree

11 files changed

+117
-71
lines changed

11 files changed

+117
-71
lines changed

flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/PublicationChannel.kt

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@file:OptIn(ExperimentalReadiumApi::class)
2+
13
package dk.nota.flutter_readium
24

35
import android.app.Application
@@ -29,19 +31,16 @@ import org.readium.r2.shared.util.resource.filename
2931
import org.readium.navigator.media.tts.android.AndroidTtsPreferences
3032
import org.readium.r2.shared.ExperimentalReadiumApi
3133
import org.readium.r2.shared.publication.Locator
34+
import org.readium.r2.shared.publication.services.content.DefaultContentService
35+
import org.readium.r2.shared.publication.services.content.contentServiceFactory
36+
import org.readium.r2.shared.publication.services.content.iterators.HtmlResourceContentIterator
37+
import org.readium.r2.shared.publication.services.search.StringSearchService
38+
import org.readium.r2.shared.publication.services.search.searchServiceFactory
3239

3340
private const val TAG = "PublicationChannel"
3441

3542
internal const val publicationChannelName = "dk.nota.flutter_readium/main"
36-
37-
private var readium: Readium? = null
38-
39-
// TODO: Do we still want to use this?
40-
private var publication: Publication? = null
41-
internal fun publicationFromHandle(): Publication? {
42-
return publication
43-
}
44-
43+
internal var readium: Readium? = null
4544
internal var currentReadiumReaderView: ReadiumReaderView? = null
4645

4746
// Collection of publications init to empty
@@ -67,6 +66,13 @@ private suspend fun assetToPublication(
6766
container = TransformingContainer(container) { _: Url, resource: Resource ->
6867
resource.injectScriptsAndStyles()
6968
}
69+
// TODO: Temporary fix for missing service factories for WebPubs with HTML content.
70+
servicesBuilder.contentServiceFactory = DefaultContentService.createFactory(
71+
resourceContentIteratorFactories = listOf(
72+
HtmlResourceContentIterator.Factory()
73+
)
74+
)
75+
servicesBuilder.searchServiceFactory = StringSearchService.createDefaultFactory()
7076
})
7177
.getOrElse { err: OpenError ->
7278
Log.e(TAG, "Error opening publication: $err")
@@ -97,7 +103,6 @@ private suspend fun openPublication(
97103
}
98104
Log.d(TAG, "Opened publication = ${pub.metadata.identifier}")
99105
publications[pub.metadata.identifier ?: pubUrl.toString()] = pub
100-
publication = pub
101106
// Manifest must now be manually turned into JSON
102107
val pubJsonManifest = pub.manifest.toJSON().toString().replace("\\/", "/")
103108
CoroutineScope(Dispatchers.Main).launch {
@@ -156,9 +161,21 @@ internal class PublicationMethodCallHandler(private val context: Context) :
156161
val args = call.arguments as Map<String, Any>?
157162
val ttsPrefs = if (args != null) androidTtsPreferencesFromMap(args) else AndroidTtsPreferences()
158163

159-
ttsViewModel = TTSViewModel(pluginAppContext as Application, publication!!, currentReadiumReaderView!!, ttsPrefs)
160-
ttsViewModel?.initNavigator()
161-
result.success(null)
164+
val pubId = currentReadiumReaderView?.currentPublicationIdentifier
165+
if (pubId == null || !publications.contains(pubId)) {
166+
Log.e(TAG, "ttsEnable: Cannot enable TTS for un-opened publication. PubId=$pubId")
167+
}
168+
val publication = publicationFromIdentifier(pubId!!)
169+
170+
171+
try {
172+
ttsViewModel = TTSViewModel(pluginAppContext as Application, publication!!, ttsPrefs)
173+
ttsViewModel?.initNavigator()
174+
result.success(null)
175+
} catch (e: Exception) {
176+
Log.e(TAG, "ttsEnable: Failed to create TTSViewModel (likely navigator). PubId=$pubId")
177+
result.error("ttsEnable", "Failed to create TTSModel", e.message)
178+
}
162179
}
163180

164181
"ttsSetPreferences" -> {
@@ -240,34 +257,26 @@ internal class PublicationMethodCallHandler(private val context: Context) :
240257
result.success(null)
241258
}
242259

243-
"get" -> {
260+
"getLinkContent" -> {
244261
try {
245262
val args = call.arguments as List<Any?>
246-
val isLink = args[0] as Boolean
247-
val linkData = args[1] as String
248-
val asString = args[2] as Boolean
249-
val link: Link
250-
if (isLink) {
251-
link = Link.fromJSON(JSONObject(linkData))!!
252-
} else {
253-
val url = Url.fromEpubHref(linkData) ?: run {
254-
Log.e(TAG, "get: invalid EPUB href $linkData")
255-
throw Exception("get: invalid EPUB href $linkData")
256-
}
257-
link = Link(url)
263+
val pubId = args[0] as String
264+
val linkStr = args[1] as String
265+
val asString = args[2] as? Boolean ?: true
266+
val link = Link.fromJSON(JSONObject(linkStr))
267+
val publication = publications[pubId]
268+
269+
if (publication == null || link == null) {
270+
throw Exception("getLinkContent: failed to get resource. Missing pub or link: $publication, $link")
258271
}
272+
259273
Log.d(TAG, "Use publication = $publication")
260-
// TODO Debug why the next line crashed with a NullPointerException one time. Probably
261-
// somehow related to the server being re-indexed. Was an invalid publication somehow
262-
// created, or was a valid publication disposed and then used?
263274

264-
val resource = publication!!.get(link) ?: run {
265-
Log.e(TAG, "get: failed to get resource via link $link")
266-
throw Exception("failed to get resource via link $link")
275+
val resource = publication.get(link) ?: run {
276+
throw Exception("getLinkContent: failed to find pub resource via link: pubId=${publication.metadata.identifier},link=$link")
267277
}
268278
val resourceBytes = resource.read().getOrElse {
269-
Log.e(TAG, "get: invalid EPUB href $linkData")
270-
throw Exception("get: invalid EPUB href $linkData")
279+
throw Exception("getLinkContent: failed to read resource. ${it.message}")
271280
}
272281

273282
CoroutineScope(Dispatchers.Main).launch {

flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/Readium.kt

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,13 @@
77
package dk.nota.flutter_readium
88

99
import android.content.Context
10-
import android.util.Log
10+
import org.readium.r2.shared.ExperimentalReadiumApi
1111
// import org.readium.adapter.pdfium.document.PdfiumDocumentFactory
1212
// import org.readium.r2.lcp.LcpError
1313
// import org.readium.r2.lcp.LcpService
1414
// import org.readium.r2.lcp.auth.LcpDialogAuthentication
15-
import org.readium.r2.shared.publication.Publication
16-
import org.readium.r2.shared.publication.presentation.presentation
17-
import org.readium.r2.shared.publication.services.content.contentServiceFactory
18-
import org.readium.r2.shared.util.Try
19-
import org.readium.r2.shared.util.Url
20-
//import org.readium.r2.shared.util.DebugError
21-
//import org.readium.r2.shared.util.Try
2215
import org.readium.r2.shared.util.asset.AssetRetriever
2316
import org.readium.r2.shared.util.http.DefaultHttpClient
24-
import org.readium.r2.shared.util.resource.Resource
25-
import org.readium.r2.shared.util.resource.TransformingContainer
26-
import org.readium.r2.shared.util.resource.TransformingResource
27-
import org.readium.r2.shared.util.resource.filename
2817
import org.readium.r2.streamer.PublicationOpener
2918
import org.readium.r2.streamer.parser.DefaultPublicationParser
3019

flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderView.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ internal class ReadiumReaderView(
4040
private var userPreferences = EpubPreferences()
4141
private var initialLocations: Locator.Locations?
4242

43-
// Create a CoroutineScope using the Main (UI) dispatcher
44-
// TODO: What was/is this used for?
45-
private var scope = CoroutineScope(Dispatchers.Main)
43+
val currentPublicationIdentifier: String
4644

4745
override fun getView(): View {
4846
//Log.d(TAG, "::getView")
@@ -82,6 +80,7 @@ internal class ReadiumReaderView(
8280
val initialPreferences =
8381
if (initPrefsMap == null) null else epubPreferencesFromMap(initPrefsMap, null)
8482
Log.d(TAG, "publication = $publication")
83+
currentPublicationIdentifier = pubIdentifier
8584

8685
initialLocations = initialLocator?.locations?.let { if (canScroll(it)) it else null }
8786
readiumView = EpubNavigatorView(context, publication, initialLocator, initialPreferences, this)
@@ -315,6 +314,7 @@ internal class ReadiumReaderView(
315314
}
316315
"dispose" -> {
317316
readiumView.removeAllViews()
317+
currentReadiumReaderView = null
318318
initialLocations = null
319319
eventSink = null
320320
eventChannel.setStreamHandler(null)

flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/TTSViewModel.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ private const val TTS_DECORATION_ID_CURRENT_RANGE = "tts-range"
3939
internal class TTSViewModel(
4040
private val appContext: Context,
4141
private val publication: Publication,
42-
private val reader: ReadiumReaderView,
4342
private var preferences: AndroidTtsPreferences = AndroidTtsPreferences()
4443
) {
4544
private val jobs = mutableListOf<Job>()
@@ -65,7 +64,7 @@ internal class TTSViewModel(
6564
}
6665
}
6766
CoroutineScope(Dispatchers.Main).async {
68-
val firstVisibleLocator = this@TTSViewModel.reader.getFirstVisibleLocator()
67+
val firstVisibleLocator = currentReadiumReaderView?.getFirstVisibleLocator()
6968

7069
val ttsNavigator = factory.createNavigator(listener, firstVisibleLocator, preferences).getOrElse {
7170
Log.e(TAG, "ttsEnable: failed to create navigator: $it")
@@ -106,10 +105,14 @@ internal class TTSViewModel(
106105
}
107106

108107
fun setPreferredVoice(voiceId: String, lang: String?) {
108+
// Modify existing map of voice overrides, in case user sets multiple preferred voices.
109+
val voices = this.preferences.voices?.toMutableMap() ?: mutableMapOf()
109110
// If no lang provided, assume client wants to override currently spoken language.
110111
val language = if (lang != null) Language(lang) else this.ttsNavigator?.settings?.value?.language
111-
val voices = if (language != null) mapOf(language to AndroidTtsEngine.Voice.Id(voiceId)) else emptyMap()
112-
this.updatePreferences(AndroidTtsPreferences(voices = voices))
112+
if (language != null) {
113+
voices[language] = AndroidTtsEngine.Voice.Id(voiceId)
114+
this.updatePreferences(AndroidTtsPreferences(voices = voices))
115+
}
113116
}
114117

115118
val voices: Set<AndroidTtsEngine.Voice>
@@ -155,7 +158,7 @@ internal class TTSViewModel(
155158
)
156159
}
157160
CoroutineScope(Dispatchers.Main).launch {
158-
this@TTSViewModel.reader.applyDecorations(decorations, group = "tts")
161+
currentReadiumReaderView?.applyDecorations(decorations, group = "tts")
159162
}
160163
}
161164
.launchIn(CoroutineScope(Dispatchers.IO))
@@ -167,7 +170,7 @@ internal class TTSViewModel(
167170
.map { it.tokenLocator ?: it.utteranceLocator }
168171
.distinctUntilChanged()
169172
.onEach { locator ->
170-
this@TTSViewModel.reader.justGoToLocator(locator, animated = true)
173+
currentReadiumReaderView?.justGoToLocator(locator, animated = true)
171174
}
172175
.launchIn(CoroutineScope(Dispatchers.Main))
173176
.let { jobs.add(it) }

flutter_readium/example/lib/state/player_controls_bloc.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class PlayerControlsBloc extends Bloc<PlayerControlsEvent, PlayerControlsState>
117117
// Change to first voice matching "da-DK" language.
118118
final daVoice = voices.firstWhereOrNull((l) => l.language == "da-DK");
119119
if (daVoice != null) {
120-
await instance.ttsSetVoice(daVoice.identifier);
120+
await instance.ttsSetVoice(daVoice.identifier, null);
121121
}
122122
});
123123
}

flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,45 @@ public class FlutterReadiumPlugin: NSObject, FlutterPlugin, ReadiumShared.Warnin
120120
}
121121
}
122122
}
123+
case "getLinkContent":
124+
let args = call.arguments as! [Any?]
125+
//let asString = args[2] as? Bool ?? true
126+
let asString = true
127+
guard let pubId = args[0] as? String,
128+
let linkStr = args[1] as? String,
129+
let publication = getPublicationByIdentifier(pubId),
130+
let link = try? Link(fromJsonString: linkStr) else {
131+
return result(FlutterError.init(
132+
code: "getLinkContent",
133+
message: "Failed to get link content",
134+
details: nil))
135+
}
136+
Task.detached(priority: .background) {
137+
let resource = publication.get(link)
138+
do {
139+
if (asString) {
140+
let linkContent = try await resource?.readAsString(encoding: .utf8).get()
141+
await MainActor.run {
142+
result(linkContent)
143+
}
144+
} else {
145+
let data = try await resource!.read().get()
146+
await MainActor.run {
147+
result(FlutterStandardTypedData(bytes: data))
148+
}
149+
}
150+
} catch let err {
151+
await MainActor.run {
152+
print("\(TAG).getLinkContent exception: \(err)")
153+
result(
154+
FlutterError.init(
155+
code: "getLinkContent",
156+
message: err.localizedDescription,
157+
details: "Something went wrong fetching link content."))
158+
}
159+
}
160+
}
161+
123162
case "ttsEnable":
124163
Task.detached(priority: .high) {
125164
do {
@@ -176,7 +215,9 @@ public class FlutterReadiumPlugin: NSObject, FlutterPlugin, ReadiumShared.Warnin
176215
let availableVocies = self.ttsGetAvailableVoices()
177216
result(availableVocies.map { $0.jsonString } )
178217
case "ttsSetVoice":
179-
let voiceIdentifier = call.arguments as! String
218+
let args = call.arguments as! [Any?]
219+
let voiceIdentifier = args[0] as! String
220+
// TODO: language might be supplied as args[1], ignored on iOS for now.
180221
do {
181222
try self.ttsSetVoice(voiceIdentifier: voiceIdentifier)
182223
result(nil)

flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumExtensions.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ func clamp<T>(_ value: T, minValue: T, maxValue: T) -> T where T : Comparable {
88
return min(max(value, minValue), maxValue)
99
}
1010

11-
extension Resource {
12-
var propertiesSync: ResourceProperties {
13-
let semaphore = DispatchSemaphore(value: 0)
14-
var props: ResourceProperties? = nil
15-
Task {
16-
props = await properties().getOrNil()
17-
semaphore.signal()
11+
extension Link {
12+
init(fromJsonString jsonString: String) throws {
13+
do {
14+
let jsonObj = try JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!)
15+
try self.init(json: jsonObj)
16+
} catch {
17+
print("Invalid Link object: \(error)")
18+
throw JSONError.parsing(Self.self)
1819
}
19-
semaphore.wait()
20-
return props ?? ResourceProperties()
20+
2121
}
2222
}
2323

flutter_readium/test/flutter_readium_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class MockFlutterReadiumPlatform with MockPlatformInterfaceMixin implements Flut
110110
}
111111

112112
@override
113-
Future<void> ttsSetVoice(String voiceIdentifier) {
113+
Future<void> ttsSetVoice(String voiceIdentifier, String? forLanguage) {
114114
// TODO: implement ttsSetVoice
115115
throw UnimplementedError();
116116
}

flutter_readium_platform_interface/lib/flutter_readium_platform_interface.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ abstract class FlutterReadiumPlatform extends PlatformInterface {
7272
Future<void> ttsPrevious() => throw UnimplementedError('ttsPrevious() has not been implemented');
7373
Future<List<ReaderTTSVoice>> ttsGetAvailableVoices() =>
7474
throw UnimplementedError('ttsGetAvailableVoices() has not been implemented');
75-
Future<void> ttsSetVoice(String voiceIdentifier) =>
75+
Future<void> ttsSetVoice(String voiceIdentifier, String? forLanguage) =>
7676
throw UnimplementedError('ttsSetVoice() has not been implemented');
7777
Future<void> ttsSetDecorationStyle(
7878
ReaderDecorationStyle? utteranceDecoration,

flutter_readium_platform_interface/lib/method_channel_flutter_readium.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,15 @@ class MethodChannelFlutterReadium extends FlutterReadiumPlatform {
122122
}
123123

124124
@override
125-
Future<void> ttsSetVoice(String voiceIdentifier) async {
126-
await methodChannel.invokeMethod('ttsSetVoice', voiceIdentifier);
125+
Future<void> ttsSetVoice(String voiceIdentifier, String? forLanguage) async {
126+
await methodChannel.invokeMethod('ttsSetVoice', [voiceIdentifier, forLanguage]);
127127
}
128128

129129
@override
130130
Future<void> ttsSetPreferences(TTSPreferences preferences) =>
131131
methodChannel.invokeMethod('ttsSetPreferences', preferences.toMap());
132+
133+
@override
134+
Future<String?> getLinkContent(final String pubIdentifier, final Link link) =>
135+
methodChannel.invokeMethod<String>('getLinkContent', [pubIdentifier, jsonEncode(link.toJson())]);
132136
}

0 commit comments

Comments
 (0)