Critical chat fix, added voice mode
This commit is contained in:
parent
93818cfbad
commit
baf654be40
|
|
@ -50,6 +50,20 @@
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https"/>
|
||||||
</intent>
|
</intent>
|
||||||
<!-- check if url is valid end -->
|
<!-- check if url is valid end -->
|
||||||
|
<!-- voice mode -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.speech.RecognitionService" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||||
|
</intent>
|
||||||
|
<!-- voice mode end-->
|
||||||
</queries>
|
</queries>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<!-- voice mode -->
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||||
|
<!-- voice mode end -->
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -20,7 +20,7 @@ pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "7.3.0" apply false
|
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"
|
include ":app"
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,11 @@
|
||||||
"description": "Title of the interface settings section",
|
"description": "Title of the interface settings section",
|
||||||
"context": "Visible in the settings view"
|
"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": "Exportieren",
|
||||||
"@settingsTitleExport": {
|
"@settingsTitleExport": {
|
||||||
"description": "Title of the export settings section",
|
"description": "Title of the export settings section",
|
||||||
|
|
@ -160,6 +165,36 @@
|
||||||
"description": "Text displayed when settings are saved automatically",
|
"description": "Text displayed when settings are saved automatically",
|
||||||
"context": "Visible in the settings view"
|
"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": "Host",
|
||||||
"@settingsHost": {
|
"@settingsHost": {
|
||||||
"description": "Text displayed as description for host input",
|
"description": "Text displayed as description for host input",
|
||||||
|
|
@ -333,6 +368,41 @@
|
||||||
"description": "Text displayed for cancel button, should be capitalized",
|
"description": "Text displayed for cancel button, should be capitalized",
|
||||||
"context": "Visible in the settings view"
|
"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": "Chats exportieren",
|
||||||
"@settingsExportChats": {
|
"@settingsExportChats": {
|
||||||
"description": "Text displayed as description for export chats button",
|
"description": "Text displayed as description for export chats button",
|
||||||
|
|
@ -378,11 +448,6 @@
|
||||||
"description": "Warning displayed for export and import options",
|
"description": "Warning displayed for export and import options",
|
||||||
"context": "Visible in the settings view"
|
"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": "Nach Updates suchen",
|
||||||
"@settingsUpdateCheck": {
|
"@settingsUpdateCheck": {
|
||||||
"description": "Text displayed as description for check for updates button",
|
"description": "Text displayed as description for check for updates button",
|
||||||
|
|
@ -444,6 +509,11 @@
|
||||||
"description": "Text displayed for cancel button, should be capitalized",
|
"description": "Text displayed for cancel button, should be capitalized",
|
||||||
"context": "Visible in the settings view"
|
"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": "GitHub",
|
||||||
"@settingsGithub": {
|
"@settingsGithub": {
|
||||||
"description": "Text displayed as description for GitHub button",
|
"description": "Text displayed as description for GitHub button",
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,11 @@
|
||||||
"description": "Title of the interface settings section",
|
"description": "Title of the interface settings section",
|
||||||
"context": "Visible in the settings view"
|
"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": "Export",
|
||||||
"@settingsTitleExport": {
|
"@settingsTitleExport": {
|
||||||
"description": "Title of the export settings section",
|
"description": "Title of the export settings section",
|
||||||
|
|
@ -160,6 +165,36 @@
|
||||||
"description": "Text displayed when settings are saved automatically",
|
"description": "Text displayed when settings are saved automatically",
|
||||||
"context": "Visible in the settings view"
|
"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": "Host",
|
||||||
"@settingsHost": {
|
"@settingsHost": {
|
||||||
"description": "Text displayed as description for host input",
|
"description": "Text displayed as description for host input",
|
||||||
|
|
@ -333,6 +368,41 @@
|
||||||
"description": "Text displayed for cancel button, should be capitalized",
|
"description": "Text displayed for cancel button, should be capitalized",
|
||||||
"context": "Visible in the settings view"
|
"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": "Export chats",
|
||||||
"@settingsExportChats": {
|
"@settingsExportChats": {
|
||||||
"description": "Text displayed as description for export chats button",
|
"description": "Text displayed as description for export chats button",
|
||||||
|
|
|
||||||
353
lib/main.dart
353
lib/main.dart
|
|
@ -10,9 +10,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'screen_settings.dart';
|
import 'screen_settings.dart';
|
||||||
|
import 'screen_voice.dart';
|
||||||
import 'screen_welcome.dart';
|
import 'screen_welcome.dart';
|
||||||
import 'worker/setter.dart';
|
import 'worker/setter.dart';
|
||||||
import 'worker/haptic.dart';
|
import 'worker/haptic.dart';
|
||||||
|
import 'worker/sender.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
|
|
@ -22,13 +24,14 @@ import 'package:uuid/uuid.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
// import 'package:http/http.dart' as http;
|
// 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';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
import 'package:markdown/markdown.dart' as md;
|
import 'package:markdown/markdown.dart' as md;
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.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
|
// client configuration
|
||||||
|
|
||||||
|
|
@ -66,6 +69,15 @@ final user = types.User(id: const Uuid().v4());
|
||||||
final assistant = types.User(id: const Uuid().v4());
|
final assistant = types.User(id: const Uuid().v4());
|
||||||
|
|
||||||
bool settingsOpen = false;
|
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() {
|
void main() {
|
||||||
runApp(const App());
|
runApp(const App());
|
||||||
|
|
@ -101,6 +113,12 @@ class _AppState extends State<App> {
|
||||||
try {
|
try {
|
||||||
await FlutterDisplayMode.setHighRefreshRate();
|
await FlutterDisplayMode.setHighRefreshRate();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
if ((await Permission.bluetoothConnect.isGranted) &&
|
||||||
|
(await Permission.microphone.isGranted)) {
|
||||||
|
voiceSupported = await speech.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
SharedPreferences.setPrefix("ollama.");
|
SharedPreferences.setPrefix("ollama.");
|
||||||
SharedPreferences tmp = await SharedPreferences.getInstance();
|
SharedPreferences tmp = await SharedPreferences.getInstance();
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -177,11 +195,7 @@ class MainApp extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MainAppState extends State<MainApp> {
|
class _MainAppState extends State<MainApp> {
|
||||||
bool logoVisible = true;
|
|
||||||
bool menuVisible = false;
|
|
||||||
|
|
||||||
int tipId = Random().nextInt(5);
|
int tipId = Random().nextInt(5);
|
||||||
bool sendable = false;
|
|
||||||
|
|
||||||
List<Widget> sidebar(BuildContext context, Function setState) {
|
List<Widget> sidebar(BuildContext context, Function setState) {
|
||||||
return List.from([
|
return List.from([
|
||||||
|
|
@ -747,18 +761,14 @@ class _MainAppState extends State<MainApp> {
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {selectionHaptic();
|
||||||
HapticFeedback
|
|
||||||
.selectionClick();
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(AppLocalizations.of(
|
child: Text(AppLocalizations.of(
|
||||||
context)!
|
context)!
|
||||||
.deleteDialogCancel)),
|
.deleteDialogCancel)),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {selectionHaptic();
|
||||||
HapticFeedback
|
|
||||||
.selectionClick();
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
for (var i = 0;
|
for (var i = 0;
|
||||||
|
|
@ -1083,259 +1093,8 @@ class _MainAppState extends State<MainApp> {
|
||||||
child: const ImageIcon(
|
child: const ImageIcon(
|
||||||
AssetImage("assets/logo512.png"),
|
AssetImage("assets/logo512.png"),
|
||||||
size: 44)))),
|
size: 44)))),
|
||||||
onSendPressed: (p0) async {
|
onSendPressed: (p0) {
|
||||||
selectionHaptic();
|
send(p0.text, context, setState);
|
||||||
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<llama.Message> history = [
|
|
||||||
llama.Message(
|
|
||||||
role: llama.MessageRole.system, content: system)
|
|
||||||
];
|
|
||||||
List<String> 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<String, String>(),
|
|
||||||
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<Map<String, String>> history = [];
|
|
||||||
for (var i = 0; i < messages.length; i++) {
|
|
||||||
if (jsonDecode(jsonEncode(messages[i]))["text"] ==
|
|
||||||
null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
history.add({
|
|
||||||
"role": (messages[i].author == user)
|
|
||||||
? "user"
|
|
||||||
: "assistant",
|
|
||||||
"content":
|
|
||||||
jsonDecode(jsonEncode(messages[i]))["text"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
history = history.reversed.toList();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final generated = await client
|
|
||||||
.generateCompletion(
|
|
||||||
request: llama.GenerateCompletionRequest(
|
|
||||||
model: model!,
|
|
||||||
prompt:
|
|
||||||
"You must not use markdown or any other formatting language! Create a short title for the subject of the conversation described in the following json object. It is not allowed to be too general; no 'Assistance', 'Help' or similar!\n\n```json\n${jsonEncode(history)}\n```",
|
|
||||||
keepAlive: int.parse(
|
|
||||||
prefs!.getString("keepAlive") ??
|
|
||||||
"300")),
|
|
||||||
)
|
|
||||||
.timeout(const Duration(seconds: 10));
|
|
||||||
var title = generated.response!
|
|
||||||
.replaceAll("\"", "")
|
|
||||||
.replaceAll("'", "")
|
|
||||||
.replaceAll("*", "")
|
|
||||||
.replaceAll("_", "")
|
|
||||||
.trim();
|
|
||||||
var tmp = (prefs!.getStringList("chats") ?? []);
|
|
||||||
for (var i = 0; i < tmp.length; i++) {
|
|
||||||
if (jsonDecode((prefs!.getStringList("chats") ??
|
|
||||||
[])[i])["uuid"] ==
|
|
||||||
chatUuid) {
|
|
||||||
var tmp2 = jsonDecode(tmp[i]);
|
|
||||||
tmp2["title"] = title;
|
|
||||||
tmp[i] = jsonEncode(tmp2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prefs!.setStringList("chats", tmp);
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
setTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
chatAllowed = true;
|
|
||||||
},
|
},
|
||||||
onMessageDoubleTap: (context, p1) {
|
onMessageDoubleTap: (context, p1) {
|
||||||
selectionHaptic();
|
selectionHaptic();
|
||||||
|
|
@ -1408,7 +1167,20 @@ class _MainAppState extends State<MainApp> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
onAttachmentPressed: (!multimodal)
|
onAttachmentPressed: (!multimodal)
|
||||||
? null
|
? (prefs?.getBool("voiceModeEnabled") ?? false)
|
||||||
|
? (model != null)
|
||||||
|
? () {
|
||||||
|
selectionHaptic();
|
||||||
|
setMainState = setState;
|
||||||
|
settingsOpen = true;
|
||||||
|
logoVisible = false;
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const ScreenVoice()));
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
: null
|
||||||
: () {
|
: () {
|
||||||
selectionHaptic();
|
selectionHaptic();
|
||||||
if (!chatAllowed || model == null) return;
|
if (!chatAllowed || model == null) return;
|
||||||
|
|
@ -1455,9 +1227,28 @@ class _MainAppState extends State<MainApp> {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {selectionHaptic();
|
||||||
HapticFeedback
|
Navigator.of(context)
|
||||||
.selectionClick();
|
.pop();
|
||||||
|
setMainState = setState;
|
||||||
|
settingsOpen = true;
|
||||||
|
logoVisible = false;
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
const ScreenVoice()));
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons
|
||||||
|
.headphones_rounded),
|
||||||
|
label: Text(AppLocalizations
|
||||||
|
.of(context)!
|
||||||
|
.settingsTitleVoice))),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () async {selectionHaptic();
|
||||||
|
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pop();
|
.pop();
|
||||||
|
|
@ -1497,8 +1288,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
messages.insert(
|
messages.insert(
|
||||||
0, message);
|
0, message);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
HapticFeedback
|
selectionHaptic();
|
||||||
.selectionClick();
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons
|
icon: const Icon(Icons
|
||||||
.photo_camera_rounded),
|
.photo_camera_rounded),
|
||||||
|
|
@ -1510,9 +1300,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {selectionHaptic();
|
||||||
HapticFeedback
|
|
||||||
.selectionClick();
|
|
||||||
|
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pop();
|
.pop();
|
||||||
|
|
@ -1552,8 +1340,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
messages.insert(
|
messages.insert(
|
||||||
0, message);
|
0, message);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
HapticFeedback
|
selectionHaptic();
|
||||||
.selectionClick();
|
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.image_rounded),
|
Icons.image_rounded),
|
||||||
|
|
@ -1589,8 +1376,12 @@ class _MainAppState extends State<MainApp> {
|
||||||
(theme ?? ThemeData()).colorScheme.surface,
|
(theme ?? ThemeData()).colorScheme.surface,
|
||||||
primaryColor:
|
primaryColor:
|
||||||
(theme ?? ThemeData()).colorScheme.primary,
|
(theme ?? ThemeData()).colorScheme.primary,
|
||||||
attachmentButtonIcon:
|
attachmentButtonIcon: !multimodal
|
||||||
const Icon(Icons.add_a_photo_rounded),
|
? (prefs?.getBool("voiceModeEnabled") ??
|
||||||
|
false)
|
||||||
|
? const Icon(Icons.headphones_rounded)
|
||||||
|
: null
|
||||||
|
: const Icon(Icons.add_a_photo_rounded),
|
||||||
sendButtonIcon: SizedBox(
|
sendButtonIcon: SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
|
|
@ -1635,7 +1426,11 @@ class _MainAppState extends State<MainApp> {
|
||||||
backgroundColor: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
backgroundColor: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||||
primaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(40),
|
primaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(40),
|
||||||
secondaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(20),
|
secondaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(20),
|
||||||
attachmentButtonIcon: const Icon(Icons.add_a_photo_rounded),
|
attachmentButtonIcon: !multimodal
|
||||||
|
? (prefs?.getBool("voiceModeEnabled") ?? false)
|
||||||
|
? const Icon(Icons.headphones_rounded)
|
||||||
|
: null
|
||||||
|
: const Icon(Icons.add_a_photo_rounded),
|
||||||
sendButtonIcon: SizedBox(
|
sendButtonIcon: SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'settings/behavior.dart';
|
import 'settings/behavior.dart';
|
||||||
import 'settings/interface.dart';
|
import 'settings/interface.dart';
|
||||||
|
import 'settings/voice.dart';
|
||||||
import 'settings/export.dart';
|
import 'settings/export.dart';
|
||||||
import 'settings/about.dart';
|
import 'settings/about.dart';
|
||||||
|
|
||||||
|
|
@ -18,7 +19,11 @@ import 'package:http/http.dart' as http;
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
|
|
||||||
Widget toggle(BuildContext context, String text, bool value,
|
Widget toggle(BuildContext context, String text, bool value,
|
||||||
Function(bool value) onChanged) {
|
Function(bool value) onChanged,
|
||||||
|
{bool disabled = false,
|
||||||
|
void Function()? onDisabledTap,
|
||||||
|
void Function()? onLongTap,
|
||||||
|
void Function()? onDoubleTap}) {
|
||||||
var space = ""; // Invisible character: U+2063
|
var space = ""; // Invisible character: U+2063
|
||||||
var spacePlus = " $space";
|
var spacePlus = " $space";
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
|
@ -26,8 +31,17 @@ Widget toggle(BuildContext context, String text, bool value,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onChanged(!value);
|
if (disabled) {
|
||||||
|
selectionHaptic();
|
||||||
|
if (onDisabledTap != null) {
|
||||||
|
onDisabledTap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onChanged(!value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
onLongPress: onLongTap,
|
||||||
|
onDoubleTap: onDoubleTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 4, bottom: 4),
|
padding: const EdgeInsets.only(top: 4, bottom: 4),
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
|
|
@ -43,6 +57,7 @@ Widget toggle(BuildContext context, String text, bool value,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
color: disabled ? Colors.grey : null,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
(Theme.of(context).brightness == Brightness.light)
|
(Theme.of(context).brightness == Brightness.light)
|
||||||
? (theme ?? ThemeData()).colorScheme.surface
|
? (theme ?? ThemeData()).colorScheme.surface
|
||||||
|
|
@ -56,7 +71,22 @@ Widget toggle(BuildContext context, String text, bool value,
|
||||||
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Switch(value: value, onChanged: onChanged)))
|
child: Switch(
|
||||||
|
value: value,
|
||||||
|
onChanged: disabled
|
||||||
|
? (p0) {
|
||||||
|
selectionHaptic();
|
||||||
|
if (onDisabledTap != null) {
|
||||||
|
onDisabledTap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: onChanged,
|
||||||
|
trackOutlineColor: disabled
|
||||||
|
? const WidgetStatePropertyAll(Colors.grey)
|
||||||
|
: null,
|
||||||
|
thumbColor: disabled
|
||||||
|
? const WidgetStatePropertyAll(Colors.grey)
|
||||||
|
: null)))
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|
@ -86,17 +116,36 @@ Widget titleDivider({double top = 16, double bottom = 16}) {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget button(String text, IconData icon, void Function()? onPressed,
|
Widget button(String text, IconData? icon, void Function()? onPressed,
|
||||||
{Color? color}) {
|
{Color? color,
|
||||||
|
bool disabled = false,
|
||||||
|
void Function()? onDisabledTap,
|
||||||
|
void Function()? onLongTap,
|
||||||
|
void Function()? onDoubleTap}) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onPressed,
|
onTap: disabled
|
||||||
|
? () {
|
||||||
|
selectionHaptic();
|
||||||
|
if (onDisabledTap != null) {
|
||||||
|
onDisabledTap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: onPressed,
|
||||||
|
onLongPress: onLongTap,
|
||||||
|
onDoubleTap: onDoubleTap,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Icon(icon, color: color),
|
(icon != null)
|
||||||
const SizedBox(width: 16, height: 42),
|
? Icon(icon, color: disabled ? Colors.grey : color)
|
||||||
Expanded(child: Text(text, style: TextStyle(color: color)))
|
: const SizedBox.shrink(),
|
||||||
|
(icon != null)
|
||||||
|
? const SizedBox(width: 16, height: 42)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
Expanded(
|
||||||
|
child: Text(text,
|
||||||
|
style: TextStyle(color: disabled ? Colors.grey : color)))
|
||||||
]),
|
]),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -389,6 +438,21 @@ class _ScreenSettingsState extends State<ScreenSettings> {
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
const ScreenSettingsInterface()));
|
const ScreenSettingsInterface()));
|
||||||
}),
|
}),
|
||||||
|
(!(Platform.isWindows ||
|
||||||
|
Platform.isLinux ||
|
||||||
|
Platform.isMacOS))
|
||||||
|
? button(
|
||||||
|
AppLocalizations.of(context)!
|
||||||
|
.settingsTitleVoice,
|
||||||
|
Icons.headphones_rounded, () {
|
||||||
|
selectionHaptic();
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const ScreenSettingsVoice()));
|
||||||
|
})
|
||||||
|
: const SizedBox.shrink(),
|
||||||
button(
|
button(
|
||||||
AppLocalizations.of(context)!.settingsTitleExport,
|
AppLocalizations.of(context)!.settingsTitleExport,
|
||||||
Icons.share_rounded, () {
|
Icons.share_rounded, () {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,389 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:ollama_app/worker/haptic.dart';
|
||||||
|
import 'package:ollama_app/worker/setter.dart';
|
||||||
|
|
||||||
|
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
||||||
|
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||||
|
import 'package:datetime_loop/datetime_loop.dart';
|
||||||
|
|
||||||
|
import 'main.dart';
|
||||||
|
import 'worker/sender.dart';
|
||||||
|
import 'settings/voice.dart';
|
||||||
|
|
||||||
|
class ScreenVoice extends StatefulWidget {
|
||||||
|
const ScreenVoice({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ScreenVoice> createState() => _ScreenVoiceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
|
Iterable<String> languageOptionIds = [];
|
||||||
|
Iterable<String> languageOptions = [];
|
||||||
|
|
||||||
|
bool speaking = false;
|
||||||
|
bool aiThinking = false;
|
||||||
|
|
||||||
|
bool sttDone = true;
|
||||||
|
String text = "";
|
||||||
|
String aiText = "";
|
||||||
|
|
||||||
|
bool intendedStop = false;
|
||||||
|
|
||||||
|
void setBrightness() {
|
||||||
|
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
||||||
|
() {
|
||||||
|
// invert colors used, because brightness not updated yet
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor:
|
||||||
|
(prefs!.getString("brightness") ?? "system") == "system"
|
||||||
|
? ((MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light)
|
||||||
|
? (themeDark ?? ThemeData.dark()).colorScheme.surface
|
||||||
|
: (theme ?? ThemeData()).colorScheme.surface)
|
||||||
|
: (prefs!.getString("brightness") == "dark"
|
||||||
|
? (themeDark ?? ThemeData()).colorScheme.surface
|
||||||
|
: (theme ?? ThemeData.dark()).colorScheme.surface),
|
||||||
|
systemNavigationBarIconBrightness:
|
||||||
|
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
||||||
|
MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.dark) ||
|
||||||
|
prefs!.getString("brightness") == "light")
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light));
|
||||||
|
};
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor:
|
||||||
|
(prefs!.getString("brightness") ?? "system") == "system"
|
||||||
|
? ((MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light)
|
||||||
|
? (theme ?? ThemeData.dark()).colorScheme.surface
|
||||||
|
: (themeDark ?? ThemeData()).colorScheme.surface)
|
||||||
|
: (prefs!.getString("brightness") == "dark"
|
||||||
|
? (themeDark ?? ThemeData()).colorScheme.surface
|
||||||
|
: (theme ?? ThemeData.dark()).colorScheme.surface),
|
||||||
|
systemNavigationBarIconBrightness:
|
||||||
|
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
||||||
|
MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light) ||
|
||||||
|
prefs!.getString("brightness") == "light")
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light));
|
||||||
|
}
|
||||||
|
|
||||||
|
void process() async {
|
||||||
|
setState(() {
|
||||||
|
speaking = true;
|
||||||
|
sttDone = false;
|
||||||
|
});
|
||||||
|
var textOldOld = text;
|
||||||
|
var textOld = "";
|
||||||
|
text = "";
|
||||||
|
|
||||||
|
speech.listen(
|
||||||
|
localeId: (prefs!.getString("voiceLanguage") ?? ""),
|
||||||
|
listenOptions:
|
||||||
|
stt.SpeechListenOptions(listenMode: stt.ListenMode.dictation),
|
||||||
|
onResult: (result) {
|
||||||
|
lightHaptic();
|
||||||
|
if (!speaking) return;
|
||||||
|
setState(() {
|
||||||
|
sttDone = result.finalResult;
|
||||||
|
text = result.recognizedWords;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pauseFor: const Duration(seconds: 3));
|
||||||
|
|
||||||
|
DateTime start = DateTime.now();
|
||||||
|
bool timeout = false;
|
||||||
|
await Future.doWhile(() =>
|
||||||
|
Future.delayed(const Duration(milliseconds: 1)).then((_) {
|
||||||
|
if (textOld != text) {
|
||||||
|
start = DateTime.now();
|
||||||
|
}
|
||||||
|
timeout =
|
||||||
|
(DateTime.now().difference(start) >= const Duration(seconds: 3));
|
||||||
|
textOld = text;
|
||||||
|
return !sttDone && speaking && !timeout;
|
||||||
|
}));
|
||||||
|
if (!sttDone || timeout) {
|
||||||
|
sttDone = true;
|
||||||
|
speech.stop();
|
||||||
|
if (timeout) {
|
||||||
|
text = textOldOld;
|
||||||
|
try {
|
||||||
|
setState(() {});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
if (!intendedStop) {
|
||||||
|
speaking = false;
|
||||||
|
try {
|
||||||
|
setState(() {});
|
||||||
|
} catch (_) {}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
intendedStop = false;
|
||||||
|
try {
|
||||||
|
setState(() {});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
speaking = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aiText = "";
|
||||||
|
heavyHaptic();
|
||||||
|
|
||||||
|
aiThinking = true;
|
||||||
|
try {
|
||||||
|
if (prefs!.getBool("aiPunctuation") ?? true) {
|
||||||
|
final generated = await llama.OllamaClient(
|
||||||
|
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
|
||||||
|
.cast<String, String>(),
|
||||||
|
baseUrl: "$host/api",
|
||||||
|
)
|
||||||
|
.generateCompletion(
|
||||||
|
request: llama.GenerateCompletionRequest(
|
||||||
|
model: model!,
|
||||||
|
prompt:
|
||||||
|
"Add punctuation and syntax to the following sentence. You must not change order of words or a word in itself! You must not add any word or phrase or remove one! Do not change between formal and personal form, keep the original one!\n\n$text",
|
||||||
|
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
|
||||||
|
)
|
||||||
|
.timeout(const Duration(seconds: 10));
|
||||||
|
setState(() {
|
||||||
|
text = generated.response!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
send(text, context, setState, onStream: (currentText, done) async {
|
||||||
|
setState(() {
|
||||||
|
aiText = currentText;
|
||||||
|
lightHaptic();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (done &&
|
||||||
|
(await voice.getLanguages as List).contains(
|
||||||
|
(prefs!.getString("voiceLanguage") ?? "en_US")
|
||||||
|
.replaceAll("_", "-"))) {
|
||||||
|
aiThinking = false;
|
||||||
|
heavyHaptic();
|
||||||
|
voice.setLanguage((prefs!.getString("voiceLanguage") ?? "en_US")
|
||||||
|
.replaceAll("_", "-"));
|
||||||
|
voice.setSpeechRate(0.6);
|
||||||
|
voice.setCompletionHandler(() async {
|
||||||
|
speaking = false;
|
||||||
|
try {
|
||||||
|
setState(() {});
|
||||||
|
} catch (_) {}
|
||||||
|
process();
|
||||||
|
});
|
||||||
|
var tmp = aiText;
|
||||||
|
tmp.replaceAll("-", ".");
|
||||||
|
tmp.replaceAll("*", ".");
|
||||||
|
voice.speak(tmp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addToSystem: (prefs!.getBool("voiceLimitLanguage") ?? true)
|
||||||
|
? "You must write in the following language: ${prefs!.getString("voiceLanguage") ?? "en_US"}!"
|
||||||
|
: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
||||||
|
() {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor:
|
||||||
|
(themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||||
|
systemNavigationBarIconBrightness: Brightness.dark));
|
||||||
|
};
|
||||||
|
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor:
|
||||||
|
(themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||||
|
systemNavigationBarIconBrightness: Brightness.dark));
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
|
||||||
|
void load() async {
|
||||||
|
var tmp = await speech.locales();
|
||||||
|
languageOptionIds = tmp.map((e) => e.localeId);
|
||||||
|
languageOptions = tmp.map((e) => e.name);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
void loadProcess() async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
process();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Theme(
|
||||||
|
data: themeDark!,
|
||||||
|
child: PopScope(
|
||||||
|
canPop: !aiThinking,
|
||||||
|
onPopInvoked: (didPop) {
|
||||||
|
speaking = false;
|
||||||
|
voice.stop();
|
||||||
|
if (chatUuid != null) {
|
||||||
|
loadChat(chatUuid!, setMainState!);
|
||||||
|
}
|
||||||
|
settingsOpen = false;
|
||||||
|
logoVisible = true;
|
||||||
|
setBrightness();
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
speaking = false;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close_rounded,
|
||||||
|
color: Colors.grey)),
|
||||||
|
title: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(model!.split(":")[0],
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: "monospace", fontSize: 16))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
speaking = false;
|
||||||
|
settingsOpen = false;
|
||||||
|
logoVisible = true;
|
||||||
|
setBrightness();
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const ScreenSettingsVoice()));
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.settings_rounded,
|
||||||
|
color: Colors.grey,
|
||||||
|
))
|
||||||
|
]),
|
||||||
|
body: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.max, children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
|
child: Center(
|
||||||
|
child: Text(text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontFamily: "monospace"))),
|
||||||
|
))
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: DateTimeLoopBuilder(
|
||||||
|
timeUnit: TimeUnit.seconds,
|
||||||
|
builder: (context, dateTime, child) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 96,
|
||||||
|
width: 96,
|
||||||
|
child: AnimatedScale(
|
||||||
|
scale: speaking
|
||||||
|
? aiThinking
|
||||||
|
? (dateTime.second).isEven
|
||||||
|
? 2.4
|
||||||
|
: 2
|
||||||
|
: 2
|
||||||
|
: dateTime.second
|
||||||
|
.toString()
|
||||||
|
.endsWith("1")
|
||||||
|
? 1.6
|
||||||
|
: 1.4,
|
||||||
|
duration: aiThinking
|
||||||
|
? const Duration(seconds: 1)
|
||||||
|
: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(48),
|
||||||
|
onTap: () {
|
||||||
|
if (speaking && !aiThinking) {
|
||||||
|
intendedStop = true;
|
||||||
|
speaking = false;
|
||||||
|
voice.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process();
|
||||||
|
},
|
||||||
|
child: CircleAvatar(
|
||||||
|
backgroundColor: themeDark!
|
||||||
|
.colorScheme.primary
|
||||||
|
.withAlpha(
|
||||||
|
!speaking ? 200 : 255),
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(
|
||||||
|
milliseconds: 200),
|
||||||
|
child: speaking
|
||||||
|
? aiThinking
|
||||||
|
? Icon(Icons.auto_awesome_rounded,
|
||||||
|
color: themeDark!
|
||||||
|
.colorScheme
|
||||||
|
.secondary,
|
||||||
|
key: const ValueKey(
|
||||||
|
"aiThinking"))
|
||||||
|
: sttDone
|
||||||
|
? Icon(Icons.volume_up_rounded,
|
||||||
|
color: themeDark!
|
||||||
|
.colorScheme
|
||||||
|
.secondary,
|
||||||
|
key: const ValueKey(
|
||||||
|
"tts"))
|
||||||
|
: Icon(Icons.mic_rounded, color: themeDark!.colorScheme.secondary, key: const ValueKey("stt"))
|
||||||
|
: null)))),
|
||||||
|
);
|
||||||
|
}))),
|
||||||
|
Expanded(
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.max, children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
|
child: Center(
|
||||||
|
child: Text(aiText,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: "monospace"))),
|
||||||
|
))
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -106,13 +106,50 @@ class _ScreenWelcomeState extends State<ScreenWelcome> {
|
||||||
curve: Curves.easeInOut);
|
curve: Curves.easeInOut);
|
||||||
} else {
|
} else {
|
||||||
prefs!.setBool("welcomeFinished", true);
|
prefs!.setBool("welcomeFinished", true);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.platformDispatcher
|
||||||
|
.onPlatformBrightnessChanged = () {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor:
|
||||||
|
(prefs!.getString("brightness") ?? "system") == "system"
|
||||||
|
? ((MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light)
|
||||||
|
? (themeDark ?? ThemeData.dark())
|
||||||
|
.colorScheme
|
||||||
|
.surface
|
||||||
|
: (theme ?? ThemeData()).colorScheme.surface)
|
||||||
|
: (prefs!.getString("brightness") == "dark"
|
||||||
|
? (themeDark ?? ThemeData()).colorScheme.surface
|
||||||
|
: (theme ?? ThemeData.dark())
|
||||||
|
.colorScheme
|
||||||
|
.surface),
|
||||||
|
systemNavigationBarIconBrightness:
|
||||||
|
(((prefs!.getString("brightness") ?? "system") ==
|
||||||
|
"system" &&
|
||||||
|
MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.dark) ||
|
||||||
|
prefs!.getString("brightness") == "light")
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light));
|
||||||
|
};
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
systemNavigationBarColor:
|
systemNavigationBarColor:
|
||||||
(Theme.of(context).brightness == Brightness.light)
|
(prefs!.getString("brightness") ?? "system") == "system"
|
||||||
? (theme ?? ThemeData()).colorScheme.surface
|
? ((MediaQuery.of(context).platformBrightness ==
|
||||||
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
Brightness.light)
|
||||||
|
? (theme ?? ThemeData.dark()).colorScheme.surface
|
||||||
|
: (themeDark ?? ThemeData()).colorScheme.surface)
|
||||||
|
: (prefs!.getString("brightness") == "dark"
|
||||||
|
? (themeDark ?? ThemeData()).colorScheme.surface
|
||||||
|
: (theme ?? ThemeData.dark())
|
||||||
|
.colorScheme
|
||||||
|
.surface),
|
||||||
systemNavigationBarIconBrightness:
|
systemNavigationBarIconBrightness:
|
||||||
(Theme.of(context).brightness == Brightness.light)
|
(((prefs!.getString("brightness") ?? "system") ==
|
||||||
|
"system" &&
|
||||||
|
MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light) ||
|
||||||
|
prefs!.getString("brightness") == "light")
|
||||||
? Brightness.dark
|
? Brightness.dark
|
||||||
: Brightness.light));
|
: Brightness.light));
|
||||||
Navigator.pushReplacement(context,
|
Navigator.pushReplacement(context,
|
||||||
|
|
|
||||||
|
|
@ -340,8 +340,7 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback
|
selectionHaptic();
|
||||||
.selectionClick();
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(AppLocalizations.of(
|
child: Text(AppLocalizations.of(
|
||||||
|
|
@ -349,8 +348,7 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
||||||
.settingsBrightnessRestartCancel)),
|
.settingsBrightnessRestartCancel)),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
HapticFeedback
|
selectionHaptic();
|
||||||
.selectionClick();
|
|
||||||
await prefs!.setString(
|
await prefs!.setString(
|
||||||
"brightness",
|
"brightness",
|
||||||
p0.elementAt(0));
|
p0.elementAt(0));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,342 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:ollama_app/worker/haptic.dart';
|
||||||
|
|
||||||
|
import '../main.dart';
|
||||||
|
// import '../worker/haptic.dart';
|
||||||
|
import '../screen_settings.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
class ScreenSettingsVoice extends StatefulWidget {
|
||||||
|
const ScreenSettingsVoice({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ScreenSettingsVoice> createState() => _ScreenSettingsVoiceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScreenSettingsVoiceState extends State<ScreenSettingsVoice> {
|
||||||
|
bool permissionLoading = true;
|
||||||
|
bool permissionRecord = false;
|
||||||
|
bool permissionBluetooth = false;
|
||||||
|
|
||||||
|
Iterable<String> languageOptionIds = [];
|
||||||
|
Iterable<String> languageOptions = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
void load() async {
|
||||||
|
var tmp = await speech.locales();
|
||||||
|
languageOptionIds = tmp.map((e) => e.localeId);
|
||||||
|
languageOptions = tmp.map((e) => e.name);
|
||||||
|
|
||||||
|
permissionRecord = await Permission.microphone.isGranted;
|
||||||
|
permissionBluetooth = await Permission.bluetoothConnect.isGranted;
|
||||||
|
permissionLoading = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WindowBorder(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context)!.settingsTitleVoice)),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
|
child: Column(children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView(children: [
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
((prefs!.getBool("voiceModeEnabled") ?? false) ||
|
||||||
|
permissionLoading ||
|
||||||
|
(permissionBluetooth &&
|
||||||
|
permissionRecord &&
|
||||||
|
voiceSupported))
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: button(
|
||||||
|
permissionLoading
|
||||||
|
? AppLocalizations.of(context)!
|
||||||
|
.settingsVoicePermissionLoading
|
||||||
|
: (permissionBluetooth && permissionRecord)
|
||||||
|
? AppLocalizations.of(context)!
|
||||||
|
.settingsVoiceNotSupported
|
||||||
|
: AppLocalizations.of(context)!
|
||||||
|
.settingsVoicePermissionNot,
|
||||||
|
Icons.info_rounded, () {
|
||||||
|
if (permissionLoading) return;
|
||||||
|
if (!(permissionBluetooth && permissionRecord)) {
|
||||||
|
void load() async {
|
||||||
|
try {
|
||||||
|
if (await Permission
|
||||||
|
.bluetooth.isPermanentlyDenied ||
|
||||||
|
await Permission
|
||||||
|
.microphone.isPermanentlyDenied) {
|
||||||
|
await openAppSettings();
|
||||||
|
}
|
||||||
|
permissionRecord = await Permission.microphone
|
||||||
|
.request()
|
||||||
|
.isGranted;
|
||||||
|
permissionBluetooth = await Permission
|
||||||
|
.bluetoothConnect
|
||||||
|
.request()
|
||||||
|
.isGranted;
|
||||||
|
permissionLoading = false;
|
||||||
|
|
||||||
|
if (permissionBluetooth && permissionRecord) {
|
||||||
|
voiceSupported = await speech.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
} catch (_) {
|
||||||
|
permissionLoading = false;
|
||||||
|
try {
|
||||||
|
setState(() {});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
toggle(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.settingsVoiceEnable,
|
||||||
|
(prefs!.getBool("voiceModeEnabled") ?? false), (value) {
|
||||||
|
selectionHaptic();
|
||||||
|
prefs!.setBool("voiceModeEnabled", value);
|
||||||
|
setState(() {});
|
||||||
|
}, disabled: !voiceSupported),
|
||||||
|
button(
|
||||||
|
((prefs!.getString("voiceLanguage") ?? "") == "" ||
|
||||||
|
languageOptions.isEmpty)
|
||||||
|
? AppLocalizations.of(context)!
|
||||||
|
.settingsVoiceNoLanguage
|
||||||
|
: () {
|
||||||
|
for (int i = 0;
|
||||||
|
i < languageOptionIds.length;
|
||||||
|
i++) {
|
||||||
|
if (languageOptionIds.elementAt(i) ==
|
||||||
|
prefs!.getString("voiceLanguage")) {
|
||||||
|
return languageOptions.elementAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}(),
|
||||||
|
Icons.language_rounded, () {
|
||||||
|
int usedIndex = -1;
|
||||||
|
Function? setModalState;
|
||||||
|
void load() async {
|
||||||
|
var tmp = await speech.locales();
|
||||||
|
languageOptionIds = tmp.map((e) => e.localeId);
|
||||||
|
languageOptions = tmp.map((e) => e.name);
|
||||||
|
|
||||||
|
if ((prefs!.getString("voiceLanguage") ?? "") != "") {
|
||||||
|
for (int i = 0; i < languageOptionIds.length; i++) {
|
||||||
|
if (languageOptionIds.elementAt(i) ==
|
||||||
|
(prefs!.getString("voiceLanguage") ?? "")) {
|
||||||
|
usedIndex = i;
|
||||||
|
setModalState!(() {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionHaptic();
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => StatefulBuilder(
|
||||||
|
builder: (context, setLocalState) {
|
||||||
|
setModalState = setLocalState;
|
||||||
|
return PopScope(
|
||||||
|
onPopInvoked: (didPop) {
|
||||||
|
if (usedIndex == -1) return;
|
||||||
|
prefs!.setString(
|
||||||
|
"voiceLanguage",
|
||||||
|
languageOptionIds
|
||||||
|
.elementAt(usedIndex));
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: ((Platform.isWindows ||
|
||||||
|
Platform.isLinux ||
|
||||||
|
Platform.isMacOS) &&
|
||||||
|
MediaQuery.of(context)
|
||||||
|
.size
|
||||||
|
.width >=
|
||||||
|
1000)
|
||||||
|
? null
|
||||||
|
: double.infinity,
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: (Platform.isWindows ||
|
||||||
|
Platform.isLinux ||
|
||||||
|
Platform.isMacOS)
|
||||||
|
? 16
|
||||||
|
: 0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: ((Platform
|
||||||
|
.isWindows ||
|
||||||
|
Platform
|
||||||
|
.isLinux ||
|
||||||
|
Platform
|
||||||
|
.isMacOS) &&
|
||||||
|
MediaQuery.of(
|
||||||
|
context)
|
||||||
|
.size
|
||||||
|
.width >=
|
||||||
|
1000)
|
||||||
|
? 300
|
||||||
|
: double.infinity,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight:
|
||||||
|
MediaQuery.of(
|
||||||
|
context)
|
||||||
|
.size
|
||||||
|
.height *
|
||||||
|
0.4),
|
||||||
|
child:
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection:
|
||||||
|
Axis.vertical,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: ((Platform.isWindows ||
|
||||||
|
Platform
|
||||||
|
.isLinux ||
|
||||||
|
Platform
|
||||||
|
.isMacOS) &&
|
||||||
|
MediaQuery.of(context).size.width >=
|
||||||
|
1000)
|
||||||
|
? 10.0
|
||||||
|
: 5.0,
|
||||||
|
runSpacing: (Platform.isWindows ||
|
||||||
|
Platform
|
||||||
|
.isLinux ||
|
||||||
|
Platform
|
||||||
|
.isMacOS)
|
||||||
|
? (MediaQuery.of(context).size.width >=
|
||||||
|
1000)
|
||||||
|
? 10.0
|
||||||
|
: 5.0
|
||||||
|
: 0.0,
|
||||||
|
alignment:
|
||||||
|
WrapAlignment
|
||||||
|
.center,
|
||||||
|
children: List<
|
||||||
|
Widget>.generate(
|
||||||
|
languageOptionIds
|
||||||
|
.length,
|
||||||
|
(int index) {
|
||||||
|
return ChoiceChip(
|
||||||
|
label: Text(
|
||||||
|
languageOptions
|
||||||
|
.elementAt(index)),
|
||||||
|
selected:
|
||||||
|
usedIndex ==
|
||||||
|
index,
|
||||||
|
avatar: (usedIndex ==
|
||||||
|
index)
|
||||||
|
? null
|
||||||
|
: (languageOptionIds.elementAt(index).startsWith(AppLocalizations.of(context)!.localeName))
|
||||||
|
? const Icon(Icons.star_rounded)
|
||||||
|
: null,
|
||||||
|
checkmarkColor: (usedIndex ==
|
||||||
|
index)
|
||||||
|
? ((MediaQuery.of(context).platformBrightness == Brightness.light)
|
||||||
|
? (theme ?? ThemeData()).colorScheme.secondary
|
||||||
|
: (themeDark ?? ThemeData.dark()).colorScheme.secondary)
|
||||||
|
: null,
|
||||||
|
labelStyle: (usedIndex ==
|
||||||
|
index)
|
||||||
|
? TextStyle(
|
||||||
|
color: (MediaQuery.of(context).platformBrightness == Brightness.light) ? (theme ?? ThemeData()).colorScheme.secondary : (themeDark ?? ThemeData.dark()).colorScheme.secondary)
|
||||||
|
: null,
|
||||||
|
selectedColor: (MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness
|
||||||
|
.light)
|
||||||
|
? (theme ?? ThemeData())
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
: (themeDark ?? ThemeData.dark())
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
onSelected:
|
||||||
|
(bool
|
||||||
|
selected) {
|
||||||
|
selectionHaptic();
|
||||||
|
setLocalState(
|
||||||
|
() {
|
||||||
|
usedIndex = selected
|
||||||
|
? index
|
||||||
|
: -1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
)))
|
||||||
|
])));
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
disabled: (!voiceSupported ||
|
||||||
|
!(prefs!.getBool("voiceModeEnabled") ?? false))),
|
||||||
|
titleDivider(),
|
||||||
|
toggle(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!
|
||||||
|
.settingsVoiceLimitLanguage,
|
||||||
|
(prefs!.getBool("voiceLimitLanguage") ?? true),
|
||||||
|
(value) {
|
||||||
|
selectionHaptic();
|
||||||
|
prefs!.setBool("voiceLimitLanguage", value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
disabled: (!voiceSupported ||
|
||||||
|
!(prefs!.getBool("voiceModeEnabled") ?? false))),
|
||||||
|
toggle(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.settingsVoicePunctuation,
|
||||||
|
(prefs!.getBool("aiPunctuation") ?? true), (value) {
|
||||||
|
selectionHaptic();
|
||||||
|
prefs!.setBool("aiPunctuation", value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
disabled: (!voiceSupported ||
|
||||||
|
!(prefs!.getBool("voiceModeEnabled") ?? false)))
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
button(
|
||||||
|
AppLocalizations.of(context)!
|
||||||
|
.settingsExperimentalBetaFeature,
|
||||||
|
Icons.warning_rounded,
|
||||||
|
null,
|
||||||
|
color: Colors.orange, onLongTap: () {
|
||||||
|
selectionHaptic();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context)!
|
||||||
|
.settingsExperimentalBetaDescription),
|
||||||
|
showCloseIcon: true));
|
||||||
|
})
|
||||||
|
]))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,5 +18,7 @@ void heavyHaptic() {
|
||||||
|
|
||||||
void selectionHaptic() {
|
void selectionHaptic() {
|
||||||
if (!(prefs!.getBool("enableHaptic") ?? true)) return;
|
if (!(prefs!.getBool("enableHaptic") ?? true)) return;
|
||||||
HapticFeedback.selectionClick();
|
// same name but for better experience, change behavior
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
// HapticFeedback.selectionClick();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'haptic.dart';
|
||||||
|
import 'setter.dart';
|
||||||
|
import '../main.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||||
|
import 'package:dartx/dartx.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
// ignore: depend_on_referenced_packages
|
||||||
|
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||||
|
|
||||||
|
List<String> images = [];
|
||||||
|
Future<List<llama.Message>> getHistory([String? addToSystem]) async {
|
||||||
|
var system = prefs?.getString("system") ?? "You are a helpful assistant";
|
||||||
|
if (prefs!.getBool("noMarkdown") ?? false) {
|
||||||
|
system +=
|
||||||
|
"\nYou must not use markdown or any other formatting language in any way!";
|
||||||
|
}
|
||||||
|
if (addToSystem != null) {
|
||||||
|
system += "\n$addToSystem";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<llama.Message> history = [
|
||||||
|
llama.Message(role: llama.MessageRole.system, content: system)
|
||||||
|
];
|
||||||
|
List<llama.Message> history2 = [];
|
||||||
|
images = [];
|
||||||
|
for (var i = 0; i < messages.length; i++) {
|
||||||
|
if (jsonDecode(jsonEncode(messages[i]))["text"] != null) {
|
||||||
|
history2.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));
|
||||||
|
images = [];
|
||||||
|
} 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.addAll(history2.reversed.toList());
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> send(String value, BuildContext context, Function setState,
|
||||||
|
{void Function(String currentText, bool done)? onStream,
|
||||||
|
String? addToSystem}) 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 history = await getHistory(addToSystem);
|
||||||
|
|
||||||
|
history.add(llama.Message(
|
||||||
|
role: llama.MessageRole.user,
|
||||||
|
content: value.trim(),
|
||||||
|
images: (images.isNotEmpty) ? images : null));
|
||||||
|
messages.insert(
|
||||||
|
0,
|
||||||
|
types.TextMessage(
|
||||||
|
author: user, id: const Uuid().v4(), text: value.trim()));
|
||||||
|
|
||||||
|
saveChat(chatUuid!, setState);
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
chatAllowed = false;
|
||||||
|
|
||||||
|
String text = "";
|
||||||
|
|
||||||
|
String newId = const Uuid().v4();
|
||||||
|
llama.OllamaClient client = llama.OllamaClient(
|
||||||
|
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
|
||||||
|
.cast<String, String>(),
|
||||||
|
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));
|
||||||
|
|
||||||
|
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));
|
||||||
|
if (onStream != null) {
|
||||||
|
onStream(text, false);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
heavyHaptic();
|
||||||
|
}
|
||||||
|
} 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));
|
||||||
|
text = request.message!.content;
|
||||||
|
setState(() {});
|
||||||
|
heavyHaptic();
|
||||||
|
}
|
||||||
|
} 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(
|
||||||
|
content:
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
Text(AppLocalizations.of(context)!.settingsHostInvalid("timeout")),
|
||||||
|
showCloseIcon: true));
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((prefs!.getString("requestType") ?? "stream") == "stream") {
|
||||||
|
if (onStream != null) {
|
||||||
|
onStream(text, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveChat(chatUuid!, setState);
|
||||||
|
|
||||||
|
if (newChat && (prefs!.getBool("generateTitles") ?? true)) {
|
||||||
|
void setTitle() async {
|
||||||
|
history = await getHistory();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final generated = await client
|
||||||
|
.generateCompletion(
|
||||||
|
request: llama.GenerateCompletionRequest(
|
||||||
|
model: model!,
|
||||||
|
prompt:
|
||||||
|
"You must not use markdown or any other formatting language! Create a short title for the subject of the conversation described in the following json object. It is not allowed to be too general; no 'Assistance', 'Help' or similar!\n\n```json\n${jsonEncode(history)}\n```",
|
||||||
|
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
|
||||||
|
)
|
||||||
|
.timeout(const Duration(seconds: 10));
|
||||||
|
var title = generated.response!
|
||||||
|
.replaceAll("\"", "")
|
||||||
|
.replaceAll("'", "")
|
||||||
|
.replaceAll("*", "")
|
||||||
|
.replaceAll("_", "")
|
||||||
|
.trim();
|
||||||
|
var tmp = (prefs!.getStringList("chats") ?? []);
|
||||||
|
for (var i = 0; i < tmp.length; i++) {
|
||||||
|
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
|
||||||
|
chatUuid) {
|
||||||
|
var tmp2 = jsonDecode(tmp[i]);
|
||||||
|
tmp2["title"] = title;
|
||||||
|
tmp[i] = jsonEncode(tmp2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefs!.setStringList("chats", tmp);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
chatAllowed = true;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
@ -198,6 +198,7 @@ void setModel(BuildContext context, Function setState) {
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.primary,
|
.primary,
|
||||||
onSelected: (bool selected) {
|
onSelected: (bool selected) {
|
||||||
|
selectionHaptic();
|
||||||
if (addIndex == index) {
|
if (addIndex == index) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
|
|
|
||||||
104
pubspec.lock
104
pubspec.lock
|
|
@ -129,6 +129,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
datetime_loop:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: datetime_loop
|
||||||
|
sha256: "5b7b694879e505368e6d0e04ac8bb55a1e7d2e493dd0b851e08db08e1ba784e2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
diffutil_dart:
|
diffutil_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -328,6 +336,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_tts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_tts
|
||||||
|
sha256: aed2a00c48c43af043ed81145fd8503ddd793dafa7088ab137dbef81a703e53d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.2"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -445,6 +461,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.0"
|
version: "0.19.0"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.1"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -590,6 +614,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
|
pedantic:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pedantic
|
||||||
|
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.3.1"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.6"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.5"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.1"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
photo_view:
|
photo_view:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -715,6 +795,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.0"
|
||||||
|
speech_to_text:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: speech_to_text
|
||||||
|
sha256: "97425fd8cc60424061a0584b6c418c0eedab5201cc5e96ef15a946d7fab7b9b7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.2"
|
||||||
|
speech_to_text_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: speech_to_text_macos
|
||||||
|
sha256: e685750f7542fcaa087a5396ee471e727ec648bf681f4da83c84d086322173f6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
speech_to_text_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: speech_to_text_platform_interface
|
||||||
|
sha256: a0df1a907091ea09880077dc25aae02af9f79811264e6e97ddb08639b7f771c2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
sprintf:
|
sprintf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ dependencies:
|
||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
flutter_displaymode: ^0.6.0
|
flutter_displaymode: ^0.6.0
|
||||||
duration_picker: ^1.2.0
|
duration_picker: ^1.2.0
|
||||||
|
speech_to_text: ^6.6.2
|
||||||
|
flutter_tts: ^4.0.2
|
||||||
|
permission_handler: ^11.3.1
|
||||||
|
datetime_loop: ^1.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <flutter_tts/flutter_tts_plugin.h>
|
||||||
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
|
@ -15,6 +17,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
FlutterTtsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterTtsPlugin"));
|
||||||
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
bitsdojo_window_windows
|
bitsdojo_window_windows
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
flutter_tts
|
||||||
|
permission_handler_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue