ollama-app/lib/settings/interface.dart

489 lines
24 KiB
Dart

import 'package:flutter/material.dart';
import '../main.dart';
import '../worker/haptic.dart';
import '../worker/desktop.dart';
import '../worker/theme.dart';
import '../screen_settings.dart';
import 'package:ollama_app/l10n/gen/app_localizations.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:dartx/dartx.dart';
import 'package:duration_picker/duration_picker.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:url_launcher/url_launcher.dart';
class ScreenSettingsInterface extends StatefulWidget {
const ScreenSettingsInterface({super.key});
@override
State<ScreenSettingsInterface> createState() =>
_ScreenSettingsInterfaceState();
}
String secondsBeautify(double seconds) {
String? endString;
int? endMinutes;
int? endSeconds;
if (seconds > 60) {
endSeconds = seconds.toInt() % 60;
endMinutes = (seconds - endSeconds) ~/ 60;
endString = "${endMinutes}m";
if (endSeconds > 0) {
endString += " ${endSeconds}s";
}
return "($endString)";
} else {
return "";
}
}
class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
@override
Widget build(BuildContext context) {
return WindowBorder(
color: Theme.of(context).colorScheme.surface,
child: Scaffold(
appBar: AppBar(
title: Row(children: [
Text(AppLocalizations.of(context)!.settingsTitleInterface),
Expanded(child: SizedBox(height: 200, child: MoveWindow()))
]),
actions: desktopControlsActions(context)),
body: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 1000),
padding: const EdgeInsets.only(left: 16, right: 16),
child: Column(children: [
Expanded(
child: ListView(children: [
// const SizedBox(height: 8),
toggle(
context,
AppLocalizations.of(context)!.settingsShowModelTags,
(prefs!.getBool("modelTags") ?? false), (value) {
selectionHaptic();
prefs!.setBool("modelTags", value);
setState(() {});
}),
toggle(
context,
AppLocalizations.of(context)!.settingsPreloadModels,
(prefs!.getBool("preloadModel") ?? true), (value) {
selectionHaptic();
prefs!.setBool("preloadModel", value);
setState(() {});
}),
toggle(
context,
AppLocalizations.of(context)!
.settingsResetOnModelChange,
(prefs!.getBool("resetOnModelSelect") ?? true),
(value) {
selectionHaptic();
prefs!.setBool("resetOnModelSelect", value);
setState(() {});
}),
titleDivider(
bottom: desktopLayoutNotRequired(context) ? 38 : 20,
context: context),
SegmentedButton(
segments: [
ButtonSegment(
value: "stream",
label: Text(AppLocalizations.of(context)!
.settingsRequestTypeStream),
icon: const Icon(Icons.stream_rounded)),
ButtonSegment(
value: "request",
label: Text(AppLocalizations.of(context)!
.settingsRequestTypeRequest),
icon: const Icon(Icons.send_rounded))
],
selected: {
prefs!.getString("requestType") ?? "stream"
},
onSelectionChanged: (p0) {
selectionHaptic();
setState(() {
prefs!.setString("requestType", p0.elementAt(0));
});
}),
const SizedBox(height: 16),
toggle(
context,
AppLocalizations.of(context)!.settingsGenerateTitles,
(prefs!.getBool("generateTitles") ?? true), (value) {
selectionHaptic();
prefs!.setBool("generateTitles", value);
setState(() {});
}),
toggle(
context,
AppLocalizations.of(context)!.settingsEnableEditing,
(prefs!.getBool("enableEditing") ?? true), (value) {
selectionHaptic();
prefs!.setBool("enableEditing", value);
setState(() {});
}),
toggle(
context,
AppLocalizations.of(context)!.settingsAskBeforeDelete,
(prefs!.getBool("askBeforeDeletion") ?? false),
(value) {
selectionHaptic();
prefs!.setBool("askBeforeDeletion", value);
setState(() {});
}),
toggle(
context,
AppLocalizations.of(context)!.settingsShowTips,
(prefs!.getBool("tips") ?? true), (value) {
selectionHaptic();
prefs!.setBool("tips", value);
setState(() {});
}),
titleDivider(context: context),
toggle(
context,
AppLocalizations.of(context)!
.settingsKeepModelLoadedAlways,
int.parse(prefs!.getString("keepAlive") ?? "300") ==
-1, (value) {
selectionHaptic();
setState(() {
if (value) {
prefs!.setString("keepAlive", "-1");
} else {
prefs!.setString("keepAlive", "300");
}
});
}),
toggle(
context,
AppLocalizations.of(context)!
.settingsKeepModelLoadedNever,
int.parse(prefs!.getString("keepAlive") ?? "300") ==
0, (value) {
selectionHaptic();
setState(() {
if (value) {
prefs!.setString("keepAlive", "0");
} else {
prefs!.setString("keepAlive", "300");
}
});
}),
button(
(int.parse(prefs!.getString("keepAlive") ?? "300") >
0)
? AppLocalizations.of(context)!
.settingsKeepModelLoadedSet((int.parse(
prefs!.getString("keepAlive") ??
"300") ~/
60)
.toString())
: AppLocalizations.of(context)!
.settingsKeepModelLoadedFor,
Icons.snooze_rounded, () async {
selectionHaptic();
bool loaded = false;
await showDialog(
context: context,
builder: (context) {
return Dialog(
alignment: desktopLayout(context)
? null
: Alignment.bottomRight,
child: StatefulBuilder(
builder: (context, setLocalState) {
if (int.parse(
prefs!.getString("keepAlive") ??
"0") <=
0 &&
loaded == false) {
prefs!.setString("keepAlive", "0");
WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
setLocalState(() {});
void load() async {
try {
while (int.parse(prefs!
.getString("keepAlive")!) <
300) {
await Future.delayed(
const Duration(
milliseconds: 5));
prefs!.setString(
"keepAlive",
(int.parse(prefs!.getString(
"keepAlive")!) +
30)
.toString());
setLocalState(() {});
setState(() {});
}
prefs!
.setString("keepAlive", "300");
loaded = true;
} catch (_) {
prefs!
.setString("keepAlive", "300");
loaded = true;
}
}
load();
});
} else {
loaded = true;
}
return Padding(
padding: const EdgeInsets.all(16),
child: Theme(
data: (prefs?.getBool(
"useDeviceTheme") ??
false)
? Theme.of(context)
: ThemeData.from(
colorScheme:
ColorScheme.fromSeed(
seedColor: Colors.black,
brightness:
Theme.of(context)
.colorScheme
.brightness)),
child: DurationPicker(
duration: Duration(
seconds: int.parse(prefs!
.getString(
"keepAlive") ??
"300")),
baseUnit: BaseUnit.minute,
lowerBound:
const Duration(minutes: 1),
upperBound:
const Duration(minutes: 60),
onChange: (value) {
if (!loaded) return;
if (value.inSeconds == 0) return;
prefs!.setString("keepAlive",
value.inSeconds.toString());
setLocalState(() {});
setState(() {});
}),
),
);
}));
});
}),
titleDivider(context: context),
button(
AppLocalizations.of(context)!
.settingsTimeoutMultiplier,
Icons.info_rounded,
null,
iconAfterwards: true,
context: context,
alwaysMobileDescription: true,
description:
"\n${AppLocalizations.of(context)!.settingsTimeoutMultiplierDescription}"),
Slider(
value: (prefs!.getDouble("timeoutMultiplier") ?? 1),
min: 0.5,
divisions: 19,
max: 10,
label: (prefs!.getDouble("timeoutMultiplier") ?? 1)
.toString()
.removeSuffix(".0"),
onChanged: (value) {
selectionHaptic();
prefs!.setDouble("timeoutMultiplier", value);
setState(() {});
}),
button(
AppLocalizations.of(context)!
.settingsTimeoutMultiplierExample,
Icons.calculate_rounded,
null,
onlyDesktopDescription: false,
// making it complicated because web is weird and doesn't like to round numbers
description: "\n${() {
var value =
(prefs!.getDouble("timeoutMultiplier") ?? 1);
if (value == 10) {
return "${value.round()}.";
} else {
if (!value.toString().contains(".")) {
return "${value.toString()}.0";
}
return value;
}
}.call()} x 30s = ${((prefs!.getDouble("timeoutMultiplier") ?? 1) * 30).round()}s ${secondsBeautify((prefs!.getDouble("timeoutMultiplier") ?? 1) * 30)}"),
titleDivider(context: context),
toggle(
context,
AppLocalizations.of(context)!
.settingsEnableHapticFeedback,
(prefs!.getBool("enableHaptic") ?? true), (value) {
prefs!.setBool("enableHaptic", value);
selectionHaptic();
setState(() {});
}),
desktopFeature()
? toggle(
context,
AppLocalizations.of(context)!
.settingsMaximizeOnStart,
(prefs!.getBool("maximizeOnStart") ?? false),
(value) {
selectionHaptic();
prefs!.setBool("maximizeOnStart", value);
setState(() {});
})
: const SizedBox.shrink(),
const SizedBox(height: 8),
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) async {
selectionHaptic();
await prefs!
.setString("brightness", p0.elementAt(0));
setMainAppState!(() {});
setState(() {});
}),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: desktopLayoutNotRequired(context) ? 16 : 8),
(colorSchemeLight != null && colorSchemeDark != null)
? SegmentedButton(
segments: [
ButtonSegment(
value: "device",
label: Text(AppLocalizations.of(context)!
.settingsThemeDevice),
icon: const Icon(Icons.devices_rounded)),
ButtonSegment(
value: "ollama",
label: Text(AppLocalizations.of(context)!
.settingsThemeOllama),
icon: const ImageIcon(
AssetImage("assets/logo512.png")))
],
selected: {
(prefs?.getBool("useDeviceTheme") ?? false)
? "device"
: "ollama"
},
onSelectionChanged: (p0) async {
selectionHaptic();
await prefs!.setBool("useDeviceTheme",
p0.elementAt(0) == "device");
setMainAppState!(() {});
setState(() {});
})
: const SizedBox.shrink(),
titleDivider(),
button(
AppLocalizations.of(context)!.settingsTemporaryFixes,
Icons.fast_forward_rounded, () {
selectionHaptic();
showModalBottomSheet(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return Container(
width: double.infinity,
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: desktopLayout(context) ? 16 : 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
button(
AppLocalizations.of(context)!
.settingsTemporaryFixesDescription,
Icons.info_rounded,
null,
color: Colors.grey.harmonizeWith(
Theme.of(context)
.colorScheme
.primary)),
button(
AppLocalizations.of(context)!
.settingsTemporaryFixesInstructions,
Icons.warning_rounded,
null,
color: Colors.orange.harmonizeWith(
Theme.of(context)
.colorScheme
.primary)),
titleDivider(),
// Text(
// AppLocalizations.of(context)!
// .settingsTemporaryFixesNoFixes,
// style: const TextStyle(
// color: Colors.grey)),
toggle(
context,
"Fixing code block not scrollable",
(prefs!.getBool(
"fixCodeblockScroll") ??
false), (value) {
selectionHaptic();
prefs!.setBool(
"fixCodeblockScroll", value);
if ((prefs!.getBool(
"fixCodeblockScroll") ??
false) ==
false) {
prefs!.remove("fixCodeblockScroll");
}
setState(() {});
}, onLongTap: () {
selectionHaptic();
launchUrl(Uri.parse(
"https://github.com/JHubi1/ollama-app/issues/26"));
}),
const SizedBox(height: 16)
]),
);
});
});
}),
const SizedBox(height: 16)
]),
)
])),
)),
);
}
}