diff --git a/README.md b/README.md index db95559cb..c7c815398 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

A mobile video player tailored for Japanese language learners.

Latest GitHub Release:
-0.13.0-beta 🇯🇵 → 🇬🇧
+0.13.1-beta 🇯🇵 → 🇬🇧
0.5.3-beta 🇬🇧 → 🇯🇵

diff --git a/android/app/build.gradle b/android/app/build.gradle index 79c4b3c00..65d32a44b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -49,6 +49,7 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug + shrinkResources false } } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a0e9f5e0b..29b9d40b6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,8 @@ - + + + + + + + + + + + + currentSubtitle; final ValueNotifier currentDictionaryEntry; final ValueNotifier currentSubTrack; + final ValueNotifier wasPlaying; final VoidCallback playExternalSubtitles; final VoidCallback retimeSubtitles; final YouTubeMux streamData; diff --git a/chewie/lib/src/material_controls.dart b/chewie/lib/src/material_controls.dart index 0040c25ca..457f1a574 100755 --- a/chewie/lib/src/material_controls.dart +++ b/chewie/lib/src/material_controls.dart @@ -250,6 +250,43 @@ class _MaterialControlsState extends State ); } + Future openExtraShare() async { + final chosenOption = await showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: true, + builder: (context) => const _MoreOptionsDialog(options: [ + "Search Current Subtitle with Jisho.org", + "Translate Current Subtitle with DeepL", + "Translate Current Subtitle with Google Translate", + "Share Current Subtitle with Menu", + ], icons: [ + Icons.menu_book_rounded, + Icons.translate_rounded, + Icons.g_translate_rounded, + Icons.share_outlined, + ]), + ); + + final String subtitleText = chewieController.currentSubtitle.value.text; + + switch (chosenOption) { + case 0: + await launch("https://jisho.org/search/$subtitleText"); + break; + case 1: + await launch("https://www.deepl.com/translator#ja/en/$subtitleText"); + break; + case 2: + await launch( + "https://translate.google.com/?sl=ja&tl=en&text=$subtitleText&op=translate"); + break; + case 3: + Share.share(subtitleText); + break; + } + } + Widget _buildMoreButton(VlcPlayerController controller) { return GestureDetector( onTap: () async { @@ -260,62 +297,53 @@ class _MaterialControlsState extends State isScrollControlled: true, useRootNavigator: true, builder: (context) => _MoreOptionsDialog(options: [ - "Search Current Subtitle with Jisho.org", - "Translate Current Subtitle with DeepL", - "Translate Current Subtitle with Google Translate", - "Share Current Subtitle to App", - if (gIsSelectMode.value) + "Share Current Subtitle to Applications", + if (getSelectMode()) "Use Tap to Select Subtitle Selection" else "Use Drag to Select Subtitle Selection", + if (getFocusMode()) + "Turn Off Definition Focus Mode" + else + "Turn On Definition Focus Mode", "Adjust Subtitle Delay", "Load External Subtitles", "Export Current Context to Anki", ], icons: [ - Icons.menu_book_rounded, - Icons.translate_rounded, - Icons.g_translate_rounded, Icons.share_outlined, - if (gIsSelectMode.value) + if (getSelectMode()) Icons.touch_app_rounded else Icons.select_all_rounded, - Icons.timer_sharp, + if (getFocusMode()) + Icons.lightbulb_outline_rounded + else + Icons.lightbulb, + Icons.timer_rounded, Icons.subtitles_outlined, Icons.mobile_screen_share_rounded, ]), ); - final String subtitleText = chewieController.currentSubtitle.value.text; - switch (chosenOption) { case 0: - await launch("https://jisho.org/search/$subtitleText"); + openExtraShare(); break; case 1: - await launch( - "https://www.deepl.com/translator#ja/en/$subtitleText"); + toggleSelectMode(); + gIsSelectMode.value = getSelectMode(); break; case 2: - await launch( - "https://translate.google.com/?sl=ja&tl=en&text=$subtitleText&op=translate"); + toggleFocusMode(); break; case 3: - Share.share(subtitleText); - break; - case 4: - toggleSelectMode(); - gIsSelectMode.value = getSelectMode(); - break; - case 5: controller.pause(); chewieController.retimeSubtitles(); break; - case 6: + case 4: chewieController.playExternalSubtitles(); break; - case 7: - final bool wasPlaying = await controller.isPlaying(); + case 5: controller.pause(); final Subtitle currentSubtitle = @@ -332,7 +360,7 @@ class _MaterialControlsState extends State chewieController.clipboard, currentSubtitle, currentDictionaryEntry, - wasPlaying, + chewieController.wasPlaying.value, ); break; @@ -591,6 +619,7 @@ class _MaterialControlsState extends State void _playPause() { final isFinished = controller.value.isEnded; + chewieController.wasPlaying.value = false; setState(() { if (controller.value.isPlaying) { diff --git a/lib/anki.dart b/lib/anki.dart index 53d9ee8ac..acbfeab13 100644 --- a/lib/anki.dart +++ b/lib/anki.dart @@ -220,9 +220,12 @@ void showAnkiDialog( flex: 30, child: SingleChildScrollView( child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Image.file(File(getPreviewImagePath()), - fit: BoxFit.contain), + Image.file( + File(getPreviewImagePath()), + fit: BoxFit.fitWidth, + ), DeckDropDown( decks: decks, selectedDeck: _selectedDeck, diff --git a/lib/globals.dart b/lib/globals.dart index c70abe43b..ce5182ab6 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,5 +1,6 @@ import 'package:async/async.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_vlc_player/flutter_vlc_player.dart'; import 'package:fuzzy/fuzzy.dart'; import 'package:mecab_dart/mecab_dart.dart'; import 'package:package_info/package_info.dart'; @@ -29,3 +30,5 @@ Map gMetadataCache = {}; List gCustomDictionary; Fuzzy gCustomDictionaryFuzzy; + +ValueNotifier gPlayPause = ValueNotifier(true); diff --git a/lib/main.dart b/lib/main.dart index 7bbc617f4..6dd24168c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'package:async/async.dart'; +import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_vlc_player/flutter_vlc_player.dart'; import 'package:fuzzy/fuzzy.dart'; import 'package:lazy_load_scrollview/lazy_load_scrollview.dart'; @@ -29,7 +31,8 @@ typedef void SearchCallback(String term); void main() async { WidgetsFlutterBinding.ensureInitialized(); - SystemChrome.setEnabledSystemUIOverlays([]); + SystemChrome.setEnabledSystemUIOverlays( + [SystemUiOverlay.bottom, SystemUiOverlay.top]); await Permission.storage.request(); requestAnkiDroidPermissions(); @@ -47,9 +50,51 @@ void main() async { gCustomDictionary = importCustomDictionary(); gCustomDictionaryFuzzy = Fuzzy(getAllImportedWords()); + await AudioService.connect(); + await AudioService.start(backgroundTaskEntrypoint: _backgroundTaskEntrypoint); + runApp(App()); } +_backgroundTaskEntrypoint() { + AudioServiceBackground.run(() => AudioPlayerTask()); +} + +class AudioPlayerTask extends BackgroundAudioTask { + AudioPlayerTask(); + + @override + Future onPlay() async { + AudioServiceBackground.sendCustomEvent("playPause"); + } + + @override + Future onPause() async { + AudioServiceBackground.sendCustomEvent("playPause"); + } + + @override + Future onFastForward() async { + AudioServiceBackground.sendCustomEvent("rewindFastForward"); + } + + @override + Future onRewind() async { + AudioServiceBackground.sendCustomEvent("rewindFastForward"); + } +} + +void unlockLandscape() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + + SystemChrome.setEnabledSystemUIOverlays( + [SystemUiOverlay.bottom, SystemUiOverlay.top]); +} + class App extends StatelessWidget { @override Widget build(BuildContext context) { @@ -63,7 +108,7 @@ class App extends StatelessWidget { appBarTheme: AppBarTheme(backgroundColor: Colors.black), canvasColor: Colors.grey[900], ), - home: Home(), + home: AudioServiceWidget(child: Home()), ); } } @@ -97,13 +142,7 @@ class _HomeState extends State { builder: (context) => Player(), ), ).then((returnValue) { - setState(() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); - }); + unlockLandscape(); }); } else { _selectedIndex = index; @@ -177,11 +216,7 @@ class _HomeState extends State { @override Widget build(BuildContext context) { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); + unlockLandscape(); return new WillPopScope( onWillPop: _onWillPop, @@ -716,11 +751,7 @@ class _HomeState extends State { ), ).then((returnValue) { setState(() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); + unlockLandscape(); }); setLastPlayedPath(webURL); @@ -889,11 +920,7 @@ class _HomeState extends State { ), ), ).then((returnValue) { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); + unlockLandscape(); }); }, ); @@ -1145,11 +1172,7 @@ class _YouTubeResultState extends State ), ), ).then((returnValue) { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); + unlockLandscape(); setLastPlayedPath(videoStreamURL); setLastPlayedPosition(0); @@ -1759,11 +1782,7 @@ class _HistoryResultState extends State ), ), ).then((returnValue) { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); + unlockLandscape(); setLastPlayedPath(history.url); setLastPlayedPosition(0); diff --git a/lib/player.dart b/lib/player.dart index d990caa35..16f9a9716 100644 --- a/lib/player.dart +++ b/lib/player.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:audio_service/audio_service.dart'; import 'package:chewie/chewie.dart'; import 'package:clipboard_monitor/clipboard_monitor.dart'; import 'package:external_app_launcher/external_app_launcher.dart'; @@ -33,6 +34,15 @@ class Player extends StatelessWidget { final String url; final Video video; + void lockLandscape() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + + SystemChrome.setEnabledSystemUIOverlays([]); + } + @override Widget build(BuildContext context) { SystemChrome.setEnabledSystemUIOverlays([]); @@ -104,10 +114,7 @@ class Player extends StatelessWidget { setLastPlayedPosition(0); gIsResumable.value = getResumeAvailable(); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); + lockLandscape(); VideoHistory history = VideoHistory( videoFile.path, @@ -168,10 +175,7 @@ class Player extends StatelessWidget { internalSubs = extractWebSubtitle(webSubtitles); } - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); + lockLandscape(); VideoHistory history = VideoHistory( url, @@ -260,6 +264,7 @@ class _VideoPlayerState extends State { String _volatileText = ""; FocusNode _subtitleFocusNode = new FocusNode(); bool networkNotSet = true; + ValueNotifier _wasPlaying = ValueNotifier(false); Timer timer; @@ -270,6 +275,31 @@ class _VideoPlayerState extends State { Duration(seconds: 1), (Timer t) => updateDurationOrSeek()); } + Future playPause() async { + _wasPlaying.value = false; + + if (getVideoPlayerController().value.isPlaying) { + await getVideoPlayerController().pause(); + } else { + await getVideoPlayerController().play(); + } + } + + Future rewindFastForward() async { + await getVideoPlayerController().seekTo(_currentSubtitle.value.startTime); + } + + @override + void dispose() { + if (_videoPlayerController != null && _chewieController != null) { + _videoPlayerController?.stopRendererScanning(); + _videoPlayerController?.dispose(); + _chewieController?.dispose(); + } + timer.cancel(); + super.dispose(); + } + void startClipboardMonitor() { ClipboardMonitor.registerCallback(onClipboardText); } @@ -349,17 +379,6 @@ class _VideoPlayerState extends State { ); final _currentSubTrack = ValueNotifier(-1); - @override - void dispose() { - super.dispose(); - if (_videoPlayerController != null && _chewieController != null) { - _videoPlayerController?.stopRendererScanning(); - _videoPlayerController?.dispose(); - _chewieController?.dispose(); - } - timer.cancel(); - } - @override Widget build(BuildContext context) { startClipboardMonitor(); @@ -375,7 +394,7 @@ class _VideoPlayerState extends State { onHorizontalDragUpdate: (details) { if (details.delta.dx > 20) { getVideoPlayerController() - .seekTo(_currentSubtitle.value.endTime); + .seekTo(_currentSubtitle.value.startTime); } else if (details.delta.dx < -20) { getVideoPlayerController() .seekTo(_currentSubtitle.value.startTime); @@ -392,6 +411,22 @@ class _VideoPlayerState extends State { }, child: getSubtitleWrapper(), ), + StreamBuilder( + stream: AudioService.customEventStream, + builder: (context, snapshot) { + String response = snapshot.data; + switch (response) { + case "playPause": + playPause(); + break; + case "rewindFastForward": + rewindFastForward(); + break; + default: + } + return Container(); + }, + ), buildSubTrackChanger(), buildDictionary(), ], @@ -400,6 +435,20 @@ class _VideoPlayerState extends State { ); } + void exportLongCallback( + Subtitle selectedSubtitle, + ) { + exportToAnki( + context, + getChewieController(), + getVideoPlayerController(), + _clipboard, + selectedSubtitle, + _currentDictionaryEntry.value, + false, + ); + } + Future _onWillPop() async { Widget alertDialog = AlertDialog( shape: RoundedRectangleBorder( @@ -486,6 +535,7 @@ class _VideoPlayerState extends State { currentDictionaryEntry: _currentDictionaryEntry, currentSubtitle: _currentSubtitle, currentSubTrack: _currentSubTrack, + wasPlaying: _wasPlaying, playExternalSubtitles: playExternalSubtitles, retimeSubtitles: retimeSubtitles, streamData: streamData, @@ -803,6 +853,11 @@ class _VideoPlayerState extends State { padding: EdgeInsets.all(16.0), child: InkWell( onTap: () { + if (getFocusMode() && _wasPlaying.value) { + _videoPlayerController.play(); + _wasPlaying.value = false; + } + _clipboard.value = ""; _currentDictionaryEntry.value = DictionaryEntry( word: "", @@ -837,6 +892,11 @@ class _VideoPlayerState extends State { alignment: Alignment.topCenter, child: GestureDetector( onTap: () { + if (getFocusMode() && _wasPlaying.value) { + _videoPlayerController.play(); + _wasPlaying.value = false; + } + _clipboard.value = ""; _currentDictionaryEntry.value = DictionaryEntry( word: "", @@ -972,8 +1032,15 @@ class _VideoPlayerState extends State { if (_clipboard.value == "") { return Container(); } + switch (snapshot.connectionState) { case ConnectionState.waiting: + if (getFocusMode()) { + _wasPlaying.value = + getVideoPlayerController().value.isPlaying; + _videoPlayerController.pause(); + } + return buildDictionaryLoading(clipboard); default: List entries = snapshot.data; @@ -1067,7 +1134,7 @@ class _VideoPlayerState extends State { } } - showModalBottomSheet( + await showModalBottomSheet( backgroundColor: Theme.of(context).primaryColor.withOpacity(0.8), context: context, isScrollControlled: true, @@ -1121,12 +1188,8 @@ class _VideoPlayerState extends State { String subtitleEnd = getTimestampFromDuration(subtitle.endTime); String subtitleDuration = "$subtitleStart - $subtitleEnd"; - int endSubtitleIndex; - return ListTile( - selected: (endSubtitleIndex == null) - ? i == index - : i <= index && i >= endSubtitleIndex, + selected: i == index, selectedTileColor: Colors.red.withOpacity(0.15), dense: true, title: Column( @@ -1166,13 +1229,13 @@ class _VideoPlayerState extends State { onLongPress: () { if (i <= index) { List selectedSubtitles = []; - for (int subIndex = i; subIndex < index; subIndex++) { + for (int subIndex = i; subIndex <= index; subIndex++) { selectedSubtitles.add(subtitles[subIndex]); } String selectedText = ""; String removeLastNewline(String n) => - n = n.substring(0, n.length - 2); + n = n.substring(0, n.length - 1); selectedSubtitles.forEach( (subtitle) => selectedText += subtitle.text + "\n"); selectedText = removeLastNewline(selectedText); @@ -1186,15 +1249,36 @@ class _VideoPlayerState extends State { endTime: selectedEndTime, ); - exportToAnki( - context, - chewie, - controller, - chewie.clipboard, - selectedSubtitle, - currentDictionaryEntry, - false, + chewie.clipboard.value = "&<&>export&<&>"; + + exportLongCallback(selectedSubtitle); + Navigator.pop(context); + } else if (i > index) { + List selectedSubtitles = []; + for (int subIndex = index; subIndex <= i; subIndex++) { + selectedSubtitles.add(subtitles[subIndex]); + } + + String selectedText = ""; + String removeLastNewline(String n) => + n = n.substring(0, n.length - 1); + selectedSubtitles.forEach( + (subtitle) => selectedText += subtitle.text + "\n"); + selectedText = removeLastNewline(selectedText); + + Duration selectedStartTime = selectedSubtitles.first.startTime; + Duration selectedEndTime = selectedSubtitles.last.endTime; + + Subtitle selectedSubtitle = Subtitle( + text: selectedText, + startTime: selectedStartTime, + endTime: selectedEndTime, ); + + chewie.clipboard.value = "&<&>export&<&>"; + + exportLongCallback(selectedSubtitle); + Navigator.pop(context); } }); }, diff --git a/lib/preferences.dart b/lib/preferences.dart index 0d3d4dc00..61828124d 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -142,6 +142,14 @@ bool getSelectMode() { return gSharedPrefs.getBool("selectMode") ?? false; } +Future toggleFocusMode() async { + await gSharedPrefs.setBool("focusMode", !getFocusMode()); +} + +bool getFocusMode() { + return gSharedPrefs.getBool("focusMode") ?? false; +} + bool getResumeAvailable() { String lastPlayedPath = getLastPlayedPath(); return lastPlayedPath != "-1"; diff --git a/lib/youtube.dart b/lib/youtube.dart index 7859f4059..e49e563a0 100644 --- a/lib/youtube.dart +++ b/lib/youtube.dart @@ -149,8 +149,12 @@ Future getPlayerYouTubeInfo(String webURL) async { return aHeight.compareTo(bHeight); }); + for (var stream in streamManifest.audioOnly.sortByBitrate()) { + print(stream.audioCodec); + } AudioStreamInfo streamAudioInfo = streamManifest.audioOnly.sortByBitrate().last; + String audioURL = streamAudioInfo.url.toString(); String audioMetadata = "[${streamAudioInfo.container.name}] - [${streamAudioInfo.bitrate.kiloBitsPerSecond.floor()} Kbps]"; diff --git a/pubspec.lock b/pubspec.lock index 6a32334c5..adeef162f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.5.0" + audio_service: + dependency: "direct main" + description: + name: audio_service + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + audio_session: + dependency: transitive + description: + name: audio_session + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" audioplayers: dependency: "direct main" description: @@ -195,6 +209,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" flutter_ffmpeg: dependency: "direct main" description: @@ -209,6 +230,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.3.2" + flutter_isolate: + dependency: transitive + description: + name: flutter_isolate + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -470,6 +498,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.3.3" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.26.0" scrollable_positioned_list: dependency: "direct main" description: @@ -531,6 +566,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + sqflite: + dependency: transitive + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0+3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0+2" string_scanner: dependency: transitive description: @@ -545,6 +594,13 @@ packages: relative: true source: path version: "1.0.4" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" term_glyph: dependency: transitive description: @@ -714,4 +770,4 @@ packages: version: "8.0.0" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.22.2" + flutter: ">=1.24.0-10" diff --git a/pubspec.yaml b/pubspec.yaml index 395fb9efc..92c5ac14c 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.13.0+5 +version: 0.13.1+6 environment: sdk: ">=2.7.0 <3.0.0" @@ -12,6 +12,7 @@ dependencies: sdk: flutter audioplayers: + audio_service: ^0.17.0 async: chewie: path: ./chewie