Skip to content

Commit f9db852

Browse files
committed
Add iOS language setting, resolves #17
1 parent 54be65e commit f9db852

11 files changed

+222
-75
lines changed

ios/Runner/AppDelegate.swift

+69-56
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,84 @@
1-
import UIKit
21
import Flutter
32
import Speech
3+
import UIKit
4+
45
@UIApplicationMain
56
@objc class AppDelegate: FlutterAppDelegate {
6-
77
private func receiveTxRequest(call: FlutterMethodCall, result: @escaping FlutterResult) {
8-
let uri = (call.arguments as! [String:Any])["path"] as! String
9-
let url = URL(fileURLWithPath: uri)
10-
guard let myRecognizer = SFSpeechRecognizer() else {
11-
// A recognizer is not supported for the current locale
12-
result(FlutterError(code: "FAILED_REC", message: "unsupported locale", details: nil))
13-
return
14-
}
15-
16-
if !myRecognizer.isAvailable {
17-
result(FlutterError(code: "FAILED_REC", message: "unavailable recognizer", details: nil))
18-
return
19-
}
8+
let uri = (call.arguments as! [String: Any])["path"] as! String
9+
let localeId = (call.arguments as! [String: Any])["locale"] as! String
10+
let url = URL(fileURLWithPath: uri)
11+
guard let myRecognizer = SFSpeechRecognizer(locale: Locale.init(identifier: localeId)) else {
12+
// A recognizer is not supported for the current locale
13+
result(FlutterError(code: "FAILED_REC", message: "unsupported locale", details: nil))
14+
return
15+
}
16+
17+
if !myRecognizer.isAvailable {
18+
result(FlutterError(code: "FAILED_REC", message: "unavailable recognizer", details: nil))
19+
return
20+
}
2021

21-
let request = SFSpeechURLRecognitionRequest(url: url)
22-
myRecognizer.recognitionTask(with: request) { (res, error) in
23-
guard let res = res else {
24-
// Recognition failed, so check error for details and handle it
25-
result(FlutterError(code: "FAILED_REC", message: error.debugDescription, details: nil))
26-
return
27-
}
22+
let request = SFSpeechURLRecognitionRequest(url: url)
23+
myRecognizer.recognitionTask(with: request) { (res, error) in
24+
guard let res = res else {
25+
// Recognition failed, so check error for details and handle it
26+
result(FlutterError(code: "FAILED_REC", message: error.debugDescription, details: nil))
27+
return
28+
}
2829

29-
// Print the speech that has been recognized so far
30-
if res.isFinal {
31-
result(String(res.bestTranscription.formattedString))
32-
}
33-
}
30+
// Print the speech that has been recognized so far
31+
if res.isFinal {
32+
result(String(res.bestTranscription.formattedString))
33+
}
34+
}
35+
}
36+
private func requestTxPermission(result: @escaping FlutterResult) {
37+
let status = SFSpeechRecognizer.authorizationStatus()
38+
switch status {
39+
case .notDetermined:
40+
SFSpeechRecognizer.requestAuthorization({ (status) -> Void in
41+
result(Bool(status == SFSpeechRecognizerAuthorizationStatus.authorized))
42+
})
43+
case .denied:
44+
result(Bool(false))
45+
case .restricted:
46+
result(Bool(false))
47+
case .authorized:
48+
result(Bool(true))
49+
default:
50+
result(Bool(true))
51+
}
3452
}
35-
private func requestTxPermission(result: @escaping FlutterResult) {
36-
let status = SFSpeechRecognizer.authorizationStatus()
37-
switch status {
38-
case .notDetermined:
39-
SFSpeechRecognizer.requestAuthorization({(status)->Void in
40-
result(Bool(status == SFSpeechRecognizerAuthorizationStatus.authorized))
41-
})
42-
case .denied:
43-
result(Bool(false))
44-
case .restricted:
45-
result(Bool(false))
46-
case .authorized:
47-
result(Bool(true))
48-
default:
49-
result(Bool(true))
50-
}
53+
private func getLocaleOptions(result: @escaping FlutterResult) {
54+
let locales = SFSpeechRecognizer.supportedLocales()
55+
var localeOptions: [String: String] = [:]
56+
for locale in locales {
57+
localeOptions[locale.identifier] = locale.localizedString(forIdentifier: locale.identifier)!
5158
}
59+
result(localeOptions)
60+
}
5261
override func application(
5362
_ application: UIApplication,
5463
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
5564
) -> Bool {
56-
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
57-
let transcribeChannel = FlutterMethodChannel(name: "voiceoutliner.saga.chat/iostx", binaryMessenger: controller.binaryMessenger)
58-
transcribeChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult)-> Void in
59-
switch call.method {
60-
case "transcribe":
61-
self.receiveTxRequest(call: call, result: result)
62-
case "requestPermission":
63-
self.requestTxPermission(result: result)
64-
default:
65-
result(FlutterMethodNotImplemented)
66-
}
67-
})
68-
GeneratedPluginRegistrant.register(with: self)
69-
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
65+
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
66+
let transcribeChannel = FlutterMethodChannel(
67+
name: "voiceoutliner.saga.chat/iostx", binaryMessenger: controller.binaryMessenger)
68+
transcribeChannel.setMethodCallHandler({
69+
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
70+
switch call.method {
71+
case "transcribe":
72+
self.receiveTxRequest(call: call, result: result)
73+
case "requestPermission":
74+
self.requestTxPermission(result: result)
75+
case "getLocaleOptions":
76+
self.getLocaleOptions(result: result)
77+
default:
78+
result(FlutterMethodNotImplemented)
79+
}
80+
})
81+
GeneratedPluginRegistrant.register(with: self)
82+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
7083
}
7184
}

lib/consts.dart

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const lastRouteKey = "last_route";
1111
const lastOutlineKey = "last_outline";
1212
const modelDirKey = "model_dir";
1313
const modelLanguageKey = "model_language";
14+
const localeKey = "ios_locale";
1415

1516
const classicPurple = Color.fromRGBO(169, 129, 234, 1.0);
1617
const basePurple = Color.fromRGBO(163, 95, 255, 1);

lib/main.dart

+12-3
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@ import 'package:voice_outliner/repositories/drive_backup.dart';
1010
import 'package:voice_outliner/repositories/vosk_speech_recognizer.dart';
1111
import 'package:voice_outliner/state/outline_state.dart';
1212
import 'package:voice_outliner/state/player_state.dart';
13+
import 'package:voice_outliner/views/ios_transcription_setup_view.dart';
1314
import 'package:voice_outliner/views/notes_view.dart';
1415
import 'package:voice_outliner/views/onboarding_view.dart';
1516
import 'package:voice_outliner/views/outlines_view.dart';
16-
import 'package:voice_outliner/views/transcription_setup_view.dart';
17+
import 'package:voice_outliner/views/vosk_transcription_setup_view.dart';
1718

1819
import 'consts.dart';
1920

2021
final routes = {
2122
"/": const OutlinesView(),
2223
"/notes": const NotesView(),
2324
"/onboarding": const OnboardingView(),
24-
"/transcription_setup": const TranscriptionSetupView()
25+
"/transcription_setup_vosk": const VoskTranscriptionSetupView(),
26+
"/transcription_setup_ios": const IOSTranscriptionSetupView()
2527
};
2628

