Markdown renderer, other changes

This commit is contained in:
JHubi1 2025-10-28 07:23:28 +01:00
parent 83ba8bd15f
commit ba63e0fc50
No known key found for this signature in database
GPG Key ID: F538DC3FC5B07498
11 changed files with 2702 additions and 250 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:ollama_dart/ollama_dart.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
// import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
// import 'package:flutter_chat_ui/flutter_chat_ui.dart' as chat_ui;
@ -18,8 +19,8 @@ import '../l10n/gen/app_localizations.dart';
import '../main.dart';
import '../services/chat.dart';
import '../services/error.dart';
import '../services/markdown.dart';
import '../services/model.dart';
import '../services/theme.dart';
// import '../services/model.dart';
// import '../services/preferences.dart';
// import '../worker/desktop.dart';
@ -1834,8 +1835,6 @@ class _ScreenMainState extends State<ScreenMain> {
return Scaffold(
body: ListView(
children: [
const ListTile(title: ThemeModeSwitch()),
const ListTile(title: ThemeSwitch()),
...ChatManager.instance.chats.map(
(chat) => ScreenMainChatTile(chat: chat),
),
@ -1853,7 +1852,7 @@ class _ScreenMainState extends State<ScreenMain> {
"M49WC9CW",
() async {
var msg = TextMessage(
"Hi!", // "Hello! Please explain what you can do.",
"Write a long demo message for all GitHub Flavored Markdown features.",
sender: MessageSender.user,
);
return chat.send(msg);
@ -1916,11 +1915,20 @@ class _ScreenMainChatTileState extends State<ScreenMainChatTile> {
widget.chat.title.emptyOn(AppLocalizations.of(context).newChatTitle),
placeholder: Text(AppLocalizations.of(context).newChatTitle),
),
subtitle: (widget.chat.messages.isEmpty)
? const SizedBox.shrink()
: ChatText(
(widget.chat.messages.last as TextMessage).content,
placeholder: const Text("<incoming>"),
subtitle:
(widget.chat.messages.isEmpty ||
widget.chat.messages.last.runtimeType != TextMessage)
? null
// : ChatText(
// (widget.chat.messages.last as TextMessage).content,
// placeholder: const Text("<incoming>"),
// ),
: (widget.chat.messages.last as TextMessage).content.isEmpty
? const LinearProgressIndicator()
: Text.rich(
Markdown(
(widget.chat.messages.last as TextMessage).content,
).toTextSpan(context),
),
onTap: () => ChatManager.instance.deleteChat(widget.chat),
);

View File

@ -15,7 +15,7 @@ import 'preferences.dart';
enum MessageSender { user, assistant }
class Message extends ChangeNotifier {
abstract class Message extends ChangeNotifier {
final String id;
final MessageSender sender;
@ -27,17 +27,13 @@ class Message extends ChangeNotifier {
void modify() => notifyListeners();
Map<String, dynamic> toJson() =>
throw UnimplementedError("toJson() must be implemented in subclasses");
Map<String, dynamic> toJson();
@override
String toString() => toJson().toString();
@override
operator ==(Object other) {
return other is Message && other.id == id;
}
operator ==(Object other) => other is Message && other.id == id;
@override
int get hashCode => id.hashCode;
}
@ -75,10 +71,8 @@ class TextMessage extends Message {
required MessageSender sender,
Completer<void>? completer,
void Function(String content)? onContent,
}) {
return TextMessage("", sender: sender, createdAt: DateTime.now())
.._contentFromStream(stream, completer: completer, onContent: onContent);
}
}) => TextMessage("", sender: sender, createdAt: DateTime.now())
.._contentFromStream(stream, completer: completer, onContent: onContent);
Future<void> _contentFromStream(
Stream<ollama.GenerateChatCompletionResponse> stream, {
@ -89,14 +83,19 @@ class TextMessage extends Message {
_locked = true;
try {
await for (var response in stream) {
if (completer?.isCompleted ?? false) return;
_content += response.message.content;
StreamSubscription? sub;
sub = stream.listen((event) {
if (completer?.isCompleted ?? false) {
sub?.cancel();
return;
}
_content += event.message.content;
chatHaptic();
notifyListeners();
onContent?.call(_content);
}
});
await sub.asFuture();
} catch (e, s) {
if (completer?.isCompleted ?? false) return;
completer?.completeError(e, s);
@ -132,7 +131,7 @@ class ImageMessage extends Message {
}
class Chat extends ChangeNotifier {
Completer<void>? completer;
Completer<void> completer = Completer<void>()..complete();
bool get alive => ChatManager.instance.chats.contains(this);
bool get active => alive && ChatManager.instance.currentChatId == id;
@ -188,6 +187,18 @@ class Chat extends ChangeNotifier {
}
}
@override
void dispose() {
for (var message in _messages) {
message.removeListener(notifyListeners);
}
if (completer.isCompleted == false) completer.complete();
if (ChatManager.instance.currentChatId == id) {
ChatManager.instance.currentChatId = null;
}
super.dispose();
}
List<ollama.Message> toApi() {
var messages = <ollama.Message>[];
var images = <ImageMessage>[];
@ -288,6 +299,8 @@ class Chat extends ChangeNotifier {
if (alive) Error.throwWithStackTrace(e, s);
return;
}
if (!alive) return;
var newTitle = generated.message.content;
newTitle = newTitle.replaceAll("\n", " ");
@ -321,17 +334,29 @@ class Chat extends ChangeNotifier {
title = newTitle.trim();
}
void append(Message message) {
assert(alive, "Chat must be alive to be modified.");
_messages.add(message..addListener(notifyListeners));
notifyListeners();
}
Future<void> send(
Message message, {
bool awaitCompletion = true,
bool? think,
void Function(String)? onContent,
void Function(String content)? onContent,
}) async {
assert(alive, "Chat must be alive to be modified.");
assert(model != null, "Chat model must be set to send messages.");
if (model == null) return;
assert(
completer.isCompleted,
"Cannot send a message while another message is being sent.",
);
_messages.add(message..addListener(notifyListeners));
var finalThink =
@ -356,14 +381,13 @@ class Chat extends ChangeNotifier {
)..addListener(notifyListeners);
_messages.add(message);
var future = completer!.future
.then((_) => ChatManager.instance.saveChats())
.catchError((e, s) {
message
.._includesError = true
..modify();
Error.throwWithStackTrace(e, s);
});
var future = completer.future.catchError((e, s) {
message
.._locked = false
.._includesError = true
..modify();
Error.throwWithStackTrace(e, s);
});
if (awaitCompletion) await future;
}
@ -378,6 +402,11 @@ class Chat extends ChangeNotifier {
message.removeListener(notifyListeners);
notifyListeners();
}
@override
bool operator ==(Object other) => other is Chat && other.id == id;
@override
int get hashCode => id.hashCode;
}
class ChatManager extends ChangeNotifier {
@ -407,6 +436,15 @@ class ChatManager extends ChangeNotifier {
ChatManager._();
void clearChats() {
for (var chat in _chats) {
chat.dispose();
}
_chats.clear();
_currentChatId = null;
notifyListeners();
}
Future<void> loadChats() async {
DateTime getDateTimeFromMilliseconds(int? milliseconds) {
if (milliseconds == null) return DateTime.now();
@ -502,8 +540,11 @@ class ChatManager extends ChangeNotifier {
void deleteChat(Chat chat) {
_chats.remove(chat);
if (chat.completer?.isCompleted == false) chat.completer!.complete();
chat.removeListener(notifyListeners);
for (var message in chat.messages) {
message.removeListener(chat.notifyListeners);
}
if (chat.completer.isCompleted == false) chat.completer.complete();
if (_currentChatId == chat.id) _currentChatId = null;

View File

@ -6,17 +6,21 @@ import 'package:ollama_dart/ollama_dart.dart' as llama;
import '../main.dart';
const String userAgent = "Ollama App";
class OllamaHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..userAgent = userAgent
..badCertificateCallback = (_, __, ___) => true;
}
}
final httpClient = http.Client();
llama.OllamaClient get ollamaClient => llama.OllamaClient(
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
.cast<String, String>(),
baseUrl: "$host/api",
client: httpClient);
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
.cast<String, String>(),
baseUrl: "$host/api",
client: httpClient,
);

View File

@ -3,16 +3,20 @@ import 'dart:io';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:ollama_dart/ollama_dart.dart';
import 'package:url_launcher/url_launcher.dart';
import '../l10n/gen/app_localizations.dart';
import 'markdown.dart';
final _logIdRegex = RegExp(r"^[A-Z0-9]{2,16}$");
String _removeNewlines(String content) =>
content.replaceAll(RegExp(r"\s*\n\s*"), " ");
typedef ErrorGuardErrorMessageGenerator = String Function(Object exception);
typedef ErrorGuardDetailsMessageGenerator =
String? Function(Object exception, StackTrace stackTrace);
@ -24,7 +28,9 @@ String _defaultErrorMessage(Object exception) => switch (exception) {
exception.toString().split(": ").elementAtOrNull(4) ??
"An assertion failed",
TimeoutException _ => "Request timed out",
SocketException _ || HttpException _ => "Could not connect to server",
SocketException _ ||
HttpException _ ||
ClientException _ => "Could not connect to server",
TlsException _ => "Could not establish secure connection",
StateError _ => "Invalid state encountered",
_ => "An unknown error occurred",
@ -34,9 +40,13 @@ String? _defaultDetailsMessage(
StackTrace stackTrace,
) => switch (exception) {
OllamaClientException e =>
e.body.toString().startsWith("ClientException with SocketException")
[
"SocketException",
"HttpException",
"ClientException",
].contains(e.body.toString().split(":").first)
? "A network error occurred while trying to connect to the server."
"\n\nYou may check your network connection or server reachability and try again."
"\n\nYou may check your network connection and server reachability and try again."
: "The Ollama API client received a faulty response with code `${e.code}`."
"\n\nPlease check your Ollama server or proxy configuration and try again.",
AssertionError _ =>
@ -46,9 +56,9 @@ String? _defaultDetailsMessage(
TimeoutException _ =>
"Time ran out while waiting for a response from the server."
"\n\nThis might be caused by a slow or unresponsive server, or a network issue.\nYou may try increasing the Timeout Multiplier in the settings.",
SocketException _ || HttpException _ =>
"A ${exception.runtimeType.toString().split(RegExp(r"(?=[A-Z])")).join(" ").toLowerCase()} might be caused by a slow or unresponsive server, or a network issue."
"\n\nYou may check your network connection and try again.",
SocketException _ || HttpException _ || ClientException _ =>
"A ${exception.toString().split(":").first} might be caused by a slow or unresponsive server, or a network issue."
"\n\nYou may check your network connection and server reachability and try again.",
TlsException _ =>
"An error occurred while trying to establish a secure connection via TLS."
"\n\nThis might be caused by an invalid or expired certificate, though this should not happen. Please report this issue to the developers.",
@ -254,7 +264,9 @@ Future<T?> errorGuard<T>(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
_removeNewlines(errorMessageText),
_removeNewlines(
Markdown(errorMessageText).toTextSpan(context).toPlainText(),
),
style: TextStyle(color: colorScheme.onErrorContainer),
),
backgroundColor: colorScheme.errorContainer,
@ -273,9 +285,6 @@ Future<T?> errorGuard<T>(
}
}
String _removeNewlines(String content) =>
content.replaceAll(RegExp(r"\s*\n\s*"), " ");
class _ErrorGuardDetailsDialog extends StatefulWidget {
final String? logId;
final DateTime dateTime;
@ -338,7 +347,7 @@ class _ErrorGuardDetailsDialogState extends State<_ErrorGuardDetailsDialog> {
child: Text("@${widget.logId}"),
),
Text.rich(_contentFormat(context, widget.errorMessage)),
Text.rich(Markdown(widget.errorMessage).toTextSpan(context)),
const SizedBox(height: 8),
if (widget.detailsMessage != null) ...[
_ErrorGuardDetailsPanel(
@ -390,167 +399,6 @@ class _ErrorGuardDetailsDialogState extends State<_ErrorGuardDetailsDialog> {
);
}
static TextSpan _contentFormat(BuildContext context, String content) {
content = content.trim().split("\n").map((l) => l.trim()).join("\n");
if (content.isEmpty) return const TextSpan(text: "");
// content = content.replaceAll(RegExp(r"(?=)"), "\u{00AD}");
var paragraphs = content.split("\n\n").map((p) => p.trim()).toList();
InlineSpan parseInline(String text) {
var root = <String, dynamic>{
"type": "root",
"children": <Map<String, dynamic>>[],
};
var stack = <Map<String, dynamic>>[root];
var buf = StringBuffer();
void flushBufferToCurrent() {
if (buf.isEmpty) return;
var textNode = {"type": "text", "text": buf.toString()};
(stack.last["children"] as List).add(textNode);
buf.clear();
}
var i = 0;
while (i < text.length) {
var ch = text[i];
if (ch == "\\" && i + 1 < text.length) {
buf.write(text[i + 1]);
i += 2;
continue;
}
var inCode = stack.last["type"] == "code";
if (inCode) {
if (ch == "`") {
flushBufferToCurrent();
stack.removeLast();
i++;
} else {
buf.write(ch);
i++;
}
continue;
}
if (ch == '<') {
var endIndex = text.indexOf('>', i + 1);
if (endIndex != -1) {
var url = text.substring(i + 1, endIndex);
if (url.startsWith('https://') || url.startsWith('http://')) {
flushBufferToCurrent();
var linkNode = {"type": "link", "url": url};
(stack.last["children"] as List).add(linkNode);
i = endIndex + 1;
continue;
}
}
}
if (ch == "`") {
flushBufferToCurrent();
var codeNode = {"type": "code", "children": <Map<String, dynamic>>[]};
(stack.last["children"] as List).add(codeNode);
stack.add(codeNode);
i++;
continue;
}
if (ch == "*") {
flushBufferToCurrent();
if (stack.last["type"] == "italic") {
stack.removeLast();
} else {
var n = {"type": "italic", "children": <Map<String, dynamic>>[]};
(stack.last['children'] as List).add(n);
stack.add(n);
}
i++;
continue;
}
buf.write(ch);
i++;
}
flushBufferToCurrent();
InlineSpan build(Map<String, dynamic> node) {
var type = node["type"] as String;
if (type == "text") {
return TextSpan(text: node["text"] as String);
}
if (type == "link") {
var url = node["url"] as String;
return TextSpan(
text: url,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () => launchUrl(Uri.parse(url)),
);
}
var children = (node["children"] as List)
.map<InlineSpan>((c) => build(c as Map<String, dynamic>))
.toList();
switch (type) {
case "root":
return TextSpan(children: children);
case "italic":
return TextSpan(
children: children,
style: const TextStyle(fontStyle: FontStyle.italic),
);
case "code":
var codeText = (node["children"] as List)
.where((c) => (c as Map<String, dynamic>)["type"] == "text")
.map((c) => (c as Map<String, dynamic>)["text"] as String)
.join();
var widget = DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(4),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: Text(
codeText,
style: const TextStyle(fontFamily: "monospace"),
),
),
);
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: widget,
);
default:
return TextSpan(children: children);
}
}
return build(root);
}
return TextSpan(
children: List.generate(paragraphs.length * 2 - 1, (index) {
if (index.isOdd) {
return const TextSpan(
text: "\n\n",
style: TextStyle(height: 0.5, color: Colors.transparent),
);
}
var text = paragraphs[index ~/ 2];
return parseInline(text);
}),
);
}
String _reportText() =>
"""
An exception was thrown during the execution of the app.
@ -590,7 +438,7 @@ The app suggested the following cause of the issue:
url += "&context=${Uri.encodeComponent(contextText)}";
Clipboard.setData(ClipboardData(text: url));
launchUrl(Uri.parse(url));
launchUrl(Uri.parse(url), mode: LaunchMode.inAppBrowserView);
}
}
@ -680,7 +528,7 @@ class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
leading: widget.icon,
title: Text(widget.title),
trailing: ExpandIcon(
onPressed: (_) => _toggleAnimation(),
onPressed: null,
isExpanded: _expanded,
padding: EdgeInsets.zero,
),
@ -706,11 +554,7 @@ class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
),
)
: Text.rich(
_ErrorGuardDetailsDialogState._contentFormat(
context,
widget.content.trim(),
),
textAlign: TextAlign.justify,
Markdown(widget.content.trim()).toTextSpan(context),
style: TextStyle(
fontStyle: widget.italic ? FontStyle.italic : null,
color: widget.italic ? theme.disabledColor : null,

2395
lib/services/markdown.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:ollama_dart/ollama_dart.dart' as ollama;
import '../main.dart';
import 'clients.dart' as clients;
import 'preferences.dart';
typedef ModelCapability = ollama.Capability;
class Model {
class Model extends ChangeNotifier {
final String name;
String _family;
@ -50,6 +50,7 @@ class Model {
_families = data.details!.families?.toSet() ?? {};
}
_capabilities = data.capabilities!.toSet();
notifyListeners();
}
Future<void> loadIntoMemory() async {
@ -59,12 +60,9 @@ class Model {
var headers = <String, String>{
"Content-Type": "application/json",
...(jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map),
};
var body = {
"model": name,
"keep_alive": int.parse(prefs!.getString("keepAlive") ?? "300"),
...Preferences.instance.hostHeaders,
};
var body = {"model": name, "keep_alive": Preferences.instance.keepAlive};
await clients.httpClient
.post(
@ -76,10 +74,7 @@ class Model {
}
@override
operator ==(Object other) {
return other is Model && other.name == name;
}
operator ==(Object other) => other is Model && other.name == name;
@override
int get hashCode => name.hashCode;
}
@ -111,9 +106,14 @@ class ModelManager extends ChangeNotifier {
Future<void> loadModels({bool fetchCapabilitiesInBackground = true}) async {
var data = await clients.ollamaClient.listModels();
for (var model in _models) {
model.dispose();
}
_models.clear();
for (var model in data.models!) {
_models.add(Model.fromApi(model: model));
_models.add(Model.fromApi(model: model)..addListener(notifyListeners));
}
_initialized = true;

View File

@ -26,7 +26,7 @@ class Preferences extends ChangeNotifier {
Map<String, String> get hostHeaders =>
(jsonDecode(prefs?.getString("hostHeaders") ?? "{}") as Map).cast();
set hostHeaders(Map value) {
set hostHeaders(Map<String, String> value) {
if (value.isEmpty) {
prefs?.remove("hostHeaders");
} else {
@ -137,4 +137,10 @@ class TimeoutMultiplier {
/// Very long time interval. Equals 60 seconds with default multiplier.
static Duration get veryLong => calculate(const Duration(seconds: 60));
/// Interval intended for streams. Equals 5 minutes with default multiplier.
///
/// This one might not be suitable for all models and situations. Use only
/// if required.
static Duration get stream => calculate(const Duration(minutes: 5));
}

View File

@ -12,8 +12,8 @@ typedef ThemeBuilderBuilder =
);
class ThemeBuilderData {
ThemeBuilderData? _current;
ThemeBuilderData? get current => _current;
static ThemeBuilderData? _current;
static ThemeBuilderData? get current => _current;
final ColorScheme? dynamicLight;
final ColorScheme? dynamicDark;
@ -67,7 +67,7 @@ class ThemeBuilderData {
dynamicSchemeVariant: DynamicSchemeVariant.content,
brightness: Brightness.dark,
).copyWith(surface: Colors.black),
);
).copyWith(dividerColor: Colors.white30);
}
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
// idk why this fixes the cut off on the bottom, but it does, so anyways
Tween<Rect?> heroTextCreateRectTween(Rect? begin, Rect? end) =>
RectTween(begin: begin, end: end);
Widget heroTextFlightShuttleBuilder(
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
var fromText = (fromHeroContext.widget as Hero).child as Text;
var toText = (toHeroContext.widget as Hero).child as Text;
var begin = fromText.style ?? DefaultTextStyle.of(fromHeroContext).style;
var end = toText.style ?? DefaultTextStyle.of(toHeroContext).style;
if (flightDirection == HeroFlightDirection.pop) {
(fromText, toText) = (toText, fromText);
(begin, end) = (end, begin);
}
var styleTween = TextStyleTween(begin: begin, end: end);
var textString = fromText.data ?? toText.data ?? "";
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
var currentStyle = styleTween.lerp(animation.value);
return Material(
type: MaterialType.transparency,
child: Center(
child: Text(
textString,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: currentStyle,
),
),
);
},
);
}

View File

@ -302,6 +302,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_highlight:
dependency: "direct main"
description:
path: flutter_highlight
ref: HEAD
resolved-ref: "5179638b4c3a05528e1860f50ea64a95f393f2be"
url: "https://github.com/JHubi1/highlight.dart"
source: git
version: "0.7.0"
flutter_install_referrer:
dependency: "direct main"
description:
@ -339,14 +348,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_markdown:
flutter_math_fork:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
name: flutter_math_fork
sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407"
url: "https://pub.dev"
source: hosted
version: "0.7.7+1"
version: "0.7.4"
flutter_parsed_text:
dependency: transitive
description:
@ -363,6 +372,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.29"
flutter_svg:
dependency: transitive
description:
name: flutter_svg
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
url: "https://pub.dev"
source: hosted
version: "2.2.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -389,6 +406,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.4"
highlight:
dependency: transitive
description:
name: highlight
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
html:
dependency: transitive
description:
@ -413,14 +438,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
hyphenatorx:
dependency: "direct main"
description:
name: hyphenatorx
sha256: "9afe950d84e56fde91b9129bf3c95e86e70f19a20e24ba6458ee1d028a64816e"
url: "https://pub.dev"
source: hosted
version: "1.5.11"
image_picker:
dependency: "direct main"
description:
@ -589,6 +606,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
ogp_data_extract:
dependency: "direct main"
description:
name: ogp_data_extract
sha256: "6389120f7d0a20d9bff5c37c5b46d8977aea2e604f5a04ff75fee597b8865269"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
ollama_dart:
dependency: "direct main"
description:
@ -621,6 +654,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider_linux:
dependency: transitive
description:
@ -701,6 +742,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
photo_view:
dependency: transitive
description:
@ -725,6 +774,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pwa_install:
dependency: "direct main"
description:
@ -882,6 +939,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
string_validator:
dependency: transitive
description:
name: string_validator
sha256: "240f4c98027dfbe8639c8271ef18cc9de735b47067aa15a720cfed9576a512b1"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
@ -914,6 +979,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:
@ -1010,6 +1083,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
url: "https://pub.dev"
source: hosted
version: "1.1.19"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
url: "https://pub.dev"
source: hosted
version: "1.1.19"
vector_math:
dependency: transitive
description:
@ -1066,6 +1163,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.35.0"

View File

@ -26,7 +26,6 @@ dependencies:
transparent_image: ^2.0.1
simple_icons: ^14.6.1
url_launcher: ^6.2.6
flutter_markdown: ^0.7.1
file_picker: ^8.0.3
file_selector: ^1.0.3
bitsdojo_window: ^0.1.6
@ -42,8 +41,13 @@ dependencies:
universal_html: ^2.2.4
pwa_install: ^0.0.5
flutter_chat_types: any
markdown: any
hyphenatorx: ^1.5.11
markdown: ^7.3.0
flutter_math_fork: ^0.7.4
flutter_highlight:
git:
url: https://github.com/JHubi1/highlight.dart
path: flutter_highlight
ogp_data_extract: ^0.2.2
# dynamic_color: ^1.7.0
dynamic_system_colors: ^1.8.0