Skip to content

Commit

Permalink
add option search by image from gallery or camera
Browse files Browse the repository at this point in the history
  • Loading branch information
3nws committed May 28, 2024
1 parent 631f347 commit fb42739
Show file tree
Hide file tree
Showing 14 changed files with 520 additions and 233 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release

/assets/tessdata
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ Japanese-English dictionary app powered by Jisho.org.
<a href="https://www.gnu.org/licenses/gpl-3.0"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-red.svg?style=flat-square"></a>
</div>

## Screenshots

[<img src="metadata/en-US/images/phoneScreenshots/01.png" width="160">](metadata/en-US/images/phoneScreenshots/01.png)
[<img src="metadata/en-US/images/phoneScreenshots/02.png" width="160">](metadata/en-US/images/phoneScreenshots/02.png)
[<img src="metadata/en-US/images/phoneScreenshots/03.png" width="160">](metadata/en-US/images/phoneScreenshots/03.png)
[<img src="metadata/en-US/images/phoneScreenshots/04.png" width="160">](metadata/en-US/images/phoneScreenshots/04.png)
[<img src="metadata/en-US/images/phoneScreenshots/05.png" width="160">](metadata/en-US/images/phoneScreenshots/05.png)
[<img src="metadata/en-US/images/phoneScreenshots/06.png" width="160">](metadata/en-US/images/phoneScreenshots/06.png)
[<img src="metadata/en-US/images/phoneScreenshots/07.png" width="160">](metadata/en-US/images/phoneScreenshots/07.png)
## OCR

Download and place the necessary models in `assets/tessdata/`, and build using the `ocr` flavor.

Files:
- https://github.com/tesseract-ocr/tessdata/blob/main/jpn.traineddata
- https://github.com/tesseract-ocr/tessdata/blob/main/jpn_vert.traineddata

## License

Expand Down
17 changes: 16 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ android {
applicationId "io.github.petlyh.jsdict"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 19
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand All @@ -58,6 +58,21 @@ android {
signingConfig signingConfigs.debug
}
}


flavorDimensions "default"
productFlavors {
base {
dimension "default"
resValue "string", "app_name", "Base flavor"
applicationIdSuffix ".base"
}
ocr {
dimension "default"
resValue "string", "app_name", "OCR flavor"
applicationIdSuffix ".ocr"
}
}
}

flutter {
Expand Down
5 changes: 4 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
xmlns:tools="http://schemas.android.com/tools"
package="io.github.petlyh.jsdict">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.CAMERA" />

<application
tools:replace="android:label"
Expand Down
6 changes: 6 additions & 0 deletions assets/tessdata_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"files": [
"jpn.traineddata",
"jpn_vert.traineddata"
]
}
1 change: 1 addition & 0 deletions lib/providers/query_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class QueryProvider extends ChangeNotifier {

void addToHistoryAndSearch(String text) {
_query = text;
searchController.text = text;
_preferences.setStringList(
_historyKey,
history
Expand Down
1 change: 0 additions & 1 deletion lib/screens/kanji_details/stroke_order.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import "package:collection/collection.dart";
import "package:expansion_tile_card/expansion_tile_card.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter/widgets.dart";
import "package:flutter_svg/flutter_svg.dart";
import "package:jsdict/packages/kanji_diagram/kanji_diagram.dart";
import "package:jsdict/providers/theme_provider.dart";
Expand Down
127 changes: 107 additions & 20 deletions lib/screens/search/search_screen.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_tesseract_ocr/android_ios.dart";
import "package:image_picker/image_picker.dart";
import "package:jsdict/jp_text.dart";
import "package:jsdict/models/models.dart";
import "package:jsdict/packages/link_handler.dart";
Expand All @@ -13,6 +16,7 @@ import "package:jsdict/screens/search_options/history_selection_screen.dart";
import "package:jsdict/screens/search_options/radical_search_screen.dart";
import "package:jsdict/screens/search_options/tag_selection_screen.dart";
import "package:jsdict/screens/settings_screen.dart";
import "package:jsdict/widgets/items/extracted_text_item.dart";
import "package:provider/provider.dart";

class SearchScreen extends StatefulWidget {
Expand All @@ -31,6 +35,7 @@ class _SearchScreenState extends State<SearchScreen>
late LinkHandler _linkHandler;
late ShareIntentHandler _shareIntentHandler;
late FocusNode _searchFocusNode;
final ImagePicker picker = ImagePicker();

@override
void initState() {
Expand All @@ -51,34 +56,116 @@ class _SearchScreenState extends State<SearchScreen>
super.dispose();
}

void processImage(BuildContext context, XFile? image) async {
if (image == null) return;
final List<String> models = ["jpn_vert", "jpn"];
final List<ExtractedTextItem> items = [];
for (final model in models) {
final String text = await FlutterTesseractOcr.extractText(
image.path,
language: model,
);
if (text.isNotEmpty) {
items.add(ExtractedTextItem(text: text));
}
}
if (items.isEmpty && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("No matches."),
duration: Duration(seconds: 1),
),
);
return;
}
if (!context.mounted) return;
showModalBottomSheet(
context: context,
showDragHandle: true,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
expand: false,
builder: (context, scrollController) => Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.separated(
itemCount: items.length,
controller: scrollController,
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) {
return const Divider();
},
),
),
),
);
}

@override
Widget build(BuildContext context) {
final queryProvider = QueryProvider.of(context);
final searchController = queryProvider.searchController;
final canvasProvider = CanvasProvider.of(context);

final List<Widget> ocrFlavorWidgets = appFlavor == "ocr"
? [
FloatingActionButton(
onPressed: () async {
final XFile? image = await picker.pickImage(
source: ImageSource.gallery, imageQuality: 100);
if (!context.mounted) return;
processImage(context, image);
},
tooltip: "Image Search",
heroTag: "imagesearch",
child: const Icon(Icons.image),
),
const SizedBox(
height: 10,
),
FloatingActionButton(
onPressed: () async {
final XFile? image = await picker.pickImage(
source: ImageSource.camera, imageQuality: 100);
if (!context.mounted) return;
processImage(context, image);
},
tooltip: "Camera Search",
heroTag: "camerasearch",
child: const Icon(Icons.camera_alt),
),
const SizedBox(
height: 10,
)
]
: [];

return Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: pushScreen(context, const RadicalSearchScreen()),
tooltip: "Radicals",
heroTag: "radicals",
child: const Text("部", style: TextStyle(fontSize: 20)),
),
const SizedBox(
width: 10,
),
FloatingActionButton(
onPressed: pushScreen(
context, HandwritingSearchScreen(back: canvasProvider.back)),
tooltip: "Handwriting",
heroTag: "handwriting",
child: const Icon(Icons.draw_outlined),
),
],
floatingActionButton: Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
...ocrFlavorWidgets,
FloatingActionButton(
onPressed: pushScreen(
context, HandwritingSearchScreen(back: canvasProvider.back)),
tooltip: "Handwriting",
heroTag: "handwriting",
child: const Icon(Icons.draw_outlined),
),
const SizedBox(
height: 10,
),
FloatingActionButton(
onPressed: pushScreen(context, const RadicalSearchScreen()),
tooltip: "Radicals",
heroTag: "radicals",
child: const Text("部", style: TextStyle(fontSize: 20)),
),
],
),
),
appBar: AppBar(
title: TextField(
Expand Down
2 changes: 1 addition & 1 deletion lib/screens/search_options/handwriting_search_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class _KanjiSelection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textColor = Theme.of(context).textTheme.bodyLarge!.color;
final backgroundColor = Theme.of(context).colorScheme.surfaceVariant;
final backgroundColor = Theme.of(context).colorScheme.surfaceContainerHighest;

return Consumer<CanvasProvider>(
builder: (_, provider, __) => provider.matchingKanji.isEmpty
Expand Down
4 changes: 2 additions & 2 deletions lib/screens/search_options/radical_search_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class _RadicalSelection extends StatelessWidget {
Widget build(BuildContext context) {
final strokeIndicatorColor = Theme.of(context).highlightColor;
final textColor = Theme.of(context).textTheme.bodyLarge!.color;
final selectedColor = Theme.of(context).colorScheme.surfaceVariant;
final selectedColor = Theme.of(context).colorScheme.surfaceContainerHighest;
final disabledColor = Theme.of(context).focusColor;

return SingleChildScrollView(
Expand Down Expand Up @@ -158,7 +158,7 @@ class _KanjiSelection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textColor = Theme.of(context).textTheme.bodyLarge!.color;
final backgroundColor = Theme.of(context).colorScheme.surfaceVariant;
final backgroundColor = Theme.of(context).colorScheme.surfaceContainerHighest;

return matchingKanji.isEmpty
? const Center(child: Text("Select radicals"))
Expand Down
Loading

0 comments on commit fb42739

Please sign in to comment.