2729
const generalAppBar =
@@ -118,7 +120,14 @@ class _VoiceOutlinerAppState extends State<VoiceOutlinerApp> {
118120
widget.sharedPreferences.getString(modelDirKey) == null &&
119121
(widget.sharedPreferences.getBool(shouldTranscribeKey) ?? true) &&
120122
lastRoute != null) {
121-
return "/transcription_setup";
123+
return "/transcription_setup_vosk";
124+
}
125+
// If you've onboarded but don't have language set up on iOS
126+
if (Platform.isIOS &&
127+
widget.sharedPreferences.getString(localeKey) == null &&
128+
(widget.sharedPreferences.getBool(shouldTranscribeKey) ?? true) &&
129+
lastRoute != null) {
130+
return "/transcription_setup_ios";
122131
}
123132
if (lastRoute != null) {
124133
return lastRoute;

lib/repositories/ios_speech_recognizer.dart

+18-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import 'package:flutter/services.dart';
44

55
const iosPlatform = MethodChannel("voiceoutliner.saga.chat/iostx");
66

7-
Future<String?> recognizeNoteIOS(String path) async {
7+
Future<String?> recognizeNoteIOS(String path, String locale) async {
88
try {
9-
final platformRes =
10-
await iosPlatform.invokeMethod("transcribe", {"path": path});
9+
final platformRes = await iosPlatform
10+
.invokeMethod("transcribe", {"path": path, "locale": locale});
1111
if (platformRes is String) {
1212
return platformRes;
1313
} else {
@@ -32,3 +32,18 @@ Future<bool> tryTxPermissionIOS() async {
3232
return false;
3333
}
3434
}
35+
36+
/// Returns a map {"en-US": "English (US)"}
37+
Future<Map<String, String>> getLocaleOptions() async {
38+
if (!Platform.isIOS) {
39+
print("Not IOS");
40+
return {};
41+
}
42+
try {
43+
final res = await iosPlatform.invokeMethod("getLocaleOptions");
44+
return Map<String, String>.from(res);
45+
} catch (err) {
46+
print(err);
47+
return {};
48+
}
49+
}

lib/state/notes_state.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class NotesModel extends ChangeNotifier {
2929
bool shouldLocate = false;
3030
bool showCompleted = true;
3131
bool isReady = false;
32+
String locale = "en-US";
3233
Completer<bool> _readyCompleter = Completer();
3334
bool isIniting = false;
3435
final LinkedList<Note> notes = LinkedList<Note>();
@@ -78,7 +79,7 @@ class NotesModel extends ChangeNotifier {
7879
if (shouldTranscribe && !note.transcribed && note.filePath != null) {
7980
final path = _playerModel.getPathFromFilename(note.filePath!);
8081
final res = Platform.isIOS
81-
? await recognizeNoteIOS(path)
82+
? await recognizeNoteIOS(path, locale)
8283
: await voskSpeechRecognize(path);
8384
// Guard against writing after user went back
8485
if (isReady) {
@@ -469,6 +470,9 @@ class NotesModel extends ChangeNotifier {
469470
shouldTranscribe = prefs.getBool(shouldTranscribeKey) ?? false;
470471
shouldLocate = prefs.getBool(shouldLocateKey) ?? false;
471472
showCompleted = prefs.getBool(showCompletedKey) ?? true;
473+
if (Platform.isIOS) {
474+
locale = prefs.getString(localeKey) ?? locale;
475+
}
472476
isReady = true;
473477
_readyCompleter.complete(true);
474478
_readyCompleter = Completer();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:voice_outliner/widgets/ios_locale_selector.dart';
3+
4+
class IOSTranscriptionSetupView extends StatelessWidget {
5+
const IOSTranscriptionSetupView({Key? key}) : super(key: key);
6+
7+
@override
8+
Widget build(BuildContext context) {
9+
return Scaffold(
10+
appBar: AppBar(
11+
title: const Text("Setup"),
12+
automaticallyImplyLeading: false,
13+
),
14+
body: Padding(
15+
padding: const EdgeInsets.all(20.0),
16+
child: Center(
17+
child: Column(children: [
18+
const Text(
19+
"Select transcription language",
20+
style: TextStyle(fontSize: 18.0),
21+
),
22+
const SizedBox(height: 20),
23+
const IOSLocaleSelector(),
24+
const SizedBox(height: 20),
25+
ElevatedButton(
26+
onPressed: () => Navigator.pushNamedAndRemoveUntil(
27+
context, "/", (route) => false),
28+
child: const Text("continue"))
29+
]))),
30+
);
31+
}
32+
}

lib/views/onboarding_view.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ class _OnboardingViewState extends State<OnboardingView> {
5151

5252
void onDone() {
5353
Navigator.pushNamedAndRemoveUntil(
54-
context, Platform.isIOS ? "/" : "/transcription_setup", (_) => false);
54+
context,
55+
Platform.isIOS
56+
? "/transcription_setup_ios"
57+
: "/transcription_setup_vosk",
58+
(_) => false);
5559
}
5660

5761
@override

lib/views/settings_view.dart

+18-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import 'package:voice_outliner/consts.dart';
88
import 'package:voice_outliner/repositories/ios_speech_recognizer.dart';
99
import 'package:voice_outliner/state/outline_state.dart';
1010
import 'package:voice_outliner/views/drive_settings_view.dart';
11-
import 'package:voice_outliner/views/transcription_setup_view.dart';
11+
import 'package:voice_outliner/views/ios_transcription_setup_view.dart';
12+
import 'package:voice_outliner/views/vosk_transcription_setup_view.dart';
1213

1314
class SettingsView extends StatefulWidget {
1415
const SettingsView({Key? key}) : super(key: key);
@@ -28,7 +29,6 @@ class _SettingsViewState extends State<SettingsView> {
2829

2930
Future<void> init() async {
3031
sharedPreferences = await SharedPreferences.getInstance();
31-
3232
setState(() {
3333
isInited = true;
3434
});
@@ -114,7 +114,7 @@ class _SettingsViewState extends State<SettingsView> {
114114
? Column(
115115
children: [
116116
const SizedBox(height: 10.0),
117-
if (Platform.isIOS)
117+
if (Platform.isIOS) ...[
118118
SwitchListTile(
119119
secondary: const Icon(Icons.voicemail),
120120
title: const Text("Transcribe Recordings"),
@@ -136,14 +136,28 @@ class _SettingsViewState extends State<SettingsView> {
136136
sharedPreferences.setBool(shouldTranscribeKey, v);
137137
});
138138
}),
139+
if (sharedPreferences.getBool(shouldTranscribeKey) ?? true)
140+
ListTile(
141+
leading: const Icon(Icons.language),
142+
trailing: const Icon(Icons.arrow_forward_ios),
143+
title: const Text("Transcription Language"),
144+
onTap: () => Navigator.push(
145+
context,
146+
MaterialPageRoute(
147+
builder: (_) =>
148+
const IOSTranscriptionSetupView())),
149+
),
150+
],
139151
if (Platform.isAndroid)
140152
ListTile(
141153
leading: const Icon(Icons.voicemail),
154+
trailing: const Icon(Icons.arrow_forward_ios),
142155
title: const Text("Transcription Setup"),
143156
onTap: () => Navigator.push(
144157
context,
145158
MaterialPageRoute(
146-
builder: (_) => const TranscriptionSetupView())),
159+
builder: (_) =>
160+
const VoskTranscriptionSetupView())),
147161
),
148162
SwitchListTile(
149163
secondary: const Icon(Icons.location_pin),

lib/views/transcription_setup_view.dart lib/views/vosk_transcription_setup_view.dart

+6-4
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import 'package:voice_outliner/consts.dart';
66
import 'package:voice_outliner/repositories/ios_speech_recognizer.dart';
77
import 'package:voice_outliner/repositories/vosk_speech_recognizer.dart';
88

9-
class TranscriptionSetupView extends StatefulWidget {
10-
const TranscriptionSetupView({Key? key}) : super(key: key);
9+
class VoskTranscriptionSetupView extends StatefulWidget {
10+
const VoskTranscriptionSetupView({Key? key}) : super(key: key);
1111

1212
@override
13-
_TranscriptionSetupViewState createState() => _TranscriptionSetupViewState();
13+
_VoskTranscriptionSetupViewState createState() =>
14+
_VoskTranscriptionSetupViewState();
1415
}
1516

16-
class _TranscriptionSetupViewState extends State<TranscriptionSetupView> {
17+
class _VoskTranscriptionSetupViewState
18+
extends State<VoskTranscriptionSetupView> {
1719
SharedPreferences? sharedPreferences;
1820
bool isInited = false;
1921
bool loading = false;

0 commit comments

Comments
 (0)