From 34044e74bbd85befdc2343e51cc8835e9c439f82 Mon Sep 17 00:00:00 2001 From: JHubi1 Date: Wed, 26 Jun 2024 16:07:02 +0200 Subject: [PATCH] Improved desktop mode --- lib/l10n/app_en.arb | 25 ++ lib/main.dart | 12 +- lib/screen_settings.dart | 634 ++++++++++++++++++++++-------------- lib/settings/about.dart | 14 +- lib/settings/behavior.dart | 2 +- lib/settings/interface.dart | 8 +- 6 files changed, 442 insertions(+), 253 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2112a67..7a2b837 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -165,26 +165,51 @@ "description": "Title of the behavior settings section", "context": "Visible in the settings view" }, + "settingsDescriptionBehavior": "Change the behavior of the AI to your liking.", + "@settingsDescriptionBehavior": { + "description": "Description of the behavior settings section", + "context": "Visible in the settings view" + }, "settingsTitleInterface": "Interface", "@settingsTitleInterface": { "description": "Title of the interface settings section", "context": "Visible in the settings view" }, + "settingsDescriptionInterface": "Edit how Ollama App looks and behaves.", + "@settingsDescriptionInterface": { + "description": "Description of the interface settings section", + "context": "Visible in the settings view" + }, "settingsTitleVoice": "Voice", "@settingsTitleVoice": { "description": "Title of the voice settings section. Do not translate if not required!", "context": "Visible in the settings view" }, + "settingsDescriptionVoice": "Enable voice mode and configure voice settings.", + "@settingsDescriptionVoice": { + "description": "Description of the voice settings section", + "context": "Visible in the settings view" + }, "settingsTitleExport": "Export", "@settingsTitleExport": { "description": "Title of the export settings section", "context": "Visible in the settings view" }, + "settingsDescriptionExport": "Export and import your chat history.", + "@settingsDescriptionExport": { + "description": "Description of the export settings section", + "context": "Visible in the settings view" + }, "settingsTitleAbout": "About", "@settingsTitleAbout": { "description": "Title of the about settings section", "context": "Visible in the settings view" }, + "settingsDescriptionAbout": "Check for updates and learn more about Ollama App.", + "@settingsDescriptionAbout": { + "description": "Description of the about settings section", + "context": "Visible in the settings view" + }, "settingsSavedAutomatically": "Settings are saved automatically", "@settingsSavedAutomatically": { "description": "Text displayed when settings are saved automatically", diff --git a/lib/main.dart b/lib/main.dart index 36c2929..57e3201 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -259,7 +259,8 @@ class _MainAppState extends State { left: desktopLayoutRequired(context) ? 17 : 12, right: desktopLayoutRequired(context) ? 17 : 12); return List.from([ - desktopLayout(context) + desktopFeature() ? const SizedBox(height: 8) : const SizedBox.shrink(), + desktopLayoutNotRequired(context) ? const SizedBox.shrink() : (Padding( padding: padding, @@ -282,11 +283,14 @@ class _MainAppState extends State { ), const SizedBox(width: 16), ]))))), - desktopLayout(context) + desktopLayoutNotRequired(context) ? const SizedBox.shrink() : (!allowMultipleChats && !allowSettings) ? const SizedBox.shrink() - : const Divider(), + : Divider( + color: desktopLayout(context) + ? Theme.of(context).colorScheme.onSurface.withAlpha(20) + : null), (allowMultipleChats) ? (Padding( padding: padding, @@ -1513,7 +1517,7 @@ class _MainAppState extends State { drawerEdgeDragWidth: desktopLayout(context) ? null : MediaQuery.of(context).size.width, drawer: Builder(builder: (context) { - if (desktopLayoutRequired(context)) { + if (desktopLayoutRequired(context) && !settingsOpen) { WidgetsBinding.instance.addPostFrameCallback((_) { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); diff --git a/lib/screen_settings.dart b/lib/screen_settings.dart index bc74584..6e82540 100644 --- a/lib/screen_settings.dart +++ b/lib/screen_settings.dart @@ -19,6 +19,7 @@ import 'package:dartx/dartx.dart'; import 'package:http/http.dart' as http; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:dynamic_color/dynamic_color.dart'; +import 'package:transparent_image/transparent_image.dart'; Widget toggle(BuildContext context, String text, bool value, Function(bool value) onChanged, @@ -120,49 +121,99 @@ Widget title(String text, {double top = 16, double bottom = 16}) { ])); } -Widget titleDivider({double top = 16, double bottom = 16}) { +Widget titleDivider({double? top, double? bottom, BuildContext? context}) { + top ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16; + bottom ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16; return Padding( padding: EdgeInsets.only(left: 8, right: 8, top: top, bottom: bottom), child: const Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded(child: Divider()), - ], - )); + mainAxisSize: MainAxisSize.max, + children: [Expanded(child: Divider())])); +} + +Widget verticalTitleDivider( + {double? left, double? right, BuildContext? context}) { + left ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16; + right ??= (context != null && desktopLayoutNotRequired(context)) ? 32 : 16; + return Padding( + padding: EdgeInsets.only(left: left, right: right, top: 8, bottom: 8), + child: const Row(mainAxisSize: MainAxisSize.max, children: [ + // Expanded(child: + VerticalDivider() + // ), + ])); } Widget button(String text, IconData? icon, void Function()? onPressed, - {Color? color, + {BuildContext? context, + Color? color, bool disabled = false, + bool replaceIconIfNull = false, + String? description, void Function()? onDisabledTap, void Function()? onLongTap, void Function()? onDoubleTap}) { - return InkWell( - onTap: disabled - ? () { - selectionHaptic(); - if (onDisabledTap != null) { - onDisabledTap(); + if (description != null && + (context != null && desktopLayoutNotRequired(context)) && + !description.startsWith("\n")) { + description = " • $description"; + } + return Padding( + padding: (context != null && desktopLayoutNotRequired(context)) + ? const EdgeInsets.only(top: 8, bottom: 8) + : EdgeInsets.zero, + child: InkWell( + onTap: disabled + ? () { + selectionHaptic(); + if (onDisabledTap != null) { + onDisabledTap(); + } } - } - : onPressed, - onLongPress: onLongTap, - onDoubleTap: onDoubleTap, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(8), - child: Row(children: [ - (icon != null) - ? Icon(icon, color: disabled ? Colors.grey : color) - : const SizedBox.shrink(), - (icon != null) - ? const SizedBox(width: 16, height: 42) - : const SizedBox.shrink(), - Expanded( - child: Text(text, - style: TextStyle(color: disabled ? Colors.grey : color))) - ]), - )); + : onPressed, + onLongPress: (description != null && context != null) + ? desktopLayoutNotRequired(context) + ? null + : () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(description!.trim()), + showCloseIcon: true)); + } + : onLongTap, + onDoubleTap: onDoubleTap, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row(children: [ + (icon != null || replaceIconIfNull) + ? replaceIconIfNull + ? ImageIcon(MemoryImage(kTransparentImage)) + : Icon(icon, color: disabled ? Colors.grey : color) + : const SizedBox.shrink(), + (icon != null || replaceIconIfNull) + ? const SizedBox(width: 16, height: 42) + : const SizedBox.shrink(), + Expanded( + child: (context != null) + ? RichText( + text: TextSpan( + text: text, + style: DefaultTextStyle.of(context).style.copyWith( + color: disabled ? Colors.grey : color), + children: [ + (description != null && + desktopLayoutNotRequired(context)) + ? TextSpan( + text: description, + style: const TextStyle(color: Colors.grey)) + : const TextSpan() + ])) + : Text(text, + style: + TextStyle(color: disabled ? Colors.grey : color))) + ]), + )), + ); } class ScreenSettings extends StatefulWidget { @@ -233,6 +284,10 @@ class _ScreenSettingsState extends State { selectionHaptic(); } + double iconSize = 1; + bool animatedInitialized = false; + bool animatedDesktop = false; + @override void initState() { super.initState(); @@ -249,6 +304,10 @@ class _ScreenSettingsState extends State { @override Widget build(BuildContext context) { + if (!animatedInitialized) { + animatedInitialized = true; + animatedDesktop = desktopLayoutNotRequired(context); + } return PopScope( canPop: !hostLoading, onPopInvoked: (didPop) { @@ -267,222 +326,323 @@ class _ScreenSettingsState extends State { actions: desktopControlsActions(context)), body: Padding( padding: const EdgeInsets.only(left: 16, right: 16), - child: Column(children: [ - Expanded( - child: ListView(children: [ - const SizedBox(height: 8), - TextField( - controller: hostInputController, - keyboardType: TextInputType.url, - readOnly: useHost, - onSubmitted: (value) { - selectionHaptic(); - checkHost(); - }, - decoration: InputDecoration( - labelText: AppLocalizations.of(context)! - .settingsHost, - hintText: "http://localhost:11434", - prefixIcon: IconButton( - tooltip: AppLocalizations.of(context)! - .tooltipAddHostHeaders, - onPressed: () async { - selectionHaptic(); - String tmp = await prompt(context, - placeholder: - "{\"Authorization\": \"Bearer ...\"}", - title: AppLocalizations.of(context)! - .settingsHostHeaderTitle, - value: (prefs! - .getString("hostHeaders") ?? - ""), - valueIfCanceled: "{}", - validator: (content) async { - try { - var tmp = jsonDecode(content); - tmp as Map; - return true; - } catch (_) { - return false; - } - }, - validatorError: - AppLocalizations.of(context)! - .settingsHostHeaderInvalid, - prefill: !((prefs!.getString( - "hostHeaders") ?? - {}) == - "{}")); - prefs!.setString("hostHeaders", tmp); + child: LayoutBuilder(builder: (context, constraints) { + var column1 = + Column(mainAxisSize: MainAxisSize.min, children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: animatedDesktop ? 8 : 0, + child: const SizedBox.shrink()), + const SizedBox(height: 8), + TextField( + controller: hostInputController, + keyboardType: TextInputType.url, + readOnly: useHost, + onSubmitted: (value) { + selectionHaptic(); + checkHost(); + }, + decoration: InputDecoration( + labelText: + AppLocalizations.of(context)!.settingsHost, + hintText: "http://localhost:11434", + prefixIcon: IconButton( + tooltip: AppLocalizations.of(context)! + .tooltipAddHostHeaders, + onPressed: () async { + selectionHaptic(); + String tmp = await prompt(context, + placeholder: + "{\"Authorization\": \"Bearer ...\"}", + title: AppLocalizations.of(context)! + .settingsHostHeaderTitle, + value: (prefs! + .getString("hostHeaders") ?? + ""), + valueIfCanceled: "{}", + validator: (content) async { + try { + var tmp = jsonDecode(content); + tmp as Map; + return true; + } catch (_) { + return false; + } }, - icon: const Icon(Icons.add_rounded)), - suffixIcon: useHost - ? const SizedBox.shrink() - : (hostLoading - ? Transform.scale( - scale: 0.5, - child: - const CircularProgressIndicator()) - : IconButton( - tooltip: - AppLocalizations.of(context)! - .tooltipSave, - onPressed: () { - selectionHaptic(); - checkHost(); - }, - icon: const Icon( - Icons.save_rounded), - )), - border: const OutlineInputBorder(), - error: (hostInvalidHost || hostInvalidUrl) - ? InkWell( - onTap: () { - selectionHaptic(); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: Text(AppLocalizations - .of(context)! - .settingsHostInvalidDetailed( - hostInvalidHost - ? "host" - : "url")), - showCloseIcon: true)); - }, - highlightColor: Colors.transparent, - splashFactory: NoSplash.splashFactory, - child: Row( + validatorError: + AppLocalizations.of(context)! + .settingsHostHeaderInvalid, + prefill: !((prefs!.getString( + "hostHeaders") ?? + {}) == + "{}")); + prefs!.setString("hostHeaders", tmp); + }, + icon: const Icon(Icons.add_rounded)), + suffixIcon: useHost + ? const SizedBox.shrink() + : (hostLoading + ? Transform.scale( + scale: 0.5, + child: + const CircularProgressIndicator()) + : IconButton( + tooltip: + AppLocalizations.of(context)! + .tooltipSave, + onPressed: () { + selectionHaptic(); + checkHost(); + }, + icon: + const Icon(Icons.save_rounded), + )), + border: const OutlineInputBorder(), + error: (hostInvalidHost || hostInvalidUrl) + ? InkWell( + onTap: () { + selectionHaptic(); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar( + content: Text(AppLocalizations + .of(context)! + .settingsHostInvalidDetailed( + hostInvalidHost + ? "host" + : "url")), + showCloseIcon: true)); + }, + splashFactory: NoSplash.splashFactory, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + child: Row( + children: [ + Icon(Icons.error_rounded, + color: Theme.of(context) + .colorScheme + .error), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)! + .settingsHostInvalid( + hostInvalidHost + ? "host" + : "url"), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .error)) + ], + )) + : null, + helper: InkWell( + onTap: () { + selectionHaptic(); + }, + splashFactory: NoSplash.splashFactory, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + child: hostLoading + ? Row( children: [ - Icon(Icons.error_rounded, - color: Theme.of(context) - .colorScheme - .error), + const Icon(Icons.search_rounded, + color: Colors.grey), const SizedBox(width: 8), Text( AppLocalizations.of(context)! - .settingsHostInvalid( - hostInvalidHost - ? "host" - : "url"), - style: TextStyle( - color: Theme.of(context) - .colorScheme - .error)) + .settingsHostChecking, + style: const TextStyle( + color: Colors.grey, + fontFamily: "monospace")) ], - )) - : null, - helper: InkWell( - onTap: () { - selectionHaptic(); - }, - highlightColor: Colors.transparent, - splashFactory: NoSplash.splashFactory, - child: hostLoading - ? Row( - children: [ - const Icon(Icons.search_rounded, - color: Colors.grey), - const SizedBox(width: 8), - Text( + ) + : Row( + children: [ + Icon(Icons.check_rounded, + color: Colors.green + .harmonizeWith( + Theme.of(context) + .colorScheme + .primary)), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)! + .settingsHostValid, + style: TextStyle( + color: Colors.green + .harmonizeWith( + Theme.of(context) + .colorScheme + .primary), + fontFamily: "monospace")) + ], + )))) + ]); + var column2 = + Column(mainAxisSize: MainAxisSize.min, children: [ + button( + AppLocalizations.of(context)!.settingsTitleBehavior, + Icons.psychology_rounded, () { + selectionHaptic(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const ScreenSettingsBehavior())); + }, + context: context, + description: + "\n${AppLocalizations.of(context)!.settingsDescriptionBehavior}"), + button( + AppLocalizations.of(context)! + .settingsTitleInterface, + Icons.web_asset_rounded, () { + selectionHaptic(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const ScreenSettingsInterface())); + }, + context: context, + description: + "\n${AppLocalizations.of(context)!.settingsDescriptionInterface}"), + (!desktopFeature()) + ? button( + AppLocalizations.of(context)! + .settingsTitleVoice, + Icons.headphones_rounded, () { + selectionHaptic(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const ScreenSettingsVoice())); + }, + context: context, + description: + "\n${AppLocalizations.of(context)!.settingsDescriptionVoice}") + : const SizedBox.shrink(), + button( + AppLocalizations.of(context)!.settingsTitleExport, + Icons.share_rounded, () { + selectionHaptic(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const ScreenSettingsExport())); + }, + context: context, + description: + "\n${AppLocalizations.of(context)!.settingsDescriptionExport}"), + button(AppLocalizations.of(context)!.settingsTitleAbout, + Icons.help_rounded, () { + selectionHaptic(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const ScreenSettingsAbout())); + }, + context: context, + description: + "\n${AppLocalizations.of(context)!.settingsDescriptionAbout}") + ]); + animatedDesktop = desktopLayoutNotRequired(context); + return Column(children: [ + Expanded( + child: desktopLayoutNotRequired(context) + ? Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + column1, + Expanded( + child: Center( + child: InkWell( + splashFactory: + NoSplash.splashFactory, + highlightColor: + Colors.transparent, + enableFeedback: false, + hoverColor: Colors.transparent, + onTap: () async { + if (iconSize != 1) return; + heavyHaptic(); + setState(() { + iconSize = 0.8; + }); + await Future.delayed( + const Duration( + milliseconds: 200)); + setState(() { + iconSize = 1.2; + }); + await Future.delayed( + const Duration( + milliseconds: 200)); + setState(() { + iconSize = 1; + }); + }, + child: AnimatedScale( + scale: iconSize, + duration: const Duration( + milliseconds: 400), + child: const ImageIcon( + AssetImage( + "assets/logo512.png"), + size: 44), + ), + ))), + Transform.translate( + offset: const Offset(0, 8), + child: button( AppLocalizations.of( context)! - .settingsHostChecking, - style: const TextStyle( - color: Colors.grey, - fontFamily: - "monospace")) - ], - ) - : Row( - children: [ - Icon(Icons.check_rounded, - color: Colors.green + .settingsSavedAutomatically, + Icons.info_rounded, + null, + color: Colors.grey .harmonizeWith( Theme.of(context) .colorScheme .primary)), - const SizedBox(width: 8), - Text( - AppLocalizations.of( - context)! - .settingsHostValid, - style: TextStyle( - color: Colors.green - .harmonizeWith( - Theme.of( - context) - .colorScheme - .primary), - fontFamily: - "monospace")) - ], - )))), - titleDivider(bottom: 4), - button( - AppLocalizations.of(context)! - .settingsTitleBehavior, - Icons.psychology_rounded, () { - selectionHaptic(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const ScreenSettingsBehavior())); - }), - button( - AppLocalizations.of(context)! - .settingsTitleInterface, - Icons.web_asset_rounded, () { - selectionHaptic(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const ScreenSettingsInterface())); - }), - (!desktopFeature()) - ? button( - AppLocalizations.of(context)! - .settingsTitleVoice, - Icons.headphones_rounded, () { - selectionHaptic(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const ScreenSettingsVoice())); - }) - : const SizedBox.shrink(), - button( - AppLocalizations.of(context)!.settingsTitleExport, - Icons.share_rounded, () { - selectionHaptic(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const ScreenSettingsExport())); - }), - button( - AppLocalizations.of(context)!.settingsTitleAbout, - Icons.help_rounded, () { - selectionHaptic(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const ScreenSettingsAbout())); - }) - ]), - ), - const SizedBox(height: 8), - button( - AppLocalizations.of(context)! - .settingsSavedAutomatically, - Icons.info_rounded, - null, - color: Colors.grey.harmonizeWith( - Theme.of(context).colorScheme.primary)) - ]))))); + ) + ])), + verticalTitleDivider(context: context), + Expanded(child: column2) + ]) + : ListView(children: [ + column1, + AnimatedOpacity( + opacity: animatedDesktop ? 0 : 1, + duration: + const Duration(milliseconds: 200), + child: titleDivider(bottom: 4)), + AnimatedOpacity( + opacity: animatedDesktop ? 0 : 1, + duration: + const Duration(milliseconds: 200), + child: column2) + ])), + const SizedBox(height: 8), + desktopLayoutNotRequired(context) + ? const SizedBox.shrink() + : button( + AppLocalizations.of(context)! + .settingsSavedAutomatically, + Icons.info_rounded, + null, + color: Colors.grey.harmonizeWith( + Theme.of(context).colorScheme.primary)) + ]); + }))))); } } diff --git a/lib/settings/about.dart b/lib/settings/about.dart index ad21ad0..b5b3a81 100644 --- a/lib/settings/about.dart +++ b/lib/settings/about.dart @@ -37,13 +37,11 @@ class _ScreenSettingsAboutState extends State { color: Theme.of(context).colorScheme.surface, child: Scaffold( appBar: AppBar( - title: Row(children: [ - Text(AppLocalizations.of(context)!.settingsTitleAbout), - Expanded(child: SizedBox(height: 200, child: MoveWindow())) - ]), - actions: - desktopControlsActions(context) - ), + title: Row(children: [ + Text(AppLocalizations.of(context)!.settingsTitleAbout), + Expanded(child: SizedBox(height: 200, child: MoveWindow())) + ]), + actions: desktopControlsActions(context)), body: Padding( padding: const EdgeInsets.only(left: 16, right: 16), child: Column(children: [ @@ -110,7 +108,7 @@ class _ScreenSettingsAboutState extends State { prefs!.setBool("checkUpdateOnSettingsOpen", value); setState(() {}); }), - titleDivider(), + titleDivider(context: context), button(AppLocalizations.of(context)!.settingsGithub, SimpleIcons.github, () { selectionHaptic(); diff --git a/lib/settings/behavior.dart b/lib/settings/behavior.dart index 774b578..de76eff 100644 --- a/lib/settings/behavior.dart +++ b/lib/settings/behavior.dart @@ -46,7 +46,7 @@ class _ScreenSettingsBehaviorState extends State { TextField( controller: systemInputController, keyboardType: TextInputType.multiline, - maxLines: 2, + maxLines: desktopLayoutNotRequired(context) ? 5 : 2, decoration: InputDecoration( labelText: AppLocalizations.of(context)! .settingsSystemMessage, diff --git a/lib/settings/interface.dart b/lib/settings/interface.dart index a5213e0..2883331 100644 --- a/lib/settings/interface.dart +++ b/lib/settings/interface.dart @@ -64,7 +64,9 @@ class _ScreenSettingsInterfaceState extends State { prefs!.setBool("resetOnModelSelect", value); setState(() {}); }), - titleDivider(bottom: 20), + titleDivider( + bottom: desktopLayoutNotRequired(context) ? 38 : 20, + context: context), SegmentedButton( segments: [ ButtonSegment( @@ -121,7 +123,7 @@ class _ScreenSettingsInterfaceState extends State { prefs!.setBool("tips", value); setState(() {}); }), - titleDivider(), + titleDivider(context: context), toggle( context, AppLocalizations.of(context)! @@ -240,7 +242,7 @@ class _ScreenSettingsInterfaceState extends State { })); }); }), - titleDivider(), + titleDivider(context: context), toggle( context, AppLocalizations.of(context)!