ollama-app/lib/screen_settings.dart

447 lines
20 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'main.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:dartx/dartx.dart';
import 'package:http/http.dart' as http;
import 'package:simple_icons/simple_icons.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:restart_app/restart_app.dart';
class ScreenSettings extends StatefulWidget {
const ScreenSettings({super.key});
@override
State<ScreenSettings> createState() => _ScreenSettingsState();
}
class _ScreenSettingsState extends State<ScreenSettings> {
final hostInputController = TextEditingController(
text: (useHost) ? fixedHost : (prefs?.getString("host") ?? ""));
bool hostLoading = false;
bool hostInvalidUrl = false;
bool hostInvalidHost = false;
void checkHost() async {
setState(() {
hostLoading = true;
hostInvalidUrl = false;
hostInvalidHost = false;
});
var tmpHost = hostInputController.text.trim().removeSuffix("/").trim();
if (tmpHost.isEmpty || !Uri.parse(tmpHost).isAbsolute) {
setState(() {
hostInvalidUrl = true;
hostLoading = false;
});
return;
}
http.Response request;
try {
request = await http
.get(Uri.parse(tmpHost))
.timeout(const Duration(seconds: 5), onTimeout: () {
return http.Response("Error", 408);
});
} catch (e) {
setState(() {
hostInvalidHost = true;
hostLoading = false;
});
return;
}
if ((request.statusCode == 200 && request.body == "Ollama is running") ||
(Uri.parse(tmpHost).toString() == fixedHost)) {
// messages = [];
setState(() {
hostLoading = false;
host = tmpHost;
if (hostInputController.text != host!) {
hostInputController.text = host!;
}
});
prefs?.setString("host", host!);
} else {
setState(() {
hostInvalidHost = true;
hostLoading = false;
});
}
HapticFeedback.selectionClick();
}
final systemInputController = TextEditingController(
text: prefs?.getString("system") ?? "You are a helpful assistant");
final repoUrl = "https://github.com/JHubi1/ollama-app";
@override
void initState() {
super.initState();
checkHost();
}
@override
void dispose() {
super.dispose();
hostInputController.dispose();
}
Widget toggle(String text, bool value, Function(bool value) onChanged) {
var space = ""; // Invisible character: U+2063
var spacePlus = " $space";
return Stack(children: [
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 12),
child: Divider(
color: (Theme.of(context).brightness == Brightness.light)
? Colors.grey[300]
: Colors.grey[900])),
Row(mainAxisSize: MainAxisSize.max, children: [
Expanded(
child: Text(text + spacePlus,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
backgroundColor:
(Theme.of(context).brightness == Brightness.light)
? (theme ?? ThemeData()).colorScheme.surface
: (themeDark ?? ThemeData.dark())
.colorScheme
.surface))),
Container(
padding: const EdgeInsets.only(left: 16),
color: (Theme.of(context).brightness == Brightness.light)
? (theme ?? ThemeData()).colorScheme.surface
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
child: SizedBox(
height: 40, child: Switch(value: value, onChanged: onChanged)))
]),
]);
}
Widget title(String text, {double top = 16, double bottom = 16}) {
return Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: top, bottom: bottom),
child: Row(children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: Text(text)),
const Expanded(child: Divider())
]));
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: !hostLoading,
onPopInvoked: (didPop) {
FocusManager.instance.primaryFocus?.unfocus();
},
child: Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.optionSettings),
),
body: Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: ListView(children: [
const SizedBox(height: 16),
const SizedBox(height: 8),
TextField(
controller: hostInputController,
keyboardType: TextInputType.url,
readOnly: useHost,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.settingsHost,
hintText: "http://localhost:11434",
suffixIcon: useHost
? const SizedBox.shrink()
: (hostLoading
? Transform.scale(
scale: 0.5,
child: const CircularProgressIndicator())
: IconButton(
onPressed: () {
HapticFeedback.selectionClick();
checkHost();
},
icon: const Icon(Icons.save_rounded),
)),
border: const OutlineInputBorder(),
error: (hostInvalidHost || hostInvalidUrl)
? InkWell(
onTap: () {
HapticFeedback.selectionClick();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!
.settingsHostInvalidDetailed(
hostInvalidHost
? "host"
: "url")),
showCloseIcon: true));
},
highlightColor: Colors.transparent,
splashFactory: NoSplash.splashFactory,
child: Row(
children: [
const Icon(Icons.error_rounded,
color: Colors.red),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!
.settingsHostInvalid(hostInvalidHost
? "host"
: "url"),
style:
const TextStyle(color: Colors.red))
],
))
: null,
helper: InkWell(
onTap: () {
HapticFeedback.selectionClick();
},
highlightColor: Colors.transparent,
splashFactory: NoSplash.splashFactory,
child: hostLoading
? Row(
children: [
const Icon(Icons.search_rounded,
color: Colors.grey),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!
.settingsHostChecking,
style: const TextStyle(
color: Colors.grey,
fontFamily: "monospace"))
],
)
: Row(
children: [
const Icon(Icons.check_rounded,
color: Colors.green),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!
.settingsHostValid,
style: const TextStyle(
color: Colors.green,
fontFamily: "monospace"))
],
)))),
title(AppLocalizations.of(context)!.settingsTitleBehavior,
bottom: 24),
TextField(
controller: systemInputController,
keyboardType: TextInputType.multiline,
maxLines: 2,
decoration: InputDecoration(
labelText:
AppLocalizations.of(context)!.settingsSystemMessage,
hintText: "You are a helpful assistant",
suffixIcon: IconButton(
onPressed: () {
HapticFeedback.selectionClick();
prefs?.setString(
"system",
(systemInputController.text.isNotEmpty)
? systemInputController.text
: "You are a helpful assistant");
},
icon: const Icon(Icons.save_rounded),
),
border: const OutlineInputBorder())),
const SizedBox(height: 16),
toggle(AppLocalizations.of(context)!.settingsDisableMarkdown,
(prefs!.getBool("noMarkdown") ?? false), (value) {
HapticFeedback.selectionClick();
prefs!.setBool("noMarkdown", value);
setState(() {});
}),
const SizedBox(height: 8),
Row(children: [
const Icon(Icons.warning_rounded, color: Colors.grey),
const SizedBox(width: 16),
Expanded(
child: Text(
AppLocalizations.of(context)!
.settingsBehaviorNotUpdatedForOlderChats,
style: const TextStyle(color: Colors.grey)))
]),
title(AppLocalizations.of(context)!.settingsTitleInterface),
SegmentedButton(
segments: const [
ButtonSegment(
value: "stream",
label: Text("Stream"),
icon: Icon(Icons.stream_rounded)),
ButtonSegment(
value: "request",
label: Text("Request"),
icon: Icon(Icons.send_rounded))
],
selected: {
prefs!.getString("requestType") ?? "stream"
},
onSelectionChanged: (p0) {
HapticFeedback.selectionClick();
setState(() {
prefs!.setString("requestType", p0.elementAt(0));
});
}),
const SizedBox(height: 16),
toggle(AppLocalizations.of(context)!.settingsGenerateTitles,
(prefs!.getBool("generateTitles") ?? true), (value) {
HapticFeedback.selectionClick();
prefs!.setBool("generateTitles", value);
setState(() {});
}),
toggle(AppLocalizations.of(context)!.settingsAskBeforeDelete,
(prefs!.getBool("askBeforeDeletion") ?? false), (value) {
HapticFeedback.selectionClick();
prefs!.setBool("askBeforeDeletion", value);
setState(() {});
}),
toggle(AppLocalizations.of(context)!.settingsResetOnModelChange,
(prefs!.getBool("resetOnModelSelect") ?? true), (value) {
HapticFeedback.selectionClick();
prefs!.setBool("resetOnModelSelect", value);
setState(() {});
}),
toggle(AppLocalizations.of(context)!.settingsEnableEditing,
(prefs!.getBool("enableEditing") ?? false), (value) {
HapticFeedback.selectionClick();
prefs!.setBool("enableEditing", value);
setState(() {});
}),
toggle(AppLocalizations.of(context)!.settingsShowTips,
(prefs!.getBool("tips") ?? true), (value) {
HapticFeedback.selectionClick();
prefs!.setBool("tips", value);
setState(() {});
}),
const SizedBox(height: 16),
SegmentedButton(
segments: [
ButtonSegment(
value: "dark",
label: Text(AppLocalizations.of(context)!
.settingsBrightnessDark),
icon: const Icon(Icons.brightness_4_rounded)),
ButtonSegment(
value: "system",
label: Text(AppLocalizations.of(context)!
.settingsBrightnessSystem),
icon: const Icon(Icons.brightness_auto_rounded)),
ButtonSegment(
value: "light",
label: Text(AppLocalizations.of(context)!
.settingsBrightnessLight),
icon: const Icon(Icons.brightness_high_rounded))
],
selected: {
prefs!.getString("brightness") ?? "system"
},
onSelectionChanged: (p0) {
HapticFeedback.selectionClick();
var tmp = prefs!.getString("brightness") ?? "system";
prefs!.setString("brightness", p0.elementAt(0));
setState(() {});
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setLocalState) {
return PopScope(
onPopInvoked: (didPop) {
prefs!.setString("brightness", tmp);
setState(() {});
},
child: AlertDialog(
title: Text(AppLocalizations.of(context)!
.settingsBrightnessRestartTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(context)!
.settingsBrightnessRestartDescription),
]),
actions: [
TextButton(
onPressed: () {
HapticFeedback.selectionClick();
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(
context)!
.settingsBrightnessRestartCancel)),
TextButton(
onPressed: () async {
HapticFeedback.selectionClick();
await prefs!.setString(
"brightness",
p0.elementAt(0));
Restart.restartApp();
},
child: Text(AppLocalizations.of(
context)!
.settingsBrightnessRestartRestart))
]));
});
});
}),
title(AppLocalizations.of(context)!.settingsTitleContact),
InkWell(
onTap: () {
launchUrl(
mode: LaunchMode.inAppBrowserView,
Uri.parse(repoUrl));
},
child: Row(children: [
const Icon(SimpleIcons.github),
const SizedBox(width: 16, height: 42),
Expanded(
child: Text(
AppLocalizations.of(context)!.settingsGithub))
])),
InkWell(
onTap: () {
launchUrl(
mode: LaunchMode.inAppBrowserView,
Uri.parse("$repoUrl/issues"));
},
child: Row(children: [
const Icon(Icons.report_rounded),
const SizedBox(width: 16, height: 42),
Expanded(
child: Text(AppLocalizations.of(context)!
.settingsReportIssue))
])),
InkWell(
onTap: () {
launchUrl(
mode: LaunchMode.inAppBrowserView,
Uri.parse(
repoUrl.substring(0, repoUrl.lastIndexOf('/'))));
},
child: Row(children: [
const Icon(Icons.developer_board_rounded),
const SizedBox(width: 16, height: 42),
Expanded(
child: Text(AppLocalizations.of(context)!
.settingsMainDeveloper))
])),
const SizedBox(height: 16),
]))),
);
}
}