From 1f1ef19743ae2393521786a02121a6097f90c5e7 Mon Sep 17 00:00:00 2001 From: JHubi1 Date: Sat, 25 May 2024 13:50:30 +0200 Subject: [PATCH] Added chat ui, general ui improvements --- README.md | 6 +- lib/main.dart | 291 ++++++++++++++++++++++++++++++++++++++++---------- pubspec.lock | 138 +++++++++++++++++++++++- pubspec.yaml | 6 +- 4 files changed, 382 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 97d4f79..4505f4e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ -# ollama +# Ollama App -A new Flutter project. +A modern and easy-to-use client for Ollama. Currently, not production-ready. + +> Important: This app does not host a Ollama server on device, but rather connects to one and plays with it. diff --git a/lib/main.dart b/lib/main.dart index 5ad4463..3e15d27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,9 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:uuid/uuid.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:visibility_detector/visibility_detector.dart'; SharedPreferences? prefs; ThemeData? theme; @@ -75,16 +78,16 @@ class _AppState extends State { systemNavigationBarColor: (MediaQuery.of(context).platformBrightness == Brightness.light) - ? themeDark!.colorScheme.background - : theme!.colorScheme.background)); + ? (themeDark ?? ThemeData.dark()).colorScheme.background + : (theme ?? ThemeData()).colorScheme.background)); }; // brightness changed function not run at first startup SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - systemNavigationBarColor: - (MediaQuery.of(context).platformBrightness == - Brightness.light) - ? theme!.colorScheme.background - : themeDark!.colorScheme.background)); + systemNavigationBarColor: (MediaQuery.of(context) + .platformBrightness == + Brightness.light) + ? (theme ?? ThemeData()).colorScheme.background + : (themeDark ?? ThemeData.dark()).colorScheme.background)); setState(() {}); } }, @@ -106,9 +109,11 @@ class MainApp extends StatefulWidget { } class _MainAppState extends State { - final List _messages = []; + List _messages = []; final _user = types.User(id: const Uuid().v4()); + bool logoVisible = true; + @override void initState() { super.initState(); @@ -118,58 +123,234 @@ class _MainAppState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Row(children: [ - IconButton( - onPressed: () {}, icon: const Icon(Icons.menu_open_rounded)), - const SizedBox(width: 16), - Expanded( - child: InkWell( - onTap: () { - showModalBottomSheet( - context: context, - builder: (context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - child: const Column( - mainAxisSize: MainAxisSize.min, - children: [Text("data")])); - }); - }, - splashFactory: NoSplash.splashFactory, - highlightColor: Colors.transparent, - enableFeedback: false, - child: const SizedBox( - height: 72, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - child: Text("", - overflow: TextOverflow.fade, - style: TextStyle( - fontFamily: "monospace", - fontSize: 16))), - SizedBox(width: 4), - Icon(Icons.expand_more_rounded) - ])))), - const SizedBox(width: 16), - IconButton( - onPressed: () {}, icon: const Icon(Icons.restart_alt_rounded)) - ])), + title: InkWell( + onTap: () { + HapticFeedback.selectionClick(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [Text("data")])); + }); + }, + splashFactory: NoSplash.splashFactory, + highlightColor: Colors.transparent, + enableFeedback: false, + child: const SizedBox( + height: 72, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Text("", + overflow: TextOverflow.fade, + style: TextStyle( + fontFamily: "monospace", fontSize: 16))), + SizedBox(width: 4), + Icon(Icons.expand_more_rounded) + ]))), + actions: [ + IconButton( + onPressed: () { + _messages = []; + HapticFeedback.selectionClick(); + setState(() {}); + }, + icon: const Icon(Icons.restart_alt_rounded)) + ], + ), body: SizedBox.expand( child: Chat( messages: _messages, - onSendPressed: (p0) {}, + emptyState: Center( + child: VisibilityDetector( + key: const Key("logoVisible"), + onVisibilityChanged: (VisibilityInfo info) { + logoVisible = info.visibleFraction > 0; + setState(() {}); + }, + child: AnimatedOpacity( + opacity: logoVisible ? 1.0 : 0.0, + duration: const Duration(milliseconds: 500), + child: const ImageIcon(AssetImage("assets/logo512.png"), + size: 44)))), + onSendPressed: (p0) { + _messages.insert( + 0, + types.TextMessage( + author: _user, id: const Uuid().v4(), text: p0.text)); + setState(() {}); + HapticFeedback.selectionClick(); + }, + onMessageDoubleTap: (context, p1) { + for (var i = 0; i < _messages.length; i++) { + if (_messages[i].id == p1.id) { + _messages.removeAt(i); + break; + } + } + setState(() {}); + HapticFeedback.selectionClick(); + }, + onAttachmentPressed: () { + HapticFeedback.selectionClick(); + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () async { + HapticFeedback.selectionClick(); + + Navigator.of(context).pop(); + final result = + await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + if (result == null) return; + + final bytes = + await result.readAsBytes(); + final image = + await decodeImageFromList( + bytes); + + final message = types.ImageMessage( + author: _user, + createdAt: DateTime.now() + .millisecondsSinceEpoch, + height: image.height.toDouble(), + id: const Uuid().v4(), + name: result.name, + size: bytes.length, + uri: result.path, + width: image.width.toDouble(), + ); + + _messages.insert(0, message); + setState(() {}); + HapticFeedback.selectionClick(); + }, + icon: const Icon(Icons.image_rounded), + label: const Text("Upload Image"))), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () async { + HapticFeedback.selectionClick(); + + Navigator.of(context).pop(); + final result = await FilePicker + .platform + .pickFiles( + type: FileType.custom, + allowedExtensions: ["pdf"]); + if (result == null || + result.files.single.path == + null) return; + + final message = types.FileMessage( + author: _user, + createdAt: DateTime.now() + .millisecondsSinceEpoch, + id: const Uuid().v4(), + name: result.files.single.name, + size: result.files.single.size, + uri: result.files.single.path!, + ); + + _messages.insert(0, message); + setState(() {}); + HapticFeedback.selectionClick(); + }, + icon: const Icon( + Icons.file_copy_rounded), + label: const Text("Upload File"))) + ])); + }); + }, + l10n: const ChatL10nEn(), + inputOptions: const InputOptions( + keyboardType: TextInputType.text, + sendButtonVisibilityMode: SendButtonVisibilityMode.always), user: _user, - theme: (MediaQuery.of(context).platformBrightness == - Brightness.light) + hideBackgroundOnEmojiMessages: false, + theme: (MediaQuery.of(context).platformBrightness == Brightness.light) ? DefaultChatTheme( - backgroundColor: theme!.colorScheme.background, - primaryColor: theme!.colorScheme.primary) + backgroundColor: + (theme ?? ThemeData()).colorScheme.background, + primaryColor: + (theme ?? ThemeData()).colorScheme.primary, + attachmentButtonIcon: + const Icon(Icons.file_upload_rounded), + sendButtonIcon: const Icon(Icons.send_rounded), + inputBackgroundColor: (theme ?? ThemeData()) + .colorScheme + .onBackground + .withAlpha(10), + inputTextColor: + (theme ?? ThemeData()).colorScheme.onBackground, + inputBorderRadius: + const BorderRadius.all(Radius.circular(64)), + inputPadding: const EdgeInsets.all(16), + inputMargin: EdgeInsets.only( + left: 8, + right: 8, + bottom: + (MediaQuery.of(context).viewInsets.bottom == 0.0) + ? 0 + : 8)) : DarkChatTheme( - backgroundColor: themeDark!.colorScheme.background, - primaryColor: themeDark!.colorScheme.primary)))); + backgroundColor: + (themeDark ?? ThemeData.dark()).colorScheme.background, + primaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(40), + attachmentButtonIcon: const Icon(Icons.file_upload_rounded), + sendButtonIcon: const Icon(Icons.send_rounded), + inputBackgroundColor: (themeDark ?? ThemeData()).colorScheme.onBackground.withAlpha(40), + inputTextColor: (themeDark ?? ThemeData()).colorScheme.onBackground, + inputBorderRadius: const BorderRadius.all(Radius.circular(64)), + inputPadding: const EdgeInsets.all(16), + inputMargin: EdgeInsets.only(left: 8, right: 8, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0) ? 0 : 8)))), + drawer: NavigationDrawer( + onDestinationSelected: (value) { + if (value == 1) { + HapticFeedback.selectionClick(); + Navigator.of(context).pop(); + _messages = []; + setState(() {}); + } else if (value == 2) { + HapticFeedback.selectionClick(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Settings not implemented yet."), + showCloseIcon: true)); + } + }, + selectedIndex: 1, + children: const [ + NavigationDrawerDestination( + icon: ImageIcon(AssetImage("assets/logo512.png")), + label: Text("Ollama"), + ), + Divider(), + NavigationDrawerDestination( + icon: Icon(Icons.add_rounded), label: Text("New Chat")), + NavigationDrawerDestination( + icon: Icon(Icons.settings_rounded), label: Text("Settings")) + ])); } } diff --git a/pubspec.lock b/pubspec.lock index af4df40..9bde1de 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + animated_text_kit: + dependency: "direct main" + description: + name: animated_text_kit + sha256: "37392a5376c9a1a503b02463c38bc0342ef814ddbb8f9977bc90f2a84b22fa92" + url: "https://pub.dev" + source: hosted + version: "4.2.2" async: dependency: transitive description: @@ -41,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -97,6 +113,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + url: "https://pub.dev" + source: hosted + version: "8.0.3" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" fixnum: dependency: transitive description: @@ -158,6 +214,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + url: "https://pub.dev" + source: hosted + version: "2.0.19" flutter_test: dependency: "direct dev" description: flutter @@ -192,6 +256,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "33974eca2e87e8b4e3727f1b94fa3abcb25afe80b6bc2c4d449a0e150aedf720" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "0f57fee1e8bfadf8cc41818bbcd7f72e53bb768a54d9496355d5e8a5681a19f1" + url: "https://pub.dev" + source: hosted + version: "0.8.12+1" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "4824d8c7f6f89121ef0122ff79bb00b009607faecc8545b86bca9ab5ce1e95bf" + url: "https://pub.dev" + source: hosted + version: "0.8.11+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: transitive description: @@ -272,6 +400,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" path: dependency: transitive description: @@ -542,7 +678,7 @@ packages: source: hosted version: "2.1.4" visibility_detector: - dependency: transitive + dependency: "direct main" description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 diff --git a/pubspec.yaml b/pubspec.yaml index fb6a3ec..17454bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: ollama -description: "A new Flutter project." +description: "A modern and easy-to-use client for Ollama" publish_to: 'none' version: 0.1.0 @@ -12,6 +12,10 @@ dependencies: shared_preferences: ^2.2.3 flutter_chat_ui: ^1.6.13 uuid: ^4.4.0 + animated_text_kit: ^4.2.2 + image_picker: ^1.1.1 + file_picker: ^8.0.3 + visibility_detector: ^0.4.0+2 dev_dependencies: flutter_test: