Restrict desktop settings width, centralized delete dialog, allow changing titles and deleting non current chats

This commit is contained in:
JHubi1 2024-08-21 00:58:13 +02:00
parent 8813571b65
commit efcca113c8
No known key found for this signature in database
GPG Key ID: 7BF82570CBBBD050
10 changed files with 1210 additions and 1302 deletions

View File

@ -424,7 +424,8 @@ class _MainAppState extends State<MainApp> {
? null ? null
: () async { : () async {
selectionHaptic(); selectionHaptic();
if (!chatAllowed) return; if (!chatAllowed &&
chatUuid == jsonDecode(item)["uuid"]) return;
if (!allowSettings) return; if (!allowSettings) return;
String oldTitle = jsonDecode(item)["title"]; String oldTitle = jsonDecode(item)["title"];
var newTitle = await prompt(context, var newTitle = await prompt(context,
@ -480,6 +481,10 @@ class _MainAppState extends State<MainApp> {
width: 24, width: 24,
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
if (!chatAllowed &&
chatUuid ==
jsonDecode(item)["uuid"])
return;
if (!allowMultipleChats) { if (!allowMultipleChats) {
for (var i = 0; for (var i = 0;
i < i <
@ -511,124 +516,14 @@ class _MainAppState extends State<MainApp> {
return; return;
} }
if (!allowSettings) { if (!allowSettings) {
if (prefs!.getBool( deleteChatDialog(
"askBeforeDeletion") ?? context, setState,
false) { additionalCondition: false,
showDialog( uuid:
context: context, jsonDecode(item)["uuid"],
builder: (context) { popSidebar: true);
return StatefulBuilder(
builder: (context,
setLocalState) {
return AlertDialog(
title: Text(
AppLocalizations.of(
context)!
.deleteDialogTitle),
content: Column(
mainAxisSize:
MainAxisSize
.min,
children: [
Text(AppLocalizations.of(
context)!
.deleteDialogDescription),
]),
actions: [
TextButton(
onPressed:
() {
Navigator.of(
context)
.pop();
},
child: Text(AppLocalizations.of(
context)!
.deleteDialogCancel)),
TextButton(
onPressed:
() {
Navigator.of(
context)
.pop();
for (var i =
0;
i < (prefs!.getStringList("chats") ?? []).length;
i++) {
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])[
"uuid"] ==
jsonDecode(
item)["uuid"]) {
List<String>
tmp =
prefs!.getStringList("chats")!;
tmp.removeAt(
i);
prefs!.setStringList(
"chats",
tmp);
break;
}
}
if (chatUuid ==
jsonDecode(
item)["uuid"]) {
messages =
[];
chatUuid =
null;
if (!desktopLayoutRequired(
context)) {
Navigator.of(context)
.pop();
}
}
setState(
() {});
},
child: Text(AppLocalizations.of(
context)!
.deleteDialogDelete))
]);
});
});
} else {
for (var i = 0;
i <
(prefs!.getStringList(
"chats") ??
[])
.length;
i++) {
if (jsonDecode((prefs!
.getStringList(
"chats") ??
[])[i])["uuid"] ==
jsonDecode(
item)["uuid"]) {
List<String> tmp = prefs!
.getStringList(
"chats")!;
tmp.removeAt(i);
prefs!.setStringList(
"chats", tmp);
break;
}
}
if (chatUuid ==
jsonDecode(item)["uuid"]) {
messages = [];
chatUuid = null;
if (!desktopLayoutRequired(
context)) {
Navigator.of(context).pop();
}
}
setState(() {});
}
return; return;
} }
if (!chatAllowed) return;
if (!desktopLayoutRequired( if (!desktopLayoutRequired(
context)) { context)) {
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -636,7 +531,22 @@ class _MainAppState extends State<MainApp> {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) { builder: (context) {
return Padding( return Container(
decoration: (Theme.of(context)
.brightness ==
Brightness.dark)
? BoxDecoration(
border: Border.all(
color: Colors
.white),
borderRadius: const BorderRadius.only(
topLeft:
Radius.circular(
26),
topRight:
Radius.circular(
26)))
: null,
padding: padding:
const EdgeInsets.only( const EdgeInsets.only(
left: 16, left: 16,
@ -655,68 +565,11 @@ class _MainAppState extends State<MainApp> {
() { () {
Navigator.of(context) Navigator.of(context)
.pop(); .pop();
if (prefs!.getBool("askBeforeDeletion") ?? deleteChatDialog(
false) { context,
showDialog( setState,
context: context, uuid: jsonDecode(item)["uuid"],
builder: (context) { popSidebar: true);
return StatefulBuilder(builder: (context, setLocalState) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteDialogTitle),
content: Column(mainAxisSize: MainAxisSize.min, children: [
Text(AppLocalizations.of(context)!.deleteDialogDescription),
]),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(context)!.deleteDialogCancel)),
TextButton(
onPressed: () {
Navigator.of(context).pop();
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == jsonDecode(item)["uuid"]) {
List<String> tmp = prefs!.getStringList("chats")!;
tmp.removeAt(i);
prefs!.setStringList("chats", tmp);
break;
}
}
if (chatUuid == jsonDecode(item)["uuid"]) {
messages = [];
chatUuid = null;
if (!desktopLayoutRequired(context)) {
Navigator.of(context).pop();
}
}
setState(() {});
},
child: Text(AppLocalizations.of(context)!.deleteDialogDelete))
]);
});
});
} else {
for (var i = 0;
i < (prefs!.getStringList("chats") ?? []).length;
i++) {
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == jsonDecode(item)["uuid"]) {
List<String> tmp = prefs!.getStringList("chats")!;
tmp.removeAt(i);
prefs!.setStringList("chats", tmp);
break;
}
}
if (chatUuid ==
jsonDecode(item)["uuid"]) {
messages = [];
chatUuid = null;
if (!desktopLayoutRequired(context)) {
Navigator.of(context).pop();
}
}
setState(() {});
}
}, },
icon: const Icon(Icons icon: const Icon(Icons
.delete_forever_rounded), .delete_forever_rounded),
@ -793,48 +646,10 @@ class _MainAppState extends State<MainApp> {
? DismissDirection.startToEnd ? DismissDirection.startToEnd
: DismissDirection.none, : DismissDirection.none,
confirmDismiss: (direction) async { confirmDismiss: (direction) async {
bool returnValue = false; if (!chatAllowed && chatUuid == jsonDecode(item)["uuid"])
if (!chatAllowed) return false; return false;
return await deleteChatDialog(context, setState,
if (prefs!.getBool("askBeforeDeletion") ?? false) { takeAction: false);
await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setLocalState) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!
.deleteDialogTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(context)!
.deleteDialogDescription),
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
returnValue = false;
},
child: Text(AppLocalizations.of(context)!
.deleteDialogCancel)),
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
returnValue = true;
},
child: Text(AppLocalizations.of(context)!
.deleteDialogDelete))
]);
});
});
} else {
returnValue = true;
}
return returnValue;
}, },
onDismissed: (direction) { onDismissed: (direction) {
selectionHaptic(); selectionHaptic();
@ -937,15 +752,18 @@ class _MainAppState extends State<MainApp> {
resetSystemNavigation(context); resetSystemNavigation(context);
Widget selector = InkWell( Widget selector = InkWell(
onTap: () { onTap: !useModel
if (host == null) { ? () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( if (host == null) {
content: Text(AppLocalizations.of(context)!.noHostSelected), ScaffoldMessenger.of(context).showSnackBar(SnackBar(
showCloseIcon: true)); content:
return; Text(AppLocalizations.of(context)!.noHostSelected),
} showCloseIcon: true));
setModel(context, setState); return;
}, }
setModel(context, setState);
}
: null,
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
enableFeedback: false, enableFeedback: false,
@ -1053,90 +871,8 @@ class _MainAppState extends State<MainApp> {
onPressed: () { onPressed: () {
selectionHaptic(); selectionHaptic();
if (!chatAllowed) return; if (!chatAllowed) return;
deleteChatDialog(context, setState,
if (prefs!.getBool("askBeforeDeletion") ?? additionalCondition: messages.isNotEmpty);
// ignore: dead_code
false && messages.isNotEmpty) {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setLocalState) {
return AlertDialog(
title: Text(
AppLocalizations.of(context)!
.deleteDialogTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(context)!
.deleteDialogDescription),
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context)!
.deleteDialogCancel)),
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
for (var i = 0;
i <
(prefs!.getStringList(
"chats") ??
[])
.length;
i++) {
if (jsonDecode((prefs!
.getStringList(
"chats") ??
[])[i])["uuid"] ==
chatUuid) {
List<String> tmp = prefs!
.getStringList(
"chats")!;
tmp.removeAt(i);
prefs!.setStringList(
"chats", tmp);
break;
}
}
messages = [];
chatUuid = null;
setState(() {});
},
child: Text(
AppLocalizations.of(context)!
.deleteDialogDelete))
]);
});
});
} else {
for (var i = 0;
i <
(prefs!.getStringList("chats") ?? [])
.length;
i++) {
if (jsonDecode((prefs!.getStringList("chats") ??
[])[i])["uuid"] ==
chatUuid) {
List<String> tmp =
prefs!.getStringList("chats")!;
tmp.removeAt(i);
prefs!.setStringList("chats", tmp);
break;
}
}
messages = [];
chatUuid = null;
}
setState(() {});
}, },
icon: const Icon(Icons.restart_alt_rounded)) icon: const Icon(Icons.restart_alt_rounded))
: const SizedBox.shrink() : const SizedBox.shrink()
@ -1533,189 +1269,198 @@ class _MainAppState extends State<MainApp> {
context: context, context: context,
builder: (context) { builder: (context) {
return Container( return Container(
decoration: (Theme.of(context)
.brightness ==
Brightness.dark)
? BoxDecoration(
border: Border.all(
color:
Colors.white),
borderRadius:
const BorderRadius.only(
topLeft: Radius
.circular(
26),
topRight:
Radius.circular(
26)))
: null,
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16, left: 16,
right: 16, right: 16,
top: 16), top: 16),
child: Column( child:
mainAxisSize: Column(mainAxisSize: MainAxisSize.min, children: [
MainAxisSize.min, (prefs?.getBool(
children: [ "voiceModeEnabled") ??
(prefs?.getBool( false)
"voiceModeEnabled") ?? ? SizedBox(
false) width:
? SizedBox( double.infinity,
width: double child: OutlinedButton
.infinity, .icon(
child: OutlinedButton onPressed:
.icon( () async {
onPressed: selectionHaptic();
() async { Navigator.of(
selectionHaptic(); context)
Navigator.of(context) .pop();
.pop(); setMainState =
setMainState = setState;
setState; settingsOpen =
settingsOpen = true;
true; logoVisible =
logoVisible = false;
false; Navigator.of(
Navigator.of(context).push(MaterialPageRoute( context)
.push(MaterialPageRoute(
builder: (context) => builder: (context) =>
const ScreenVoice())); const ScreenVoice()));
},
icon: const Icon(
Icons
.headphones_rounded),
label: Text(
AppLocalizations.of(context)!
.settingsTitleVoice)))
: const SizedBox
.shrink(),
(prefs?.getBool(
"voiceModeEnabled") ??
false)
? const SizedBox(
height: 8)
: const SizedBox
.shrink(),
SizedBox(
width:
double.infinity,
child: OutlinedButton
.icon(
onPressed:
() async {
selectionHaptic();
Navigator.of(
context)
.pop();
final result =
await ImagePicker()
.pickImage(
source: ImageSource
.camera,
);
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(
() {});
selectionHaptic();
}, },
icon: const Icon( icon: const Icon(
Icons Icons
.photo_camera_rounded), .headphones_rounded),
label: Text(AppLocalizations.of( label: Text(AppLocalizations.of(
context)! context)!
.settingsTitleVoice)))
: const SizedBox.shrink(),
(prefs?.getBool(
"voiceModeEnabled") ??
false)
? const SizedBox(
height: 8)
: const SizedBox.shrink(),
SizedBox(
width: double.infinity,
child:
OutlinedButton.icon(
onPressed:
() async {
selectionHaptic();
Navigator.of(
context)
.pop();
final result =
await ImagePicker()
.pickImage(
source:
ImageSource
.camera,
);
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(() {});
selectionHaptic();
},
icon: const Icon(Icons
.photo_camera_rounded),
label: Text(
AppLocalizations.of(
context)!
.takeImage))), .takeImage))),
const SizedBox(height: 8), const SizedBox(height: 8),
SizedBox( SizedBox(
width: width: double.infinity,
double.infinity, child:
child: OutlinedButton OutlinedButton.icon(
.icon( onPressed:
onPressed: () async {
() async { selectionHaptic();
selectionHaptic();
Navigator.of( Navigator.of(
context) context)
.pop(); .pop();
final result = final result =
await ImagePicker() await ImagePicker()
.pickImage( .pickImage(
source: ImageSource source:
ImageSource
.gallery, .gallery,
); );
if (result == if (result ==
null) { null) {
return; return;
} }
final bytes = final bytes =
await result await result
.readAsBytes(); .readAsBytes();
final image = final image =
await decodeImageFromList( await decodeImageFromList(
bytes); bytes);
final message = final message =
types types
.ImageMessage( .ImageMessage(
author: author: user,
user, createdAt: DateTime
createdAt: .now()
DateTime.now() .millisecondsSinceEpoch,
.millisecondsSinceEpoch, height: image
height: image .height
.height .toDouble(),
.toDouble(), id: const Uuid()
id: const Uuid() .v4(),
.v4(), name: result
name: result .name,
.name, size: bytes
size: bytes .length,
.length, uri: result
uri: result .path,
.path, width: image
width: image .width
.width .toDouble(),
.toDouble(), );
);
messages.insert( messages.insert(
0, 0, message);
message); setState(() {});
setState( selectionHaptic();
() {}); },
selectionHaptic(); icon: const Icon(Icons
}, .image_rounded),
icon: const Icon( label: Text(
Icons AppLocalizations.of(
.image_rounded),
label: Text(AppLocalizations.of(
context)! context)!
.uploadImage))) .uploadImage)))
])); ]));
}); });
}, },
l10n: ChatL10nEn( l10n: ChatL10nEn(

View File

@ -125,14 +125,15 @@ Widget title(String text, {double top = 16, double bottom = 16}) {
Padding( Padding(
padding: const EdgeInsets.only(left: 24, right: 24), padding: const EdgeInsets.only(left: 24, right: 24),
child: Text(text)), child: Text(text)),
const Expanded(child: Divider()) const Expanded(child: Divider(height: 1))
])); ]));
} }
Widget titleDivider({double? top, double? bottom, BuildContext? context}) { Widget titleDivider({double? top, double? bottom, BuildContext? context}) {
top ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16; top ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16;
bottom ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16; bottom ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16;
return Padding( return AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.only(left: 8, right: 8, top: top, bottom: bottom), padding: EdgeInsets.only(left: 8, right: 8, top: top, bottom: bottom),
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -143,13 +144,12 @@ Widget verticalTitleDivider(
{double? left, double? right, BuildContext? context}) { {double? left, double? right, BuildContext? context}) {
left ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16; left ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16;
right ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16; right ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16;
return Padding( return AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.only(left: left, right: right, top: 8, bottom: 8), padding: EdgeInsets.only(left: left, right: right, top: 8, bottom: 8),
child: const Row(mainAxisSize: MainAxisSize.max, children: [ child: const Row(
// Expanded(child: mainAxisSize: MainAxisSize.max,
VerticalDivider() children: [VerticalDivider(width: 1)]));
// ),
]));
} }
Widget button(String text, IconData? icon, void Function()? onPressed, Widget button(String text, IconData? icon, void Function()? onPressed,
@ -359,6 +359,7 @@ class _ScreenSettingsState extends State<ScreenSettings> {
TextField( TextField(
controller: hostInputController, controller: hostInputController,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
autofillHints: const [AutofillHints.url],
readOnly: useHost, readOnly: useHost,
onSubmitted: (value) { onSubmitted: (value) {
selectionHaptic(); selectionHaptic();

View File

@ -42,123 +42,130 @@ class _ScreenSettingsAboutState extends State<ScreenSettingsAbout> {
Expanded(child: SizedBox(height: 200, child: MoveWindow())) Expanded(child: SizedBox(height: 200, child: MoveWindow()))
]), ]),
actions: desktopControlsActions(context)), actions: desktopControlsActions(context)),
body: Padding( body: Center(
padding: const EdgeInsets.only(left: 16, right: 16), child: Container(
child: Column(children: [ constraints: const BoxConstraints(maxWidth: 1000),
Expanded( padding: const EdgeInsets.only(left: 16, right: 16),
child: ListView(children: [ child: Column(children: [
// const SizedBox(height: 8), Expanded(
button( child: ListView(children: [
AppLocalizations.of(context)! // const SizedBox(height: 8),
.settingsVersion(currentVersion ?? ""), button(
Icons.verified_rounded, AppLocalizations.of(context)!
null), .settingsVersion(currentVersion ?? ""),
(updateStatus == "notAvailable") Icons.verified_rounded,
? const SizedBox.shrink() null),
: button( (updateStatus == "notAvailable")
(!updateChecked ? const SizedBox.shrink()
? AppLocalizations.of(context)! : button(
.settingsUpdateCheck (!updateChecked
: updateLoading ? AppLocalizations.of(context)!
? AppLocalizations.of(context)! .settingsUpdateCheck
.settingsUpdateChecking : updateLoading
: (updateStatus == "rateLimit") ? AppLocalizations.of(context)!
? AppLocalizations.of(context)! .settingsUpdateChecking
.settingsUpdateRateLimit : (updateStatus == "rateLimit")
: (updateStatus != "ok") ? AppLocalizations.of(context)!
? AppLocalizations.of(context)! .settingsUpdateRateLimit
.settingsUpdateIssue : (updateStatus != "ok")
: (Version.parse(latestVersion ?? ? AppLocalizations.of(context)!
"1.0.0") > .settingsUpdateIssue
Version.parse( : (Version.parse(latestVersion ??
currentVersion ?? "1.0.0") >
"2.0.0")) Version.parse(
? AppLocalizations.of(context)! currentVersion ??
.settingsUpdateAvailable( "2.0.0"))
latestVersion!) ? AppLocalizations.of(
: AppLocalizations.of(context)! context)!
.settingsUpdateLatest), .settingsUpdateAvailable(
((updateStatus != "ok") latestVersion!)
? Icons.warning_rounded : AppLocalizations.of(
: (Version.parse(latestVersion ?? "1.0.0") > context)!
Version.parse( .settingsUpdateLatest),
currentVersion ?? "2.0.0")) ((updateStatus != "ok")
? Icons.info_outline_rounded ? Icons.warning_rounded
: Icons.update_rounded), () { : (Version.parse(latestVersion ?? "1.0.0") >
if (updateLoading) return; Version.parse(
selectionHaptic(); currentVersion ?? "2.0.0"))
if ((Version.parse(latestVersion ?? "1.0.0") > ? Icons.info_outline_rounded
Version.parse(currentVersion ?? "2.0.0")) && : Icons.update_rounded), () {
(updateStatus == "ok")) { if (updateLoading) return;
updateDialog(context, title); selectionHaptic();
} else { if ((Version.parse(latestVersion ?? "1.0.0") >
checkUpdate(setState); Version.parse(
return; currentVersion ?? "2.0.0")) &&
} (updateStatus == "ok")) {
}), updateDialog(context, title);
(updateStatus == "notAvailable") } else {
? const SizedBox.shrink() checkUpdate(setState);
: toggle( return;
context, }
AppLocalizations.of(context)! }),
.settingsCheckForUpdates, (updateStatus == "notAvailable")
(prefs!.getBool("checkUpdateOnSettingsOpen") ?? ? const SizedBox.shrink()
false), (value) { : toggle(
selectionHaptic(); context,
prefs!.setBool("checkUpdateOnSettingsOpen", value); AppLocalizations.of(context)!
setState(() {}); .settingsCheckForUpdates,
}), (prefs!.getBool("checkUpdateOnSettingsOpen") ??
titleDivider(context: context), false), (value) {
button(AppLocalizations.of(context)!.settingsGithub, selectionHaptic();
SimpleIcons.github, () { prefs!
selectionHaptic(); .setBool("checkUpdateOnSettingsOpen", value);
launchUrl( setState(() {});
mode: LaunchMode.inAppBrowserView, }),
Uri.parse(repoUrl)); titleDivider(context: context),
}), button(AppLocalizations.of(context)!.settingsGithub,
button(AppLocalizations.of(context)!.settingsReportIssue, SimpleIcons.github, () {
Icons.report_rounded, () { selectionHaptic();
selectionHaptic(); launchUrl(
launchUrl( mode: LaunchMode.inAppBrowserView,
mode: LaunchMode.inAppBrowserView, Uri.parse(repoUrl));
Uri.parse("$repoUrl/issues")); }),
}), button(AppLocalizations.of(context)!.settingsReportIssue,
button(AppLocalizations.of(context)!.settingsLicenses, Icons.report_rounded, () {
Icons.gavel_rounded, () { selectionHaptic();
selectionHaptic(); launchUrl(
String legal = "Copyright 2024 JHubi1"; mode: LaunchMode.inAppBrowserView,
Widget icon = const Padding( Uri.parse("$repoUrl/issues"));
padding: EdgeInsets.all(16), }),
child: ImageIcon(AssetImage("assets/logo512.png"), button(AppLocalizations.of(context)!.settingsLicenses,
size: 48), Icons.gavel_rounded, () {
); selectionHaptic();
if (desktopFeature()) { String legal = "Copyright 2024 JHubi1";
showDialog( Widget icon = const Padding(
context: context, padding: EdgeInsets.all(16),
builder: (context) { child: ImageIcon(AssetImage("assets/logo512.png"),
return Dialog( size: 48),
child: ClipRRect( );
borderRadius: BorderRadius.circular(28), if (desktopFeature()) {
child: LicensePage( showDialog(
applicationName: "Ollama App", context: context,
applicationVersion: currentVersion, builder: (context) {
applicationIcon: icon, return Dialog(
applicationLegalese: legal), child: ClipRRect(
)); borderRadius: BorderRadius.circular(28),
}); child: LicensePage(
} else { applicationName: "Ollama App",
showLicensePage( applicationVersion: currentVersion,
context: context, applicationIcon: icon,
applicationName: "Ollama App", applicationLegalese: legal),
applicationVersion: currentVersion, ));
applicationIcon: icon, });
applicationLegalese: legal); } else {
} showLicensePage(
}), context: context,
const SizedBox(height: 16) applicationName: "Ollama App",
]), applicationVersion: currentVersion,
) applicationIcon: icon,
]))), applicationLegalese: legal);
}
}),
const SizedBox(height: 16)
]),
)
])),
)),
); );
} }
} }

View File

@ -37,71 +37,75 @@ class _ScreenSettingsBehaviorState extends State<ScreenSettingsBehavior> {
Expanded(child: SizedBox(height: 200, child: MoveWindow())) Expanded(child: SizedBox(height: 200, child: MoveWindow()))
]), ]),
actions: desktopControlsActions(context)), actions: desktopControlsActions(context)),
body: Padding( body: Center(
padding: const EdgeInsets.only(left: 16, right: 16), child: Container(
child: Column(children: [ constraints: const BoxConstraints(maxWidth: 1000),
Expanded( padding: const EdgeInsets.only(left: 16, right: 16),
child: ListView(children: [ child: Column(children: [
const SizedBox(height: 8), Expanded(
TextField( child: ListView(children: [
controller: systemInputController, const SizedBox(height: 8),
keyboardType: TextInputType.multiline, TextField(
maxLines: desktopLayoutNotRequired(context) ? 5 : 2, controller: systemInputController,
decoration: InputDecoration( keyboardType: TextInputType.multiline,
labelText: AppLocalizations.of(context)! maxLines: desktopLayoutNotRequired(context) ? 5 : 2,
.settingsSystemMessage, decoration: InputDecoration(
hintText: "You are a helpful assistant", labelText: AppLocalizations.of(context)!
suffixIcon: IconButton( .settingsSystemMessage,
tooltip: alignLabelWithHint: true,
AppLocalizations.of(context)!.tooltipSave, hintText: "You are a helpful assistant",
onPressed: () { suffixIcon: IconButton(
selectionHaptic(); tooltip:
prefs?.setString( AppLocalizations.of(context)!.tooltipSave,
"system", onPressed: () {
(systemInputController.text.isNotEmpty) selectionHaptic();
? systemInputController.text prefs?.setString(
: "You are a helpful assistant"); "system",
}, (systemInputController.text.isNotEmpty)
icon: const Icon(Icons.save_rounded), ? systemInputController.text
), : "You are a helpful assistant");
border: const OutlineInputBorder())), },
const SizedBox(height: 16), icon: const Icon(Icons.save_rounded),
toggle( ),
context, border: const OutlineInputBorder())),
AppLocalizations.of(context)!.settingsUseSystem, const SizedBox(height: 16),
(prefs!.getBool("useSystem") ?? true), toggle(
(value) { context,
selectionHaptic(); AppLocalizations.of(context)!.settingsUseSystem,
prefs!.setBool("useSystem", value); (prefs!.getBool("useSystem") ?? true),
setState(() {}); (value) {
}, selectionHaptic();
icon: const Icon(Icons.info_outline_rounded), prefs!.setBool("useSystem", value);
onLongTap: () { setState(() {});
selectionHaptic(); },
ScaffoldMessenger.of(context).showSnackBar(SnackBar( icon: const Icon(Icons.info_outline_rounded),
content: Text(AppLocalizations.of(context)! onLongTap: () {
.settingsUseSystemDescription), selectionHaptic();
showCloseIcon: true)); ScaffoldMessenger.of(context).showSnackBar(SnackBar(
}), content: Text(AppLocalizations.of(context)!
toggle( .settingsUseSystemDescription),
context, showCloseIcon: true));
AppLocalizations.of(context)!.settingsDisableMarkdown, }),
(prefs!.getBool("noMarkdown") ?? false), (value) { toggle(
selectionHaptic(); context,
prefs!.setBool("noMarkdown", value); AppLocalizations.of(context)!.settingsDisableMarkdown,
setState(() {}); (prefs!.getBool("noMarkdown") ?? false), (value) {
}) selectionHaptic();
]), prefs!.setBool("noMarkdown", value);
), setState(() {});
const SizedBox(height: 8), })
button( ]),
AppLocalizations.of(context)! ),
.settingsBehaviorNotUpdatedForOlderChats, const SizedBox(height: 8),
Icons.info_rounded, button(
null, AppLocalizations.of(context)!
color: Colors.grey .settingsBehaviorNotUpdatedForOlderChats,
.harmonizeWith(Theme.of(context).colorScheme.primary)) Icons.info_rounded,
]))), null,
color: Colors.grey
.harmonizeWith(Theme.of(context).colorScheme.primary))
])),
)),
); );
} }
} }

