Added chat ui, general ui improvements

This commit is contained in:
JHubi1 2024-05-25 13:50:30 +02:00
parent 13ea97968c
commit 1f1ef19743
No known key found for this signature in database
GPG Key ID: 106B3E4BCE046A53
4 changed files with 382 additions and 59 deletions

View File

@ -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.

View File

@ -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<App> {
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<MainApp> {
final List<types.Message> _messages = [];
List<types.Message> _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<MainApp> {
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("<none>",
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("<none>",
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"))
]));
}
}

View File

@ -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

View File

@ -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: