diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a74d935..1069bb9 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -50,6 +50,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/settings.gradle b/android/settings.gradle
index 1d6d19b..774d879 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -20,7 +20,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
- id "org.jetbrains.kotlin.android" version "1.7.10" apply false
+ id "org.jetbrains.kotlin.android" version "2.0.0" apply false
}
include ":app"
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 84500ba..8a2e39a 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -145,6 +145,11 @@
"description": "Title of the interface settings section",
"context": "Visible in the settings view"
},
+ "settingsTitleVoice": "Voice",
+ "@settingsTitleVoice": {
+ "description": "Title of the voice settings section. Do not translate if not required!",
+ "context": "Visible in the settings view"
+ },
"settingsTitleExport": "Exportieren",
"@settingsTitleExport": {
"description": "Title of the export settings section",
@@ -160,6 +165,36 @@
"description": "Text displayed when settings are saved automatically",
"context": "Visible in the settings view"
},
+ "settingsExperimentalAlpha": "alpha",
+ "@settingsExperimentalAlpha": {
+ "description": "Text displayed when a feature is in alpha",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalAlphaDescription": "Diese Funktion befindet sich im Alpha-Status und funktioniert möglicherweise nicht wie beabsichtigt oder erwartet.\nKritische Probleme und/oder dauerhafte kritische Schäden am Gerät und/oder den verwendeten Diensten können nicht ausgeschlossen werden.\nBenutzung auf eigene Gefahr. Keine Haftung seitens des App-Autors.",
+ "@settingsExperimentalAlphaDescription": {
+ "description": "Description of the alpha feature",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalAlphaFeature": "Alpha-Funktion, halte, um mehr zu erfahren",
+ "@settingsExperimentalAlphaFeature": {
+ "description": "Text displayed when a feature is in alpha",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalBeta": "beta",
+ "@settingsExperimentalBeta": {
+ "description": "Text displayed when a feature is in beta",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalBetaDescription": "Diese Funktion befindet sich im Beta-Test und funktioniert möglicherweise nicht wie beabsichtigt oder erwartet.\nWeniger schwerwiegende Probleme können auftreten oder auch nicht. Schäden sollten nicht kritisch sein.\nVerwendung auf eigene Gefahr.",
+ "@settingsExperimentalBetaDescription": {
+ "description": "Description of the beta feature",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalBetaFeature": "Beta-Funktion, halte, um mehr zu erfahren",
+ "@settingsExperimentalBetaFeature": {
+ "description": "Text displayed when a feature is in beta",
+ "context": "Visible in the settings view"
+ },
"settingsHost": "Host",
"@settingsHost": {
"description": "Text displayed as description for host input",
@@ -333,6 +368,41 @@
"description": "Text displayed for cancel button, should be capitalized",
"context": "Visible in the settings view"
},
+ "settingsVoicePermissionLoading": "Lade Sprachberechtigungen ...",
+ "@settingsVoicePermissionLoading": {
+ "description": "Text displayed while loading voice permissions",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoicePermissionNot": "Berechtigungen nicht erteilt",
+ "@settingsVoicePermissionNot": {
+ "description": "Text displayed when voice permissions are not granted",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoiceNotSupported": "Voice-Modus wird nicht unterstützt",
+ "@settingsVoiceNotSupported": {
+ "description": "Text displayed when voice mode is not supported",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoiceEnable": "Voice-Modus aktivieren",
+ "@settingsVoiceEnable": {
+ "description": "Text displayed as description for enable voice mode toggle",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoiceNoLanguage": "Keine Sprache ausgewählt",
+ "@settingsVoiceNoLanguage": {
+ "description": "Text displayed when no language is selected",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoiceLimitLanguage": "Auf gewählte Sprache beschränken",
+ "@settingsVoiceLimitLanguage": {
+ "description": "Text displayed as description for limit language toggle",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoicePunctuation": "KI Satzzeichen aktivieren",
+ "@settingsVoicePunctuation": {
+ "description": "Text displayed as description for enable AI punctuation toggle",
+ "context": "Visible in the settings view"
+ },
"settingsExportChats": "Chats exportieren",
"@settingsExportChats": {
"description": "Text displayed as description for export chats button",
@@ -378,11 +448,6 @@
"description": "Warning displayed for export and import options",
"context": "Visible in the settings view"
},
- "settingsCheckForUpdates": "Beim Öffnen nach Updates suchen",
- "@settingsCheckForUpdates": {
- "description": "Text displayed as description for check for updates toggle",
- "context": "Visible in the settings view"
- },
"settingsUpdateCheck": "Nach Updates suchen",
"@settingsUpdateCheck": {
"description": "Text displayed as description for check for updates button",
@@ -444,6 +509,11 @@
"description": "Text displayed for cancel button, should be capitalized",
"context": "Visible in the settings view"
},
+ "settingsCheckForUpdates": "Beim Öffnen nach Updates suchen",
+ "@settingsCheckForUpdates": {
+ "description": "Text displayed as description for check for updates toggle",
+ "context": "Visible in the settings view"
+ },
"settingsGithub": "GitHub",
"@settingsGithub": {
"description": "Text displayed as description for GitHub button",
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 3b27522..b81b6ea 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -145,6 +145,11 @@
"description": "Title of the interface settings section",
"context": "Visible in the settings view"
},
+ "settingsTitleVoice": "Voice",
+ "@settingsTitleVoice": {
+ "description": "Title of the voice settings section. Do not translate if not required!",
+ "context": "Visible in the settings view"
+ },
"settingsTitleExport": "Export",
"@settingsTitleExport": {
"description": "Title of the export settings section",
@@ -160,6 +165,36 @@
"description": "Text displayed when settings are saved automatically",
"context": "Visible in the settings view"
},
+ "settingsExperimentalAlpha": "alpha",
+ "@settingsExperimentalAlpha": {
+ "description": "Text displayed when a feature is in alpha",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalAlphaDescription": "This feature is in alpha and may not work as intended or expected.\nCritical issues and/or permanent critical damage to device and/or used services cannot be ruled out.\nUse at your own risk. No liability on the part of the app author.",
+ "@settingsExperimentalAlphaDescription": {
+ "description": "Description of the alpha feature",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalAlphaFeature": "Alpha feature, hold to learn more",
+ "@settingsExperimentalAlphaFeature": {
+ "description": "Text displayed when a feature is in alpha",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalBeta": "beta",
+ "@settingsExperimentalBeta": {
+ "description": "Text displayed when a feature is in beta",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalBetaDescription": "This feature is in beta and may not work intended or expected.\nLess severe issues may or may not occur. Damage shouldn't be critical.\nUse at your own risk.",
+ "@settingsExperimentalBetaDescription": {
+ "description": "Description of the beta feature",
+ "context": "Visible in the settings view"
+ },
+ "settingsExperimentalBetaFeature": "Beta feature, hold to learn more",
+ "@settingsExperimentalBetaFeature": {
+ "description": "Text displayed when a feature is in beta",
+ "context": "Visible in the settings view"
+ },
"settingsHost": "Host",
"@settingsHost": {
"description": "Text displayed as description for host input",
@@ -333,6 +368,41 @@
"description": "Text displayed for cancel button, should be capitalized",
"context": "Visible in the settings view"
},
+ "settingsVoicePermissionLoading": "Loading voice permissions ...",
+ "@settingsVoicePermissionLoading": {
+ "description": "Text displayed while loading voice permissions",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoicePermissionNot": "Permissions not granted",
+ "@settingsVoicePermissionNot": {
+ "description": "Text displayed when voice permissions are not granted",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoiceNotSupported": "Voice mode not supported",
+ "@settingsVoiceNotSupported": {
+ "description": "Text displayed when voice mode is not supported",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoiceEnable": "Enable voice mode",
+ "@settingsVoiceEnable": {
+ "description": "Text displayed as description for enable voice mode toggle",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoiceNoLanguage": "No language selected",
+ "@settingsVoiceNoLanguage": {
+ "description": "Text displayed when no language is selected",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoiceLimitLanguage": "Limit to selected language",
+ "@settingsVoiceLimitLanguage": {
+ "description": "Text displayed as description for limit language toggle",
+ "context": "Visible in the settings view"
+ },
+ "settingsVoicePunctuation": "Enable AI punctuation",
+ "@settingsVoicePunctuation": {
+ "description": "Text displayed as description for enable AI punctuation toggle",
+ "context": "Visible in the settings view"
+ },
"settingsExportChats": "Export chats",
"@settingsExportChats": {
"description": "Text displayed as description for export chats button",
diff --git a/lib/main.dart b/lib/main.dart
index 7905c8c..ebb7f42 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -10,9 +10,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'screen_settings.dart';
+import 'screen_voice.dart';
import 'screen_welcome.dart';
import 'worker/setter.dart';
import 'worker/haptic.dart';
+import 'worker/sender.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ignore: depend_on_referenced_packages
@@ -22,13 +24,14 @@ import 'package:uuid/uuid.dart';
import 'package:image_picker/image_picker.dart';
import 'package:visibility_detector/visibility_detector.dart';
// import 'package:http/http.dart' as http;
-import 'package:ollama_dart/ollama_dart.dart' as llama;
-import 'package:dartx/dartx.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
// ignore: depend_on_referenced_packages
import 'package:markdown/markdown.dart' as md;
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
+import 'package:speech_to_text/speech_to_text.dart';
+import 'package:flutter_tts/flutter_tts.dart';
+import 'package:permission_handler/permission_handler.dart';
// client configuration
@@ -66,6 +69,15 @@ final user = types.User(id: const Uuid().v4());
final assistant = types.User(id: const Uuid().v4());
bool settingsOpen = false;
+bool logoVisible = true;
+bool menuVisible = false;
+bool sendable = false;
+
+SpeechToText speech = SpeechToText();
+FlutterTts voice = FlutterTts();
+bool voiceSupported = false;
+
+Function? setMainState;
void main() {
runApp(const App());
@@ -101,6 +113,12 @@ class _AppState extends State {
try {
await FlutterDisplayMode.setHighRefreshRate();
} catch (_) {}
+
+ if ((await Permission.bluetoothConnect.isGranted) &&
+ (await Permission.microphone.isGranted)) {
+ voiceSupported = await speech.initialize();
+ }
+
SharedPreferences.setPrefix("ollama.");
SharedPreferences tmp = await SharedPreferences.getInstance();
setState(() {
@@ -177,11 +195,7 @@ class MainApp extends StatefulWidget {
}
class _MainAppState extends State {
- bool logoVisible = true;
- bool menuVisible = false;
-
int tipId = Random().nextInt(5);
- bool sendable = false;
List sidebar(BuildContext context, Function setState) {
return List.from([
@@ -747,18 +761,14 @@ class _MainAppState extends State {
]),
actions: [
TextButton(
- onPressed: () {
- HapticFeedback
- .selectionClick();
+ onPressed: () {selectionHaptic();
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(
context)!
.deleteDialogCancel)),
TextButton(
- onPressed: () {
- HapticFeedback
- .selectionClick();
+ onPressed: () {selectionHaptic();
Navigator.of(context).pop();
for (var i = 0;
@@ -1083,259 +1093,8 @@ class _MainAppState extends State {
child: const ImageIcon(
AssetImage("assets/logo512.png"),
size: 44)))),
- onSendPressed: (p0) async {
- selectionHaptic();
- setState(() {
- sendable = false;
- });
-
- if (host == null) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- content: Text(
- AppLocalizations.of(context)!.noHostSelected),
- showCloseIcon: true));
- return;
- }
-
- if (!chatAllowed || model == null) {
- if (model == null) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- content: Text(AppLocalizations.of(context)!
- .noModelSelected),
- showCloseIcon: true));
- }
- return;
- }
-
- bool newChat = false;
- if (chatUuid == null) {
- newChat = true;
- chatUuid = const Uuid().v4();
- prefs!.setStringList(
- "chats",
- (prefs!.getStringList("chats") ?? []).append([
- jsonEncode({
- "title": AppLocalizations.of(context)!
- .newChatTitle,
- "uuid": chatUuid,
- "messages": []
- })
- ]).toList());
- }
-
- var system = prefs?.getString("system") ??
- "You are a helpful assistant";
- if (prefs!.getBool("noMarkdown") ?? false) {
- system +=
- " You must not use markdown or any other formatting language in any way!";
- }
-
- List history = [
- llama.Message(
- role: llama.MessageRole.system, content: system)
- ];
- List images = [];
- for (var i = 0; i < messages.length; i++) {
- if (jsonDecode(jsonEncode(messages[i]))["text"] !=
- null) {
- history.add(llama.Message(
- role: (messages[i].author.id == user.id)
- ? llama.MessageRole.user
- : llama.MessageRole.system,
- content:
- jsonDecode(jsonEncode(messages[i]))["text"],
- images: (images.isNotEmpty) ? images : null));
- } else {
- var uri = jsonDecode(jsonEncode(messages[i]))["uri"]
- as String;
- String content = (uri
- .startsWith("data:image/png;base64,"))
- ? uri.removePrefix("data:image/png;base64,")
- : base64.encode(await File(uri).readAsBytes());
- uri = uri.removePrefix("data:image/png;base64,");
- images.add(content);
- }
- }
-
- history.add(llama.Message(
- role: llama.MessageRole.user,
- content: p0.text.trim(),
- images: (images.isNotEmpty) ? images : null));
- messages.insert(
- 0,
- types.TextMessage(
- author: user,
- id: const Uuid().v4(),
- text: p0.text.trim()));
-
- saveChat(chatUuid!, setState);
-
- setState(() {});
- chatAllowed = false;
-
- String newId = const Uuid().v4();
- llama.OllamaClient client = llama.OllamaClient(
- headers: (jsonDecode(
- prefs!.getString("hostHeaders") ?? "{}")
- as Map)
- .cast(),
- baseUrl: "$host/api");
-
- try {
- if ((prefs!.getString("requestType") ?? "stream") ==
- "stream") {
- final stream = client
- .generateChatCompletionStream(
- request: llama.GenerateChatCompletionRequest(
- model: model!,
- messages: history,
- keepAlive: int.parse(
- prefs!.getString("keepAlive") ??
- "300")),
- )
- .timeout(const Duration(seconds: 30));
-
- String text = "";
- await for (final res in stream) {
- text += (res.message?.content ?? "");
- for (var i = 0; i < messages.length; i++) {
- if (messages[i].id == newId) {
- messages.removeAt(i);
- break;
- }
- }
- if (chatAllowed) return;
- if (text.trim() == "") {
- throw Exception();
- }
- messages.insert(
- 0,
- types.TextMessage(
- author: assistant,
- id: newId,
- text: text));
- setState(() {});
- lightHaptic();
- }
- } else {
- llama.GenerateChatCompletionResponse request;
- request = await client
- .generateChatCompletion(
- request: llama.GenerateChatCompletionRequest(
- model: model!,
- messages: history,
- keepAlive: int.parse(
- prefs!.getString("keepAlive") ??
- "300")),
- )
- .timeout(const Duration(seconds: 30));
- if (chatAllowed) return;
- if (request.message!.content.trim() == "") {
- throw Exception();
- }
- messages.insert(
- 0,
- types.TextMessage(
- author: assistant,
- id: newId,
- text: request.message!.content));
- setState(() {});
- lightHaptic();
- }
- } catch (e) {
- for (var i = 0; i < messages.length; i++) {
- if (messages[i].id == newId) {
- messages.removeAt(i);
- break;
- }
- }
- setState(() {
- chatAllowed = true;
- messages.removeAt(0);
- if (messages.isEmpty) {
- var tmp = (prefs!.getStringList("chats") ?? []);
- chatUuid = null;
- for (var i = 0; i < tmp.length; i++) {
- if (jsonDecode((prefs!.getStringList("chats") ??
- [])[i])["uuid"] ==
- chatUuid) {
- tmp.removeAt(i);
- prefs!.setStringList("chats", tmp);
- break;
- }
- }
- }
- });
- // ignore: use_build_context_synchronously
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- // ignore: use_build_context_synchronously
- content: Text(AppLocalizations.of(context)!
- .settingsHostInvalid("timeout")),
- showCloseIcon: true));
- return;
- }
-
- saveChat(chatUuid!, setState);
-
- if (newChat &&
- (prefs!.getBool("generateTitles") ?? true)) {
- void setTitle() async {
- List