View File

@ -36,192 +36,204 @@ class _ScreenSettingsExportState extends State<ScreenSettingsExport> {
Expanded(child: SizedBox(height: 200, child: MoveWindow())) Expanded(child: SizedBox(height: 200, child: MoveWindow()))
]), ]),
actions: desktopControlsActions(context)), actions: desktopControlsActions(context)),
body: Padding( body: Center(
padding: const EdgeInsets.only(left: 16, right: 16), child: Container(
child: Column(children: [ constraints: const BoxConstraints(maxWidth: 1000),
Expanded( padding: const EdgeInsets.only(left: 16, right: 16),
child: ListView(children: [ child: Column(children: [
// const SizedBox(height: 8), Expanded(
button(AppLocalizations.of(context)!.settingsExportChats, child: ListView(children: [
Icons.upload_rounded, () async { // const SizedBox(height: 8),
selectionHaptic(); button(AppLocalizations.of(context)!.settingsExportChats,
var name = Icons.upload_rounded, () async {
"ollama-export-${DateFormat('yyyy-MM-dd-H-m-s').format(DateTime.now())}.json";
var content =
jsonEncode(prefs!.getStringList("chats") ?? []);
if (kIsWeb) {
// web fallback
final bytes = utf8.encode(content);
final blob = html.Blob([bytes]);
final url = html.Url.createObjectUrlFromBlob(blob);
final anchor = html.document.createElement("a")
as html.AnchorElement
..href = url
..style.display = "none"
..download = name;
html.document.body!.children.add(anchor);
anchor.click();
html.document.body!.children.remove(anchor);
html.Url.revokeObjectUrl(url);
} else {
String? path = "";
try {
path = (await file_selector
.getSaveLocation(acceptedTypeGroups: [
const file_selector.XTypeGroup(
label: "Ollama App File", extensions: ["json"])
], suggestedName: name))
?.path;
} catch (_) {
path = await FilePicker.platform.saveFile(
type: FileType.custom,
allowedExtensions: ["json"],
fileName: name,
bytes: utf8.encode(jsonEncode(
prefs!.getStringList("chats") ?? [])));
}
selectionHaptic(); selectionHaptic();
if (path == null) return; var name =
if (desktopFeature()) { "ollama-export-${DateFormat('yyyy-MM-dd-H-m-s').format(DateTime.now())}.json";
File(path).writeAsString(content); var content =
jsonEncode(prefs!.getStringList("chats") ?? []);
if (kIsWeb) {
// web fallback
final bytes = utf8.encode(content);
final blob = html.Blob([bytes]);
final url = html.Url.createObjectUrlFromBlob(blob);
final anchor = html.document.createElement("a")
as html.AnchorElement
..href = url
..style.display = "none"
..download = name;
html.document.body!.children.add(anchor);
anchor.click();
html.document.body!.children.remove(anchor);
html.Url.revokeObjectUrl(url);
} else {
String? path = "";
try {
path = (await file_selector
.getSaveLocation(acceptedTypeGroups: [
const file_selector.XTypeGroup(
label: "Ollama App File",
extensions: ["json"])
], suggestedName: name))
?.path;
} catch (_) {
path = await FilePicker.platform.saveFile(
type: FileType.custom,
allowedExtensions: ["json"],
fileName: name,
bytes: utf8.encode(jsonEncode(
prefs!.getStringList("chats") ?? [])));
}
selectionHaptic();
if (path == null) return;
if (desktopFeature()) {
File(path).writeAsString(content);
}
} }
} // ignore: use_build_context_synchronously
// ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar( // ignore: use_build_context_synchronously
// ignore: use_build_context_synchronously content: Text(AppLocalizations.of(context)!
content: Text(AppLocalizations.of(context)! .settingsExportChatsSuccess),
.settingsExportChatsSuccess), showCloseIcon: true));
showCloseIcon: true)); }),
}), allowMultipleChats
allowMultipleChats ? button(
? button( AppLocalizations.of(context)!.settingsImportChats,
AppLocalizations.of(context)!.settingsImportChats, Icons.download_rounded, () {
Icons.download_rounded, () { selectionHaptic();
selectionHaptic(); showDialog(
showDialog( context: context,
context: context, builder: (context) {
builder: (context) { return AlertDialog(
return AlertDialog( surfaceTintColor:
title: Text(AppLocalizations.of(context)! (Theme.of(context).brightness ==
.settingsImportChatsTitle), Brightness.dark)
content: Text( ? Colors.grey[800]
AppLocalizations.of(context)! : null,
.settingsImportChatsDescription), title: Text(
actions: [ AppLocalizations.of(context)!
TextButton( .settingsImportChatsTitle),
onPressed: () { content: Text(AppLocalizations.of(
selectionHaptic(); context)!
Navigator.of(context).pop(); .settingsImportChatsDescription),
}, actions: [
child: Text(AppLocalizations.of( TextButton(
context)! onPressed: () {
.settingsImportChatsCancel)), selectionHaptic();
TextButton( Navigator.of(context).pop();
onPressed: () async { },
selectionHaptic(); child: Text(AppLocalizations.of(
String content; context)!
try { .settingsImportChatsCancel)),
if (kIsWeb) { TextButton(
throw Exception( onPressed: () async {
"web must use file picker"); selectionHaptic();
} String content;
file_selector.XFile? result =
await file_selector.openFile(
acceptedTypeGroups: [
const file_selector
.XTypeGroup(
label:
"Ollama App File",
extensions: ["json"])
]);
if (result == null) {
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
return;
}
content =
await result.readAsString();
} catch (_) {
FilePickerResult? result =
await FilePicker.platform
.pickFiles(
type:
FileType.custom,
allowedExtensions: [
"json"
]);
if (result == null) {
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
return;
}
try { try {
File file = File(result if (kIsWeb) {
.files.single.path!); throw Exception(
content = "web must use file picker");
await file.readAsString(); }
file_selector.XFile? result =
await file_selector.openFile(
acceptedTypeGroups: [
const file_selector
.XTypeGroup(
label:
"Ollama App File",
extensions: [
"json"
])
]);
if (result == null) {
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
return;
}
content = await result
.readAsString();
} catch (_) { } catch (_) {
// web fallback FilePickerResult? result =
content = utf8.decode(result await FilePicker.platform
.files .pickFiles(
.single type: FileType
.bytes as List<int>); .custom,
allowedExtensions: [
"json"
]);
if (result == null) {
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
return;
}
try {
File file = File(result
.files.single.path!);
content = await file
.readAsString();
} catch (_) {
// web fallback
content = utf8.decode(result
.files
.single
.bytes as List<int>);
}
} }
} List<dynamic> tmpHistory =
List<dynamic> tmpHistory = jsonDecode(content);
jsonDecode(content); List<String> history = [];
List<String> history = [];
for (var i = 0; for (var i = 0;
i < tmpHistory.length; i < tmpHistory.length;
i++) { i++) {
history.add(tmpHistory[i]); history.add(tmpHistory[i]);
} }
prefs!.setStringList( prefs!.setStringList(
"chats", history); "chats", history);
messages = []; messages = [];
chatUuid = null; chatUuid = null;
setState(() {}); setState(() {});
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.of(context).pop(); Navigator.of(context).pop();
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.of(context).pop(); Navigator.of(context).pop();
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.of(context).pop(); Navigator.of(context).pop();
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(SnackBar( .showSnackBar(SnackBar(
content: Text(AppLocalizations content: Text(AppLocalizations
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
.of(context)! .of(context)!
.settingsImportChatsSuccess), .settingsImportChatsSuccess),
showCloseIcon: true)); showCloseIcon: true));
}, },
child: Text( child: Text(AppLocalizations.of(
AppLocalizations.of(context)! context)!
.settingsImportChatsImport)) .settingsImportChatsImport))
]); ]);
}); });
}) })
: const SizedBox.shrink() : const SizedBox.shrink()
]), ]),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
button(AppLocalizations.of(context)!.settingsExportInfo, button(AppLocalizations.of(context)!.settingsExportInfo,
Icons.info_rounded, null, Icons.info_rounded, null,
color: Colors.grey color: Colors.grey.harmonizeWith(
.harmonizeWith(Theme.of(context).colorScheme.primary)), Theme.of(context).colorScheme.primary)),
button(AppLocalizations.of(context)!.settingsExportWarning, button(AppLocalizations.of(context)!.settingsExportWarning,
Icons.warning_rounded, null, Icons.warning_rounded, null,
color: Colors.orange color: Colors.orange
.harmonizeWith(Theme.of(context).colorScheme.primary)) .harmonizeWith(Theme.of(context).colorScheme.primary))
]))), ])),
)),
); );
} }
} }

View File

@ -36,460 +36,487 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
Expanded(child: SizedBox(height: 200, child: MoveWindow())) Expanded(child: SizedBox(height: 200, child: MoveWindow()))
]), ]),
actions: desktopControlsActions(context)), actions: desktopControlsActions(context)),
body: Padding( body: Center(
padding: const EdgeInsets.only(left: 16, right: 16), child: Container(
child: Column(children: [ constraints: const BoxConstraints(maxWidth: 1000),
Expanded( padding: const EdgeInsets.only(left: 16, right: 16),
child: ListView(children: [ child: Column(children: [
// const SizedBox(height: 8), Expanded(
toggle( child: ListView(children: [
context, // const SizedBox(height: 8),
AppLocalizations.of(context)!.settingsShowModelTags, toggle(
(prefs!.getBool("modelTags") ?? false), (value) { context,
selectionHaptic(); AppLocalizations.of(context)!.settingsShowModelTags,
prefs!.setBool("modelTags", value); (prefs!.getBool("modelTags") ?? false), (value) {
setState(() {}); selectionHaptic();
}), prefs!.setBool("modelTags", value);
toggle( setState(() {});
context, }),
AppLocalizations.of(context)!.settingsPreloadModels, toggle(
(prefs!.getBool("preloadModel") ?? true), (value) { context,
selectionHaptic(); AppLocalizations.of(context)!.settingsPreloadModels,
prefs!.setBool("preloadModel", value); (prefs!.getBool("preloadModel") ?? true), (value) {
setState(() {}); selectionHaptic();
}), prefs!.setBool("preloadModel", value);
toggle( setState(() {});
context, }),
AppLocalizations.of(context)! toggle(
.settingsResetOnModelChange, context,
(prefs!.getBool("resetOnModelSelect") ?? true), AppLocalizations.of(context)!
(value) { .settingsResetOnModelChange,
selectionHaptic(); (prefs!.getBool("resetOnModelSelect") ?? true),
prefs!.setBool("resetOnModelSelect", value); (value) {
setState(() {}); selectionHaptic();
}), prefs!.setBool("resetOnModelSelect", value);
titleDivider( setState(() {});
bottom: desktopLayoutNotRequired(context) ? 38 : 20, }),
context: context), titleDivider(
SegmentedButton( bottom: desktopLayoutNotRequired(context) ? 38 : 20,
segments: [ context: context),
ButtonSegment( SegmentedButton(
value: "stream", segments: [
label: Text(AppLocalizations.of(context)! ButtonSegment(
.settingsRequestTypeStream), value: "stream",
icon: const Icon(Icons.stream_rounded)), label: Text(AppLocalizations.of(context)!
ButtonSegment( .settingsRequestTypeStream),
value: "request", icon: const Icon(Icons.stream_rounded)),
label: Text(AppLocalizations.of(context)! ButtonSegment(
.settingsRequestTypeRequest), value: "request",
icon: const Icon(Icons.send_rounded)) label: Text(AppLocalizations.of(context)!
], .settingsRequestTypeRequest),
selected: { icon: const Icon(Icons.send_rounded))
prefs!.getString("requestType") ?? "stream" ],
}, selected: {
onSelectionChanged: (p0) { prefs!.getString("requestType") ?? "stream"
selectionHaptic(); },
setState(() { onSelectionChanged: (p0) {
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)),
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),
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(); selectionHaptic();
prefs!.setBool("maximizeOnStart", value); setState(() {
setState(() {}); prefs!.setString("requestType", p0.elementAt(0));
})
: 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) {
selectionHaptic();
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: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(
context)!
.settingsBrightnessRestartCancel)),
TextButton(
onPressed: () async {
selectionHaptic();
await prefs!.setString(
"brightness",
p0.elementAt(0));
if (desktopFeature()) {
exit(0);
} else {
Restart.restartApp();
}
},
child: Text(AppLocalizations.of(
context)!
.settingsBrightnessRestartRestart))
]));
});
});
}),
const SizedBox(height: 8),
!kIsWeb
? 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) {
selectionHaptic();
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setLocalState) {
return AlertDialog(
title: Text(
AppLocalizations.of(context)!
.settingsThemeRestartTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(
context)!
.settingsThemeRestartDescription),
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(
context)!
.settingsThemeRestartCancel)),
TextButton(
onPressed: () async {
selectionHaptic();
await prefs!.setBool(
"useDeviceTheme",
p0.elementAt(0) ==
"device");
if (desktopFeature()) {
exit(0);
} else {
Restart.restartApp();
}
},
child: Text(AppLocalizations.of(
context)!
.settingsThemeRestartRestart))
]);
});
});
})
: 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),
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(
surfaceTintColor: (Theme.of(context).brightness == Brightness.dark) ? Colors.grey[800] : null,
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)),
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),
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) {
selectionHaptic();
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(
surfaceTintColor: (Theme.of(context).brightness == Brightness.dark) ? Colors.grey[800] : null,
title: Text(AppLocalizations.of(
context)!
.settingsBrightnessRestartTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(
context)!
.settingsBrightnessRestartDescription),
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(AppLocalizations
.of(context)!
.settingsBrightnessRestartCancel)),
TextButton(
onPressed: () async {
selectionHaptic();
await prefs!.setString(
"brightness",
p0.elementAt(0));
if (desktopFeature()) {
exit(0);
} else {
Restart.restartApp();
}
},
child: Text(AppLocalizations
.of(context)!
.settingsBrightnessRestartRestart))
]));
});
});
}),
const SizedBox(height: 8),
!kIsWeb
? 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) {
selectionHaptic();
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setLocalState) {
return AlertDialog(
surfaceTintColor: (Theme.of(context).brightness == Brightness.dark) ? Colors.grey[800] : null,
title: Text(
AppLocalizations.of(context)!
.settingsThemeRestartTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(
context)!
.settingsThemeRestartDescription),
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(AppLocalizations
.of(context)!
.settingsThemeRestartCancel)),
TextButton(
onPressed: () async {
selectionHaptic();
await prefs!.setBool(
"useDeviceTheme",
p0.elementAt(0) ==
"device");
if (desktopFeature()) {
exit(0);
} else {
Restart.restartApp();
}
},
child: Text(AppLocalizations
.of(context)!
.settingsThemeRestartRestart))
]);
});
});
})
: 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,
decoration: (Theme.of(context).brightness ==
Brightness.dark)
? BoxDecoration(
border:
Border.all(color: Colors.white),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(26),
topRight: Radius.circular(26)))
: null,
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)
]),
)
])),
)),
); );
} }
} }

View File

@ -216,6 +216,21 @@ class _ScreenSettingsVoiceState extends State<ScreenSettingsVoice> {
}); });
}, },
child: Container( child: Container(
decoration: (Theme.of(context)
.brightness ==
Brightness.dark)
? BoxDecoration(
border: Border.all(
color: Colors.white),
borderRadius:
const BorderRadius.only(
topLeft:
Radius.circular(
26),
topRight:
Radius.circular(
26)))
: null,
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16, left: 16,

View File

@ -80,7 +80,6 @@ List getHistoryString([String? uuid]) {
} }
Future<String> getTitleAi(List history) async { Future<String> getTitleAi(List history) async {
print(history);
final generated = await (llama.OllamaClient( final generated = await (llama.OllamaClient(
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map) headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
.cast<String, String>(), .cast<String, String>(),
@ -92,7 +91,7 @@ Future<String> getTitleAi(List history) async {
const llama.Message( const llama.Message(
role: llama.MessageRole.system, role: llama.MessageRole.system,
content: content:
"Generate a three to six word title for the conversation provided by the user. If an object or person is very important in the conversation, put it in the title as well; keep the focus on the main subject. You must not put the assistant in the focus and you must not put the word 'assistant' in the title! Do preferably use title case. Use a formal tone, don't use dramatic words, like 'mystery' Use spaces between words, do not use camel case! You must not use markdown or any other formatting language! You must not use emojis or any other symbols! You must not use general clauses like 'assistance', 'help' or 'session' in your title! \n\n~~User Introduces Themselves~~ -> User Introduction\n~~User Asks for Help with a Problem~~ -> Problem Help\n~~User has a _**big**_ Problem~~ -> Big Problem\n~~Conversation~~ -> Conversation about Fireflies"), "Generate a three to six word title for the conversation provided by the user. If an object or person is very important in the conversation, put it in the title as well; keep the focus on the main subject. You must not put the assistant in the focus and you must not put the word 'assistant' in the title! Do preferably use title case. Use a formal tone, don't use dramatic words, like 'mystery' Use spaces between words, do not use camel case! You must not use markdown or any other formatting language! You must not use emojis or any other symbols! You must not use general clauses like 'assistance', 'help' or 'session' in your title! \n\n~~User Introduces Themselves~~ -> User Introduction\n~~User Asks for Help with a Problem~~ -> Problem Help\n~~User has a _**big**_ Problem~~ -> Big Problem"),
llama.Message( llama.Message(
role: llama.MessageRole.user, role: llama.MessageRole.user,
content: "```\n${jsonEncode(history)}\n```") content: "```\n${jsonEncode(history)}\n```")

View File

@ -278,6 +278,10 @@ void setModel(BuildContext context, Function setState) {
? const Offset(289, 0) ? const Offset(289, 0)
: const Offset(0, 0), : const Offset(0, 0),
child: Dialog( child: Dialog(
surfaceTintColor:
(Theme.of(context).brightness == Brightness.dark)
? Colors.grey[800]
: null,
alignment: desktopLayoutRequired(context) alignment: desktopLayoutRequired(context)
? Alignment.topLeft ? Alignment.topLeft
: Alignment.topCenter, : Alignment.topCenter,
@ -285,7 +289,17 @@ void setModel(BuildContext context, Function setState) {
); );
}); });
} else { } else {
showModalBottomSheet(context: context, builder: (context) => content); showModalBottomSheet(
context: context,
builder: (context) => Container(
decoration: (Theme.of(context).brightness == Brightness.dark)
? BoxDecoration(
border: Border.all(color: Colors.white),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(26),
topRight: Radius.circular(26)))
: null,
child: content));
} }
} }
@ -406,6 +420,79 @@ void loadChat(String uuid, Function setState) {
setState(() {}); setState(() {});
} }
Future<bool> deleteChatDialog(BuildContext context, Function setState,
{bool takeAction = true,
bool? additionalCondition,
String? uuid,
bool popSidebar = false}) async {
additionalCondition ??= true;
uuid ??= chatUuid;
bool returnValue = false;
void delete() {
returnValue = true;
if (takeAction) {
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
uuid) {
List<String> tmp = prefs!.getStringList("chats")!;
tmp.removeAt(i);
prefs!.setStringList("chats", tmp);
break;
}
}
if (chatUuid == uuid) {
messages = [];
chatUuid = null;
if (!desktopLayoutRequired(context) &&
Navigator.of(context).canPop() &&
popSidebar) {
Navigator.of(context).pop();
}
}
}
}
if ((prefs!.getBool("askBeforeDeletion") ?? false) && additionalCondition) {
await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(builder: (context, setLocalState) {
return AlertDialog(
surfaceTintColor:
(Theme.of(context).brightness == Brightness.dark)
? Colors.grey[800]
: null,
title: Text(AppLocalizations.of(context)!.deleteDialogTitle),
content: Column(mainAxisSize: MainAxisSize.min, children: [
Text(AppLocalizations.of(context)!.deleteDialogDescription),
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context)!.deleteDialogCancel)),
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
delete();
},
child: Text(
AppLocalizations.of(context)!.deleteDialogDelete))
]);
});
});
} else {
delete();
}
setState(() {});
return returnValue;
}
Future<String> prompt(BuildContext context, Future<String> prompt(BuildContext context,
{String description = "", {String description = "",
String value = "", String value = "",
@ -431,6 +518,13 @@ Future<String> prompt(BuildContext context,
return StatefulBuilder(builder: (context, setLocalState) { return StatefulBuilder(builder: (context, setLocalState) {
return PopScope( return PopScope(
child: Container( child: Container(
decoration: (Theme.of(context).brightness == Brightness.dark)
? BoxDecoration(
border: Border.all(color: Colors.white),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(26),
topRight: Radius.circular(26)))
: null,
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 16, left: 16,
right: 16, right: 16,

View File

@ -125,6 +125,7 @@ void updateDialog(BuildContext context, Function title) {
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
surfaceTintColor: (Theme.of(context).brightness == Brightness.dark) ? Colors.grey[800] : null,
title: title:
Text(AppLocalizations.of(context)!.settingsUpdateDialogTitle), Text(AppLocalizations.of(context)!.settingsUpdateDialogTitle),
content: Column(mainAxisSize: MainAxisSize.min, children: [ content: Column(mainAxisSize: MainAxisSize.min, children: [
@ -133,9 +134,12 @@ void updateDialog(BuildContext context, Function title) {
title(AppLocalizations.of(context)!.settingsUpdateChangeLog), title(AppLocalizations.of(context)!.settingsUpdateChangeLog),
Flexible( Flexible(
child: SingleChildScrollView( child: SingleChildScrollView(
child: MarkdownBody( child: Container(
data: updateChangeLog ?? "No changelog given.", constraints: const BoxConstraints(maxWidth: 1000),
shrinkWrap: true))) child: MarkdownBody(
data: updateChangeLog ?? "No changelog given.",
shrinkWrap: true),
)))
]), ]),
actions: [ actions: [
TextButton( TextButton(