diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 29b9d40b6..960778f6b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -42,6 +42,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/io/flutter/plugins/com/lrorpilla/jidoujisho/MainActivity.java b/android/app/src/main/java/io/flutter/plugins/com/lrorpilla/jidoujisho/MainActivity.java
index 065fc89b8..41052b64e 100644
--- a/android/app/src/main/java/io/flutter/plugins/com/lrorpilla/jidoujisho/MainActivity.java
+++ b/android/app/src/main/java/io/flutter/plugins/com/lrorpilla/jidoujisho/MainActivity.java
@@ -59,6 +59,64 @@ protected void onCreate(Bundle savedInstanceState) {
mAnkiDroid = new AnkiDroidHelper(context);
}
+ private void addCreatorNote(String deck, String image, String audio, String sentence, String word, String meaning, String reading) {
+ final AddContentApi api = new AddContentApi(context);
+
+ long deckId;
+ if (deckExists(deck)) {
+ deckId = mAnkiDroid.findDeckIdByName(deck);
+ } else {
+ deckId = api.addNewDeck(deck);
+ }
+
+ long modelId;
+ if (modelExists("jidoujisho (Creator)")) {
+ modelId = mAnkiDroid.findModelIdByName("jidoujisho (Creator)", 6);
+ } else {
+ modelId = api.addNewCustomModel("jidoujisho (Creator)",
+ new String[] {"Image", "Audio", "Sentence", "Word", "Meaning", "Reading"},
+ new String[] {"jidoujisho (Creator) Default"},
+ new String[] {"{{Image}}
{{Word}}"},
+ new String[] {"{{Image}}
{{Word}}" +
+ "
{{Reading}}
{{Word}}
{{Meaning}}
"},
+ "p {\n" +
+ " margin: 0px\n" +
+ "}\n" +
+ "\n" +
+ "h2 {\n" +
+ " margin: 0px\n" +
+ "}\n" +
+ "\n" +
+ "small {\n" +
+ " margin: 0px\n" +
+ "}\n" +
+ "\n" +
+ ".card {\n" +
+ " font-family: arial;\n" +
+ " font-size: 20px;\n" +
+ " white-space: pre-line;\n" +
+ " text-align: center;\n" +
+ " color: black;\n" +
+ " background-color: white;\n" +
+ "}\n" +
+ "\n" +
+ "#sentence {\n" +
+ " font-size: 30px\n" +
+ "}",
+ null,
+ null
+ );
+ }
+
+ Set tags = new HashSet<>(Arrays.asList("jidoujisho"));
+
+ api.addNote(modelId, deckId, new String[] {image, audio, sentence, word, meaning, reading}, tags);
+
+ System.out.println("Added note via flutter_ankidroid_api");
+ System.out.println("Model: " + modelId);
+ System.out.println("Deck: " + deckId);
+ }
+
private void addNote(String deck, String image, String audio, String sentence, String word, String meaning, String reading) {
final AddContentApi api = new AddContentApi(context);
@@ -156,22 +214,27 @@ private Long getModelId() {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
+
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), ANKIDROID_CHANNEL)
.setMethodCallHandler(
(call, result) -> {
+ final String deck = call.argument("deck");
+ final String image = call.argument("image");
+ final String audio = call.argument("audio");
+ final String sentence = call.argument("sentence");
+ final String answer = call.argument("answer");
+ final String meaning = call.argument("meaning");
+ final String reading = call.argument("reading");
+
switch (call.method) {
case "addNote":
- final String deck = call.argument("deck");
- final String image = call.argument("image");
- final String audio = call.argument("audio");
- final String sentence = call.argument("sentence");
- final String answer = call.argument("answer");
- final String meaning = call.argument("meaning");
- final String reading = call.argument("reading");
-
addNote(deck, image, audio, sentence, answer, meaning, reading);
break;
+ case "addCreatorNote":
+
+ addCreatorNote(deck, image, audio, sentence, answer, meaning, reading);
+ break;
case "getDecks":
final AddContentApi api = new AddContentApi(context);
result.success(api.getDeckList());
diff --git a/chewie/lib/src/chewie_player.dart b/chewie/lib/src/chewie_player.dart
index 1f6dc13d2..9355793e0 100755
--- a/chewie/lib/src/chewie_player.dart
+++ b/chewie/lib/src/chewie_player.dart
@@ -222,6 +222,8 @@ class ChewieController extends ChangeNotifier {
@required this.playExternalSubtitles,
@required this.retimeSubtitles,
@required this.exportSingleCallback,
+ @required this.toggleShadowingMode,
+ @required this.shadowingSubtitle,
this.aspectRatio,
this.autoInitialize = false,
this.autoPlay = false,
@@ -266,6 +268,8 @@ class ChewieController extends ChangeNotifier {
final VoidCallback playExternalSubtitles;
final VoidCallback retimeSubtitles;
final VoidCallback exportSingleCallback;
+ final VoidCallback toggleShadowingMode;
+ final ValueNotifier shadowingSubtitle;
final YouTubeMux streamData;
/// Initialize the Video on Startup. This will prep the video for playback.
diff --git a/chewie/lib/src/material_controls.dart b/chewie/lib/src/material_controls.dart
index 34fdfa10c..42b05d5e9 100755
--- a/chewie/lib/src/material_controls.dart
+++ b/chewie/lib/src/material_controls.dart
@@ -549,19 +549,42 @@ class _MaterialControlsState extends State
? _latestValue.duration
: Duration.zero;
- return GestureDetector(
- child: Padding(
- padding: const EdgeInsets.only(right: 24.0),
- child: Text(
- duration != Duration.zero
- ? '${formatDuration(position)} / ${formatDuration(duration)}'
- : '',
- style: const TextStyle(
- fontSize: 14.0,
+ if (chewieController.shadowingSubtitle.value != null) {
+ return GestureDetector(
+ onTap: () {
+ chewieController.toggleShadowingMode();
+ },
+ child: Padding(
+ padding: const EdgeInsets.only(right: 24.0),
+ child: Text(
+ duration != Duration.zero
+ ? '${formatDuration(position)} / ${formatDuration(chewieController.shadowingSubtitle.value.endTime)}'
+ : '',
+ style: const TextStyle(
+ fontSize: 14.0,
+ color: Colors.red,
+ ),
),
),
- ),
- );
+ );
+ } else {
+ return GestureDetector(
+ onTap: () {
+ chewieController.toggleShadowingMode();
+ },
+ child: Padding(
+ padding: const EdgeInsets.only(right: 24.0),
+ child: Text(
+ duration != Duration.zero
+ ? '${formatDuration(position)} / ${formatDuration(duration)}'
+ : '',
+ style: const TextStyle(
+ fontSize: 14.0,
+ ),
+ ),
+ ),
+ );
+ }
}
void _cancelAndRestartTimer() {
diff --git a/lib/anki.dart b/lib/anki.dart
index 74c57fc22..caef9b99b 100644
--- a/lib/anki.dart
+++ b/lib/anki.dart
@@ -164,7 +164,7 @@ Future exportToAnki(
int audioAllowance,
int subtitleDelay,
) async {
- String lastDeck = gSharedPrefs.getString("lastDeck") ?? "Default";
+ String lastDeck = getLastDeck();
List decks;
try {
@@ -247,9 +247,9 @@ void showAnkiDialog(
decoration: InputDecoration(
prefixIcon: Icon(icon),
suffixIcon: IconButton(
- iconSize: 12,
+ iconSize: 18,
onPressed: () => controller.clear(),
- icon: Icon(Icons.clear),
+ icon: Icon(Icons.clear, color: Colors.white),
),
labelText: labelText,
hintText: hintText,
@@ -521,6 +521,33 @@ Future addNote(
}
}
+Future addCreatorNote(
+ String deck,
+ String image,
+ String audio,
+ String sentence,
+ String answer,
+ String meaning,
+ String reading,
+) async {
+ const platform = const MethodChannel('com.lrorpilla.api/ankidroid');
+
+ try {
+ await platform.invokeMethod('addCreatorNote', {
+ 'deck': deck,
+ 'image': image,
+ 'audio': audio,
+ 'sentence': sentence,
+ 'answer': answer,
+ 'meaning': meaning,
+ 'reading': reading,
+ });
+ } on PlatformException catch (e) {
+ print("Failed to add note via AnkiDroid API");
+ print(e);
+ }
+}
+
Future> getDecks() async {
const platform = const MethodChannel('com.lrorpilla.api/ankidroid');
Map deckMap = await platform.invokeMethod('getDecks');
@@ -557,7 +584,7 @@ class _DeckDropDownState extends State {
);
}).toList(),
onChanged: (selectedDeck) async {
- gSharedPrefs.setString("lastDeck", selectedDeck);
+ setLastDeck(selectedDeck);
setState(() {
_selectedDeck.value = selectedDeck;
@@ -606,3 +633,26 @@ void exportAnkiCard(String deck, String sentence, String answer, String reading,
requestAnkiDroidPermissions();
addNote(deck, addImage, addAudio, sentence, answer, meaning, reading);
}
+
+void exportCreatorAnkiCard(String deck, String sentence, String answer,
+ String reading, String meaning, File imageFile) {
+ DateTime now = DateTime.now();
+ String newFileName =
+ "jidoujisho-" + intl.DateFormat('yyyyMMddTkkmmss').format(now);
+
+ String newImagePath = path.join(
+ getAnkiDroidDirectory().path,
+ "collection.media/$newFileName.jpg",
+ );
+
+ String addImage = "";
+ String addAudio = "";
+
+ if (imageFile.existsSync()) {
+ imageFile.copySync(newImagePath);
+ addImage = "
";
+ }
+
+ requestAnkiDroidPermissions();
+ addCreatorNote(deck, addImage, addAudio, sentence, answer, meaning, reading);
+}
diff --git a/lib/main.dart b/lib/main.dart
index bdebf8797..1a9e3b363 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,16 +1,21 @@
+import 'dart:async';
import 'dart:io';
import 'package:async/async.dart';
import 'package:audio_service/audio_service.dart';
+import 'package:external_app_launcher/external_app_launcher.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fuzzy/fuzzy.dart';
-
+import 'package:image_picker/image_picker.dart';
+import 'package:http/http.dart' as http;
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:mecab_dart/mecab_dart.dart';
import 'package:package_info/package_info.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
+import 'package:photo_view/photo_view.dart';
+import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:transparent_image/transparent_image.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -26,6 +31,7 @@ import 'package:jidoujisho/preferences.dart';
import 'package:jidoujisho/util.dart';
typedef void ChannelCallback(String id, String name, bool isReversed);
+typedef void CreatorCallback(DictionaryEntry entry, File file);
typedef void SearchCallback(String term);
void main() async {
@@ -142,11 +148,23 @@ class Home extends StatefulWidget {
}
class _HomeState extends State {
+ StreamSubscription _intentDataStreamSubscription;
+ List _sharedFiles;
+ String _sharedText;
+
TextEditingController _searchQueryController = TextEditingController();
bool _isSearching = false;
bool _isChannelView = false;
+ bool _isCreatorView = false;
bool _isOldest = false;
+ DictionaryEntry _creatorDictionaryEntry = DictionaryEntry(
+ word: "",
+ reading: "",
+ meaning: "",
+ );
+ File _creatorFile;
+
String _searchQuery = "";
int _selectedIndex = 0;
String _selectedChannelName = "";
@@ -154,6 +172,84 @@ class _HomeState extends State {
ValueNotifier>([]);
YoutubeExplode yt = YoutubeExplode();
+ @override
+ void initState() {
+ super.initState();
+
+ _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream()
+ .listen((List value) {
+ if (value == null) {
+ return;
+ }
+
+ setCreatorView(DictionaryEntry(word: "", meaning: "", reading: ""),
+ File(value.first.path));
+ }, onError: (err) {
+ print("getIntentDataStream error: $err");
+ });
+
+ // For sharing images coming from outside the app while the app is closed
+ ReceiveSharingIntent.getInitialMedia().then((List value) {
+ if (value == null) {
+ return;
+ }
+
+ setCreatorView(DictionaryEntry(word: "", meaning: "", reading: ""),
+ File(value.first.path));
+ });
+
+ // For sharing or opening urls/text coming from outside the app while the app is in the memory
+ _intentDataStreamSubscription =
+ ReceiveSharingIntent.getTextStream().listen((String value) {
+ if (value == null) {
+ return;
+ }
+ if (value.startsWith("https://")) {
+ playYouTubeVideoLink(value);
+ } else {
+ setCreatorView(
+ DictionaryEntry(word: value, meaning: "", reading: ""), null);
+ }
+ }, onError: (err) {
+ print("getLinkStream error: $err");
+ });
+
+ // For sharing or opening urls/text coming from outside the app while the app is closed
+ ReceiveSharingIntent.getInitialText().then((String value) {
+ if (value == null) {
+ return;
+ }
+
+ if (value.startsWith("https://")) {
+ playYouTubeVideoLink(value);
+ } else {
+ setCreatorView(
+ DictionaryEntry(word: value, meaning: "", reading: ""), null);
+ }
+ });
+ }
+
+ void playYouTubeVideoLink(String link) {
+ if (YoutubePlayer.convertUrlToId(link) != null) {
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(
+ builder: (context) => Player(
+ url: link,
+ ),
+ ),
+ ).then((returnValue) {
+ setState(() {
+ unlockLandscape();
+ });
+
+ setLastPlayedPath(link);
+ setLastPlayedPosition(0);
+ gIsResumable.value = getResumeAvailable();
+ });
+ }
+ }
+
void setStateFromResult() {
setState(() {});
}
@@ -171,9 +267,10 @@ class _HomeState extends State {
});
} else {
_selectedIndex = index;
- if (_isSearching || _isChannelView) {
+ if (_isSearching || _isChannelView || _isCreatorView) {
_isSearching = false;
_isChannelView = false;
+ _isCreatorView = false;
_searchQuery = "";
}
}
@@ -185,6 +282,12 @@ class _HomeState extends State {
return buildBody();
} else if (_isChannelView) {
return buildChannels();
+ } else if (_isCreatorView) {
+ return Creator(
+ "",
+ _creatorDictionaryEntry,
+ _creatorFile,
+ );
}
switch (getNavigationBarItems()[index].label) {
@@ -195,7 +298,7 @@ class _HomeState extends State {
case "History":
return History();
case "Clipboard":
- return ClipboardMenu();
+ return ClipboardMenu(setCreatorView);
default:
return Container();
}
@@ -266,18 +369,17 @@ class _HomeState extends State {
onTap: onItemTapped,
items: getNavigationBarItems(),
),
- body: Center(
- child: getWidgetOptions(_selectedIndex),
- ),
+ body: getWidgetOptions(_selectedIndex),
),
);
}
Future _onWillPop() async {
- if (_isSearching || _isChannelView) {
+ if (_isSearching || _isChannelView || _isCreatorView) {
setState(() {
_isSearching = false;
_isChannelView = false;
+ _isCreatorView = false;
_searchQuery = "";
_searchSuggestions.value = [];
_searchQueryController.clear();
@@ -289,12 +391,13 @@ class _HomeState extends State {
}
Widget buildAppBarLeading() {
- if (_isSearching || _isChannelView) {
+ if (_isSearching || _isChannelView || _isCreatorView) {
return BackButton(
onPressed: () {
setState(() {
_isSearching = false;
_isChannelView = false;
+ _isCreatorView = false;
_searchQuery = "";
_searchSuggestions.value = [];
_searchQueryController.clear();
@@ -338,6 +441,16 @@ class _HomeState extends State {
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
+ } else if (_isCreatorView) {
+ return Text(
+ "Card Creator",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ );
} else {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -388,8 +501,12 @@ class _HomeState extends State {
"Error getting channels",
Icons.error,
);
+ Widget emptyMessage = centerMessage(
+ "No channels listed",
+ Icons.subscriptions_sharp,
+ );
Widget videoMessage = centerMessage(
- _isOldest ? "Listing oldest videos..." : "Listing recent videos...",
+ _isOldest ? "Listing oldest videos..." : "Listing latest videos...",
Icons.subscriptions_sharp,
);
@@ -427,25 +544,33 @@ class _HomeState extends State {
if (!snapshot.hasData) {
return errorMessage;
}
- return ListView.builder(
- addAutomaticKeepAlives: true,
- itemCount: snapshot.data.length + 1,
- itemBuilder: (BuildContext context, int index) {
- if (index == 0) {
- return buildNewChannelRow();
- }
- Channel result = results[index - 1];
- print("CHANNEL LISTED: $result");
+ if (snapshot.data.isNotEmpty) {
+ return ListView.builder(
+ addAutomaticKeepAlives: true,
+ itemCount: snapshot.data.length + 1,
+ itemBuilder: (BuildContext context, int index) {
+ if (index == 0) {
+ return buildNewChannelRow();
+ }
+
+ Channel result = results[index - 1];
+ print("CHANNEL LISTED: $result");
- return ChannelResult(
- result,
- setChannelVideoSearch,
- setStateFromResult,
- index,
- );
- },
- );
+ return ChannelResult(
+ result,
+ setChannelVideoSearch,
+ setStateFromResult,
+ index,
+ );
+ },
+ );
+ } else {
+ return Column(children: [
+ buildNewChannelRow(),
+ Expanded(child: emptyMessage),
+ ]);
+ }
}
},
);
@@ -560,6 +685,76 @@ class _HomeState extends State {
});
}
+ Future setCreatorView(
+ DictionaryEntry dictionaryEntry,
+ File file,
+ ) async {
+ try {
+ await getDecks();
+ setState(() {
+ _isCreatorView = true;
+ _creatorDictionaryEntry = dictionaryEntry;
+ _creatorFile = file;
+ });
+ } catch (e) {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return AlertDialog(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.zero,
+ ),
+ content: Text(
+ "Failed to communicate with the AnkiDroid background service, "
+ "which is necessary to use the card creator. Please launch "
+ "AnkiDroid and try again.",
+ textAlign: TextAlign.justify,
+ ),
+ actions: [
+ new TextButton(
+ child: Text(
+ 'CANCEL',
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ style: TextButton.styleFrom(
+ textStyle: TextStyle(
+ color: Colors.white,
+ ),
+ ),
+ onPressed: () async {
+ Navigator.pop(context);
+ },
+ ),
+ new TextButton(
+ child: Text(
+ 'LAUNCH ANKIDROID',
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ style: TextButton.styleFrom(
+ textStyle: TextStyle(
+ color: Colors.white,
+ ),
+ ),
+ onPressed: () async {
+ await LaunchApp.openApp(
+ androidPackageName: 'com.ichi2.anki',
+ openStore: true,
+ );
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ );
+ });
+ }
+ }
+
Widget generateSuggestions() {
return ValueListenableBuilder(
valueListenable: _searchSuggestions,
@@ -2031,10 +2226,17 @@ class _HistoryState extends State {
}
class ClipboardMenu extends StatefulWidget {
- _ClipboardState createState() => _ClipboardState();
+ final CreatorCallback creatorCallback;
+
+ ClipboardMenu(this.creatorCallback);
+
+ _ClipboardState createState() => _ClipboardState(this.creatorCallback);
}
class _ClipboardState extends State {
+ final CreatorCallback creatorCallback;
+ _ClipboardState(this.creatorCallback);
+
@override
Widget build(BuildContext context) {
List entries = getDictionaryHistory().reversed.toList();
@@ -2067,15 +2269,59 @@ class _ClipboardState extends State {
Icons.paste_sharp,
);
+ Widget cardCreatorButton() {
+ return Container(
+ width: double.infinity,
+ margin: EdgeInsets.only(bottom: 12),
+ color: Colors.grey[800].withOpacity(0.2),
+ child: InkWell(
+ child: Padding(
+ padding: EdgeInsets.all(16),
+ child: InkWell(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.note_add_sharp, size: 16),
+ SizedBox(width: 5),
+ Text(
+ "Card Creator",
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ onTap: () async {
+ creatorCallback(
+ DictionaryEntry(
+ word: "",
+ reading: "",
+ meaning: "",
+ ),
+ null,
+ );
+ },
+ ),
+ );
+ }
+
if (entries.isEmpty) {
return emptyMessage;
}
return ListView.builder(
key: UniqueKey(),
- itemCount: entries.length,
+ itemCount: entries.length + 1,
itemBuilder: (BuildContext context, int index) {
- DictionaryEntry entry = entries[index];
+ if (index == 0) {
+ return cardCreatorButton();
+ }
+
+ DictionaryEntry entry = entries[index - 1];
print("ENTRY LISTED: $entry");
return Container(
@@ -2083,7 +2329,7 @@ class _ClipboardState extends State {
color: Colors.grey[800].withOpacity(0.2),
child: InkWell(
onTap: () {
- Clipboard.setData(new ClipboardData(text: entry.word));
+ creatorCallback(entry, null);
},
child: Padding(
padding: EdgeInsets.all(16),
@@ -2091,15 +2337,15 @@ class _ClipboardState extends State {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- SelectableText(
+ Text(
entry.word,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
- SelectableText(entry.reading),
- SelectableText("\n${entry.meaning}\n"),
+ Text(entry.reading),
+ Text("\n${entry.meaning}\n"),
],
),
),
@@ -2211,3 +2457,777 @@ class _LazyResultsState extends State {
);
}
}
+
+class Creator extends StatefulWidget {
+ final String initialSentence;
+ final DictionaryEntry initialDictionaryEntry;
+ final File initialFile;
+
+ Creator(
+ this.initialSentence,
+ this.initialDictionaryEntry,
+ this.initialFile,
+ );
+
+ _CreatorState createState() => _CreatorState(
+ this.initialSentence,
+ this.initialDictionaryEntry,
+ this.initialFile,
+ );
+}
+
+class _CreatorState extends State {
+ final String initialSentence;
+ final DictionaryEntry initialDictionaryEntry;
+ final File initialFile;
+
+ List decks;
+ List imageURLs;
+ String searchTerm;
+ TextEditingController _imageSearchController;
+
+ TextEditingController _sentenceController;
+ TextEditingController _wordController;
+ TextEditingController _readingController;
+ TextEditingController _meaningController;
+ ValueNotifier _selectedDeck;
+
+ String lastDeck = getLastDeck();
+
+ ValueNotifier _selectedEntry;
+ ValueNotifier _selectedIndex = ValueNotifier(0);
+ ValueNotifier _justExported = ValueNotifier(false);
+ bool _isFileImage = false;
+ File _fileImage;
+ String _networkImageURL;
+
+ _CreatorState(
+ this.initialSentence,
+ this.initialDictionaryEntry,
+ this.initialFile,
+ );
+
+ @override
+ initState() {
+ super.initState();
+ _imageSearchController = TextEditingController(text: searchTerm);
+ _sentenceController = TextEditingController(text: initialSentence);
+ _wordController = TextEditingController(text: initialDictionaryEntry.word);
+ _readingController =
+ TextEditingController(text: initialDictionaryEntry.reading);
+ _meaningController =
+ TextEditingController(text: initialDictionaryEntry.meaning);
+
+ _selectedEntry = new ValueNotifier(initialDictionaryEntry);
+ _selectedDeck = new ValueNotifier(lastDeck);
+
+ if (initialDictionaryEntry.word == "") {
+ _isFileImage = true;
+ }
+ if (initialFile != null) {
+ _isFileImage = true;
+ _fileImage = initialFile;
+ }
+
+ if (searchTerm == null) {
+ if (initialDictionaryEntry.word.contains(";")) {
+ searchTerm = initialDictionaryEntry.word.split(";").first;
+ } else if (initialDictionaryEntry.word.contains("/")) {
+ searchTerm = initialDictionaryEntry.word.split("/").first;
+ } else {
+ searchTerm = initialDictionaryEntry.word;
+ }
+ }
+ }
+
+ @override
+ build(BuildContext context) {
+ if (decks == null) {
+ return FutureBuilder(
+ future: getDecks(),
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ switch (snapshot.connectionState) {
+ case ConnectionState.waiting:
+ return buildWaitingMessage();
+ break;
+ default:
+ decks = snapshot.data;
+ return buildEditor();
+ }
+ },
+ );
+ } else {
+ return buildEditor();
+ }
+ }
+
+ Widget buildWaitingMessage() {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.note_add_sharp,
+ color: Colors.grey,
+ size: 72,
+ ),
+ const SizedBox(height: 6),
+ Text(
+ "Preparing card creator...",
+ style: TextStyle(
+ color: Colors.grey,
+ fontSize: 20,
+ ),
+ )
+ ],
+ ),
+ );
+ }
+
+ Widget buildSearchingMessage() {
+ return ListView(
+ shrinkWrap: true,
+ children: [
+ Container(
+ height: MediaQuery.of(context).size.height / 5,
+ ),
+ SizedBox(height: 13),
+ Text(
+ "Searching for image...",
+ style: TextStyle(
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ );
+ }
+
+ void showDictionaryDialog(List results) {
+ ValueNotifier _dialogIndex = ValueNotifier(0);
+ ValueNotifier _dialogEntry =
+ ValueNotifier(results[0]);
+
+ showDialog(
+ context: context,
+ builder: (context) {
+ return AlertDialog(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.zero,
+ ),
+ content: ValueListenableBuilder(
+ valueListenable: _dialogIndex,
+ builder: (BuildContext context, int _, Widget widget) {
+ _dialogEntry.value = results[_dialogIndex.value];
+ addDictionaryEntryToHistory(_dialogEntry.value);
+
+ return Container(
+ child: GestureDetector(
+ onHorizontalDragEnd: (details) {
+ if (details.primaryVelocity == 0) return;
+
+ if (details.primaryVelocity.compareTo(0) == -1) {
+ if (_dialogIndex.value == results.length - 1) {
+ _dialogIndex.value = 0;
+ } else {
+ _dialogIndex.value += 1;
+ }
+ } else {
+ if (_dialogIndex.value == 0) {
+ _dialogIndex.value = results.length - 1;
+ } else {
+ _dialogIndex.value -= 1;
+ }
+ }
+ },
+ child: Container(
+ color: Colors.grey[800].withOpacity(0.6),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ results[_dialogIndex.value].word,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 20,
+ ),
+ ),
+ Text(results[_dialogIndex.value].reading),
+ Flexible(
+ child: SingleChildScrollView(
+ child: gCustomDictionary.isNotEmpty ||
+ getMonolingualMode()
+ ? SelectableText(
+ "\n${results[_dialogIndex.value].meaning}\n")
+ : Text(
+ "\n${results[_dialogIndex.value].meaning}\n"),
+ ),
+ ),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "Showing search result ",
+ style: TextStyle(
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "${_dialogIndex.value + 1} ",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "out of ",
+ style: TextStyle(
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "${results.length} ",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "found for",
+ style: TextStyle(
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "『${results[_dialogIndex.value].searchTerm}』",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ )
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ actions: [
+ TextButton(
+ child: Text('CANCEL', style: TextStyle(color: Colors.white)),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ TextButton(
+ child: Text('SELECT', style: TextStyle(color: Colors.white)),
+ onPressed: () async {
+ _selectedEntry.value = _dialogEntry.value;
+ _wordController =
+ TextEditingController(text: _selectedEntry.value.word);
+ _readingController =
+ TextEditingController(text: _selectedEntry.value.reading);
+ _meaningController =
+ TextEditingController(text: _selectedEntry.value.meaning);
+
+ if (!_isFileImage) {
+ if (_selectedEntry.value.word.contains(";")) {
+ searchTerm = _selectedEntry.value.word.split(";").first;
+ } else if (_selectedEntry.value.word.contains("/")) {
+ searchTerm = _selectedEntry.value.word.split("/").first;
+ } else {
+ searchTerm = _selectedEntry.value.word;
+ }
+ _selectedIndex.value = 0;
+ }
+
+ setState(() {});
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget buildEditor() {
+ Widget displayField(
+ String labelText,
+ String hintText,
+ IconData icon,
+ TextEditingController controller,
+ ) {
+ return TextFormField(
+ keyboardType: TextInputType.multiline,
+ maxLines: null,
+ controller: controller,
+ decoration: InputDecoration(
+ prefixIcon: Icon(icon),
+ suffixIcon: IconButton(
+ iconSize: 18,
+ onPressed: () => controller.clear(),
+ icon: Icon(Icons.clear, color: Colors.white),
+ ),
+ labelText: labelText,
+ hintText: hintText,
+ ),
+ );
+ }
+
+ Widget imageSearchField = TextFormField(
+ keyboardType: TextInputType.multiline,
+ maxLines: null,
+ controller: _imageSearchController,
+ decoration: InputDecoration(
+ prefixIcon: Icon(Icons.image, color: Colors.white),
+ suffixIcon: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ iconSize: 18,
+ onPressed: () {
+ setState(() {
+ _isFileImage = false;
+ _fileImage = null;
+ searchTerm = _imageSearchController.text;
+ _selectedIndex.value = 0;
+ });
+ },
+ icon: Icon(Icons.search, color: Colors.white),
+ ),
+ IconButton(
+ iconSize: 18,
+ onPressed: () async {
+ final _picker = ImagePicker();
+ final pickedFile =
+ await _picker.getImage(source: ImageSource.camera);
+
+ setState(() {
+ _fileImage = File(pickedFile.path);
+ _isFileImage = true;
+ _networkImageURL = null;
+ });
+ },
+ icon: Icon(Icons.add_a_photo, color: Colors.white),
+ ),
+ IconButton(
+ iconSize: 18,
+ onPressed: () async {
+ final _picker = ImagePicker();
+ final pickedFile =
+ await _picker.getImage(source: ImageSource.gallery);
+
+ setState(() {
+ _fileImage = File(pickedFile.path);
+ _isFileImage = true;
+ _networkImageURL = null;
+ });
+ },
+ icon: Icon(Icons.file_upload, color: Colors.white),
+ ),
+ IconButton(
+ iconSize: 18,
+ onPressed: () {
+ setState(() {
+ _isFileImage = true;
+ _fileImage = null;
+ _networkImageURL = null;
+ _imageSearchController.clear();
+ });
+ },
+ icon: Icon(Icons.clear, color: Colors.white),
+ ),
+ ],
+ ),
+ labelText: "Image",
+ hintText: "Enter search term here",
+ ),
+ );
+ Widget sentenceField = displayField(
+ "Sentence",
+ "Enter front of card or sentence here",
+ Icons.format_align_center_rounded,
+ _sentenceController,
+ );
+
+ Widget wordField = displayField(
+ "Word",
+ "Enter the word in the back here",
+ Icons.speaker_notes_outlined,
+ _wordController,
+ );
+
+ wordField = TextFormField(
+ keyboardType: TextInputType.multiline,
+ maxLines: null,
+ controller: _wordController,
+ decoration: InputDecoration(
+ prefixIcon: Icon(
+ Icons.speaker_notes_outlined,
+ ),
+ suffixIcon: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ iconSize: 18,
+ onPressed: () async {
+ String searchTerm = _wordController.text;
+ showDictionaryDialog(await getWordDetails(searchTerm));
+ },
+ icon: Text("A⌕", style: TextStyle(color: Colors.white)),
+ ),
+ IconButton(
+ iconSize: 18,
+ onPressed: () async {
+ String searchTerm = _wordController.text;
+ showDictionaryDialog(
+ await getMonolingualWordDetails(searchTerm, false));
+ },
+ icon: Text("あ⌕", style: TextStyle(color: Colors.white)),
+ ),
+ IconButton(
+ iconSize: 18,
+ onPressed: () => _wordController.clear(),
+ icon: Icon(Icons.clear, color: Colors.white),
+ ),
+ ],
+ ),
+ labelText: "Word",
+ hintText: "Enter the word in the back here",
+ ),
+ );
+
+ Widget readingField = displayField(
+ "Reading",
+ "Enter the reading of the word here",
+ Icons.surround_sound_outlined,
+ _readingController,
+ );
+ Widget meaningField = displayField(
+ "Meaning",
+ "Enter the meaning in the back here",
+ Icons.translate_rounded,
+ _meaningController,
+ );
+
+ Widget showFileImage() {
+ if (_fileImage == null) {
+ return Container();
+ }
+
+ return ListView(
+ shrinkWrap: true,
+ children: [
+ GestureDetector(
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => PhotoView(
+ initialScale: PhotoViewComputedScale.contained * 1,
+ minScale: PhotoViewComputedScale.contained * 1,
+ imageProvider: FileImage(_fileImage),
+ ),
+ ),
+ );
+ },
+ child: FadeInImage(
+ fadeOutDuration: Duration(milliseconds: 10),
+ fadeInDuration: Duration(milliseconds: 50),
+ placeholder: MemoryImage(kTransparentImage),
+ image: FileImage(_fileImage),
+ fit: BoxFit.fitHeight,
+ height: MediaQuery.of(context).size.height / 5,
+ ),
+ ),
+ SizedBox(height: 10),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "Showing local image",
+ style: TextStyle(
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Widget showNetworkImage() {
+ return FutureBuilder(
+ future: scrapeBingImages(searchTerm),
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ switch (snapshot.connectionState) {
+ case ConnectionState.waiting:
+ return buildSearchingMessage();
+ break;
+ default:
+ if (snapshot.data == null || snapshot.data.isEmpty) {
+ _fileImage = null;
+ return showFileImage();
+ }
+
+ imageURLs = snapshot.data;
+ return ListView(
+ shrinkWrap: true,
+ children: [
+ GestureDetector(
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => PhotoView(
+ initialScale:
+ PhotoViewComputedScale.contained * 1,
+ minScale: PhotoViewComputedScale.contained * 1,
+ imageProvider: NetworkImage(
+ imageURLs[_selectedIndex.value],
+ ),
+ ),
+ ),
+ );
+ },
+ onHorizontalDragEnd: (details) {
+ if (details.primaryVelocity == 0) return;
+
+ if (details.primaryVelocity.compareTo(0) == -1) {
+ if (_selectedIndex.value == imageURLs.length - 1) {
+ _selectedIndex.value = 0;
+ } else {
+ _selectedIndex.value += 1;
+ }
+ } else {
+ if (_selectedIndex.value == 0) {
+ _selectedIndex.value = imageURLs.length - 1;
+ } else {
+ _selectedIndex.value -= 1;
+ }
+ }
+ },
+ child: ValueListenableBuilder(
+ valueListenable: _selectedIndex,
+ builder: (BuildContext context, value, Widget child) {
+ _networkImageURL = imageURLs[_selectedIndex.value];
+
+ return FadeInImage(
+ fadeOutDuration: Duration(milliseconds: 10),
+ fadeInDuration: Duration(milliseconds: 50),
+ placeholder: MemoryImage(kTransparentImage),
+ image: NetworkImage(_networkImageURL),
+ fit: BoxFit.fitHeight,
+ height: MediaQuery.of(context).size.height / 5,
+ );
+ },
+ )),
+ SizedBox(height: 10),
+ ValueListenableBuilder(
+ valueListenable: _selectedIndex,
+ builder: (BuildContext context, value, Widget child) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "Selecting image ",
+ style: TextStyle(
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "${_selectedIndex.value + 1} ",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "out of ",
+ style: TextStyle(
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "${imageURLs.length} ",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "found for",
+ style: TextStyle(
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ "『$searchTerm』",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 11,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ );
+ },
+ ),
+ ],
+ );
+ }
+ },
+ );
+ }
+
+ Widget showImagePreview() {
+ if (_isFileImage) {
+ return showFileImage();
+ } else {
+ return showNetworkImage();
+ }
+ }
+
+ Widget showExportButton() {
+ return ValueListenableBuilder(
+ valueListenable: _justExported,
+ builder: (BuildContext context, bool exported, ___) {
+ return Container(
+ width: double.infinity,
+ margin: EdgeInsets.only(bottom: 12),
+ color: Colors.grey[800].withOpacity(0.2),
+ child: InkWell(
+ enableFeedback: !exported,
+ child: Padding(
+ padding: EdgeInsets.all(16),
+ child: InkWell(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.note_add_sharp,
+ size: 16,
+ color: exported ? Colors.grey : Colors.white),
+ SizedBox(width: 5),
+ Text(
+ exported ? "Card Exported" : "Export Card",
+ style: TextStyle(
+ color: exported ? Colors.grey : Colors.white,
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ onTap: () async {
+ if (_sentenceController.text == "" &&
+ _wordController.text == "" &&
+ _readingController.text == "" &&
+ _meaningController.text == "" &&
+ _fileImage == null) {
+ return;
+ }
+
+ if (_fileImage == null && _networkImageURL != null) {
+ var response = await http.get(Uri.tryParse(_networkImageURL));
+
+ File previewImageFile = File(getPreviewImagePath());
+ if (previewImageFile.existsSync()) {
+ previewImageFile.deleteSync();
+ }
+
+ previewImageFile.writeAsBytesSync(response.bodyBytes);
+ _fileImage = previewImageFile;
+ }
+
+ try {
+ exportCreatorAnkiCard(
+ _selectedDeck.value,
+ _sentenceController.text,
+ _wordController.text,
+ _readingController.text,
+ _meaningController.text,
+ _fileImage,
+ );
+
+ setState(() {
+ _isFileImage = true;
+ _fileImage = null;
+
+ _sentenceController.clear();
+ _wordController.clear();
+ _readingController.clear();
+ _meaningController.clear();
+ });
+
+ _justExported.value = true;
+ Future.delayed(Duration(seconds: 2), () {
+ _justExported.value = false;
+ });
+ } catch (e) {
+ print(e);
+ }
+ },
+ ),
+ );
+ },
+ );
+ }
+
+ return Scaffold(
+ backgroundColor: Colors.black,
+ body: Column(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ showImagePreview(),
+ DeckDropDown(
+ decks: decks,
+ selectedDeck: _selectedDeck,
+ ),
+ imageSearchField,
+ sentenceField,
+ wordField,
+ readingField,
+ meaningField,
+ SizedBox(height: 10),
+ ],
+ ),
+ ),
+ ),
+ showExportButton(),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/player.dart b/lib/player.dart
index 5ceb6de70..b655a5d10 100644
--- a/lib/player.dart
+++ b/lib/player.dart
@@ -277,7 +277,8 @@ class _VideoPlayerState extends State {
FocusNode _subtitleFocusNode = new FocusNode();
bool networkNotSet = true;
ValueNotifier _wasPlaying = ValueNotifier(false);
- ValueNotifier _widgetVisbility = ValueNotifier(false);
+ ValueNotifier _widgetVisibility = ValueNotifier(false);
+ ValueNotifier _shadowingSubtitle = ValueNotifier(null);
int audioAllowance = getAudioAllowance();
Timer durationTimer;
@@ -347,6 +348,15 @@ class _VideoPlayerState extends State {
void visibilityTimerAction() {
if (getVideoPlayerController().value.isInitialized) {
+ if (_shadowingSubtitle.value != null) {
+ if (getVideoPlayerController().value.position >
+ _shadowingSubtitle.value.endTime ||
+ getVideoPlayerController().value.position <
+ _shadowingSubtitle.value.startTime - Duration(seconds: 10)) {
+ getVideoPlayerController().seekTo(_shadowingSubtitle.value.startTime);
+ }
+ }
+
Duration cutOffStart =
_currentSubtitle.value.startTime - Duration(milliseconds: 100);
Duration cutOffEnd =
@@ -603,6 +613,14 @@ class _VideoPlayerState extends State {
return _videoPlayerController;
}
+ void toggleShadowingMode() {
+ if (_shadowingSubtitle.value == null) {
+ _shadowingSubtitle.value = _currentSubtitle.value;
+ } else {
+ _shadowingSubtitle.value = null;
+ }
+ }
+
ChewieController getChewieController() {
_chewieController ??= ChewieController(
videoPlayerController: getVideoPlayerController(),
@@ -615,6 +633,8 @@ class _VideoPlayerState extends State {
playExternalSubtitles: playExternalSubtitles,
retimeSubtitles: retimeSubtitles,
exportSingleCallback: exportSingleCallback,
+ toggleShadowingMode: toggleShadowingMode,
+ shadowingSubtitle: _shadowingSubtitle,
streamData: streamData,
aspectRatio: getVideoPlayerController().value.aspectRatio,
autoPlay: true,
@@ -640,7 +660,7 @@ class _VideoPlayerState extends State {
subtitleDecoder: SubtitleDecoder.utf8,
subtitleType: SubtitleType.srt,
subtitlesOffset: getSubtitleDelay(),
- widgetVisibility: _widgetVisbility,
+ widgetVisibility: _widgetVisibility,
);
return _subTitleController;
diff --git a/lib/preferences.dart b/lib/preferences.dart
index e9b84d260..725876336 100644
--- a/lib/preferences.dart
+++ b/lib/preferences.dart
@@ -146,6 +146,16 @@ Directory getTermBankDirectory() {
return directory;
}
+Future setLastDeck(String selectedDeck) async {
+ await gSharedPrefs.setString("lastDeck", selectedDeck);
+}
+
+String getLastDeck() {
+ String lastDeck = gSharedPrefs.getString('lastDeck') ?? 'Default';
+
+ return lastDeck;
+}
+
Future toggleSelectMode() async {
await gSharedPrefs.setBool("selectMode", !getSelectMode());
}
diff --git a/lib/util.dart b/lib/util.dart
index f289dce20..913c4d313 100644
--- a/lib/util.dart
+++ b/lib/util.dart
@@ -1,6 +1,10 @@
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
+import 'package:html/parser.dart' as parser;
+import 'package:html/dom.dart' as dom;
+import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:flutter_ffmpeg/flutter_ffmpeg.dart';
import 'package:mecab_dart/mecab_dart.dart';
@@ -422,3 +426,26 @@ String getBetterNumberTag(String text) {
return text;
}
+
+Future> scrapeBingImages(String searchTerm) async {
+ List entries = [];
+
+ var client = http.Client();
+ http.Response response = await client.get(Uri.parse(
+ 'https://www.bing.com/images/search?q=$searchTerm&FORM=HDRSC2'));
+ var document = parser.parse(response.body);
+
+ List imgElements = document.getElementsByClassName("iusc");
+
+ if (imgElements == null) {
+ return [];
+ }
+
+ for (dom.Element imgElement in imgElements) {
+ Map imgMap = jsonDecode(imgElement.attributes["m"]);
+ String imageURL = imgMap["turl"];
+ entries.add(imageURL);
+ }
+
+ return entries;
+}
diff --git a/pubspec.lock b/pubspec.lock
index efa8938ea..5a6bf29b1 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -321,6 +321,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
+ image_picker:
+ dependency: "direct main"
+ description:
+ name: image_picker
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.7+22"
+ image_picker_platform_interface:
+ dependency: transitive
+ description:
+ name: image_picker_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.6"
import_js_library:
dependency: transitive
description:
@@ -470,6 +484,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
+ photo_view:
+ dependency: "direct main"
+ description:
+ name: photo_view
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.11.1"
platform:
dependency: "direct overridden"
description:
@@ -498,6 +519,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.3"
+ receive_sharing_intent:
+ dependency: "direct main"
+ description:
+ name: receive_sharing_intent
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.4.5"
rxdart:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index a6e908008..6ab814e9d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,7 +2,7 @@ name: jidoujisho
description: A mobile video player tailored for Japanese language learners.
publish_to: none
-version: 0.14.2+16
+version: 0.15.0+17
environment:
sdk: ">=2.7.0 <3.0.0"
@@ -28,6 +28,7 @@ dependencies:
url: https://github.com/project-violet/flutter-youtube-dl
ref: 046b5e6c051b3fda333c1f366e2640cd04f763b8
fuzzy:
+ image_picker:
intl:
http:
lazy_load_scrollview: ^1.3.0
@@ -38,6 +39,8 @@ dependencies:
package_info: ^0.4.3+4
path_provider:
permission_handler:
+ photo_view:
+ receive_sharing_intent:
scrollable_positioned_list: ^0.1.9
shell:
shared_preferences: