Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,42 @@ public class FlutterReadiumPlugin: NSObject, FlutterPlugin, ReadiumShared.Warnin
let _ = await self.timebasedNavigator?.seekRelative(byOffsetSeconds: seekOffset)
result(nil)
}
case "searchInPublication":
guard let publication = getCurrentPublication(),
let query = call.arguments as? String
else {
result(
FlutterError(
code: "InvalidArgument",
message: "No publication open or invalid parameters to searchInPublication",
details: nil))
return
}
Task {
do {
let searchResults = await publication.searchInContentForQuery(query)
switch searchResults {
case .failure(let err):
throw err
case .success(let searchResultsCols):
let fallbackTitle = searchResultsCols.first?.metadata.title ?? publication.metadata.title ?? "Unknown chapter"
// TODO: Should we try to find physical page-numbers for the results?
let results = searchResultsCols.flatMap { $0.locators.map { l in TextSearchResult(locator: l, chapterTitle: l.title ?? fallbackTitle, pageNumbers: nil) } }
let searchResultsJson = try results.map { try $0.toJsonString() }
await MainActor.run {
result(searchResultsJson)
}
}
} catch {
await MainActor.run {
result(
FlutterError(
code: "SearchError",
message: "Failed to perform search with query: \(query)",
details: error.localizedDescription))
}
}
}

default:
result(FlutterMethodNotImplemented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele
private let _view: UIView
private let readiumViewController: EPUBNavigatorViewController
private var hasSentReady = false
private let publication: Publication

var publicationIdentifier: String?

Expand All @@ -63,6 +64,7 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele
let creationParams = args as! Dictionary<String, Any?>

let publication = FlutterReadiumPlugin.instance!.getCurrentPublication()!
self.publication = publication

let preferencesMap = creationParams["preferences"] as? Dictionary<String, String>?
let defaultPreferences = preferencesMap == nil ? nil : EPUBPreferences.init(fromMap: preferencesMap!!)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ReadiumShared

struct TextSearchResult {
let locator: Locator
let chapterTitle: String?
let pageNumbers: [String]?

public init(locator: Locator, chapterTitle: String? = nil, pageNumbers: [String]? = nil) {
self.locator = locator
self.chapterTitle = chapterTitle
self.pageNumbers = pageNumbers
}

func toJson() -> [String: Any?] {
let map: [String: Any?] = [
"locator": locator.jsonString,
"chapterTitle": chapterTitle,
"pageNumbers": pageNumbers?.joined(separator: ","),
]

return map
}

func toJsonString(pretty: Bool = false) throws -> String? {
let options: JSONSerialization.WritingOptions = pretty ? [.prettyPrinted] : []
let data = try JSONSerialization.data(withJSONObject: toJson(), options: options)
return String(data: data, encoding: .utf8)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,28 +74,22 @@ extension Publication {
}
}

func searchInContentForQuery(_ query: String) async -> [LocatorCollection] {
func searchInContentForQuery(_ query: String) async -> Result<[LocatorCollection]> {
guard let searchService: SearchService = findService(SearchService.self) else {
Log.readium.warn("No SearchService available")
return []
return Result.failure(SearchError.publicationNotSearchable)
}
var collections: [LocatorCollection] = []
switch await searchService.search(query: query, options: .init()) {
case .failure(let err):
switch err {
case .badQuery(let queryErr):
Log.readium.error("Search failed, bad query: \(queryErr)")
case .reading(let readErr):
Log.readium.error("Search failed, reading error: \(readErr)")
case .publicationNotSearchable:
Log.readium.error("Search failed, publication is not searchable")
}
Log.readium.error("Search in publication content failed: \(err)")
return Result.failure(err)
case .success(let iterator):
_ = await iterator.forEach { collection in
collections.append(collection)
}
}
return collections
return .success(collections)
}

/**
Expand Down
3 changes: 3 additions & 0 deletions flutter_readium/lib/flutter_readium.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,7 @@ class FlutterReadium {

return goByLink(pageLink, pub);
}

Future<List<TextSearchResult>> searchInPublication(String searchKey) async =>
_platform.searchInPublication(searchKey);
}
6 changes: 6 additions & 0 deletions flutter_readium/test/flutter_readium_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ class MockFlutterReadiumPlatform with MockPlatformInterfaceMixin implements Flut
// TODO: implement audioSeekBy
throw UnimplementedError();
}

@override
Future<List<TextSearchResult>> searchInPublication(String searchKey) {
// TODO: implement searchInPublication
throw UnimplementedError();
}
}

void main() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ abstract class FlutterReadiumPlatform extends PlatformInterface {
Future<void> audioSeekBy(Duration offset) => throw UnimplementedError('seekInAudio() has not been implemented');
// AUDIOBOOK API - END

Future<List<TextSearchResult>> searchInPublication(final String searchKey) {
throw UnimplementedError('searchInPublication() has not been implemented');
}

// State stream for reader status changes
Stream<ReadiumReaderStatus> get onReaderStatusChanged {
throw UnimplementedError('onReaderStatus stream has not been implemented.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,28 @@ class MethodChannelFlutterReadium extends FlutterReadiumPlatform {

@override
Future<void> audioSeekBy(Duration offset) => methodChannel.invokeMethod('audioSeekBy', offset.inSeconds);

@override
Future<List<TextSearchResult>> searchInPublication(String searchKey) async {
final resultString = await methodChannel.invokeMethod<String>('searchInPublication', searchKey);

if (resultString == null || resultString.isEmpty) {
return <TextSearchResult>[];
}

try {
final decoded = json.decode(resultString);
if (decoded is List) {
final results = decoded
.map((e) => TextSearchResult.fromJson(e as Map<String, dynamic>?))
.whereType<TextSearchResult>()
.toList();

return results;
}
return <TextSearchResult>[];
} catch (e) {
throw Exception('Failed to parse search results: $e');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'text_search_result.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// NOTE: This is a Nota type
import 'package:equatable/equatable.dart';

import '../../../utils/jsonable.dart';
import '../index.dart';

class TextSearchResult with EquatableMixin implements JSONable {
const TextSearchResult({required this.locator, this.chapterTitle, this.pageNumbers});

final Locator locator;
final String? chapterTitle;
final List<String>? pageNumbers;

factory TextSearchResult.fromJson(Map<String, dynamic>? json) {
if (json == null) {
throw ArgumentError('json cannot be null');
}

final jsonObject = Map<String, dynamic>.of(json);

return TextSearchResult(
locator: Locator.fromJson(jsonObject['locator'] as Map<String, dynamic>)!,
chapterTitle: jsonObject['chapterTitle'] as String?,
pageNumbers: (jsonObject['pageNumbers'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
}

@override
Map<String, dynamic> toJson() => <String, dynamic>{}
..put('locator', locator.toJson())
..putOpt('chapterTitle', chapterTitle)
..putIterableIfNotEmpty('pageNumbers', pageNumbers);

@override
List<Object?> get props => [locator, chapterTitle, pageNumbers];

@override
String toString() =>
'TextSearchResult{locator: $locator, chapterTitle: $chapterTitle, '
'pageNumbers: $pageNumbers}';
}

extension TextSearchResultExtension on TextSearchResult {
LocatorText? get text => locator.text;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'drm.dart';
export 'dto/index.dart';
export 'encryption.dart';
export 'epub/index.dart';
export 'format.dart';
Expand All @@ -7,6 +8,7 @@ export 'link.dart';
export 'link_list_extension.dart';
export 'localized_string.dart';
export 'locator.dart';
export 'locator_collection.dart';
export 'metadata/index.dart';
export 'opds/index.dart';
export 'positions_list.dart';
Expand Down
Loading