Centralized theme worker, simplified readme, unification on some spots, catches in voice mode, added tests
252
README.md
|
@ -9,26 +9,11 @@ A modern and easy-to-use client for Ollama. Have the greatest experience while k
|
||||||
> This app does not host a Ollama server on device, but rather connects to one and uses its api endpoint.
|
> This app does not host a Ollama server on device, but rather connects to one and uses its api endpoint.
|
||||||
> You don't know what Ollama is? Learn more at [ollama.com](https://ollama.com/).
|
> You don't know what Ollama is? Learn more at [ollama.com](https://ollama.com/).
|
||||||
|
|
||||||
- [Ollama App](#ollama-app)
|
## Getting Started
|
||||||
- [Installation](#installation)
|
|
||||||
- [Initial Setup](#initial-setup)
|
|
||||||
- [Chat View](#chat-view)
|
|
||||||
- [Multimodal Input](#multimodal-input)
|
|
||||||
- [Model Selector](#model-selector)
|
|
||||||
- [Side Menu](#side-menu)
|
|
||||||
- [Settings](#settings)
|
|
||||||
- [Host](#host)
|
|
||||||
- [Custom headers](#custom-headers)
|
|
||||||
- [Behavior](#behavior)
|
|
||||||
- [Interface](#interface)
|
|
||||||
- [Voice](#voice)
|
|
||||||
- [Export](#export)
|
|
||||||
- [About (and Updates)](#about-and-updates)
|
|
||||||
- [Multilingual Interface](#multilingual-interface)
|
|
||||||
- [Custom Builds](#custom-builds)
|
|
||||||
- [Actually Building](#actually-building)
|
|
||||||
|
|
||||||
## Installation
|
Ollama App has a pretty simple and intuitive interface to be as open as possible. Everything just works out of the but, you just have to follow the two next steps.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
You'll find the latest recommended version of the Ollama App under [the releases tab](https://github.com/JHubi1/ollama-app/releases). Download the APK and install it on your Android device. That's it, now proceed to [Initial Setup](#initial-setup).
|
You'll find the latest recommended version of the Ollama App under [the releases tab](https://github.com/JHubi1/ollama-app/releases). Download the APK and install it on your Android device. That's it, now proceed to [Initial Setup](#initial-setup).
|
||||||
|
|
||||||
|
@ -36,232 +21,27 @@ Alternatively, you can also download the app from any of the following stores:
|
||||||
|
|
||||||
[<img src="assets/stores/IzzyOnDroid.png" width="215" style="padding: 5px" />](https://apt.izzysoft.de/fdroid/index/apk/com.freakurl.apps.ollama/)
|
[<img src="assets/stores/IzzyOnDroid.png" width="215" style="padding: 5px" />](https://apt.izzysoft.de/fdroid/index/apk/com.freakurl.apps.ollama/)
|
||||||
|
|
||||||
## Initial Setup
|
### Setup
|
||||||
|
|
||||||
After installing the app and opening it for the first time, you'll encounter this popup:
|
The most difficult part is setting up the host. To learn more visit the [wiki guide on how to do so](https://github.com/JHubi1/ollama-app/wiki/Getting-Started#setting-up-the-host). After setting up, you normally don't have to enter it again.
|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_01.png" alt="welcome instructions" height="720" />
|
And you're done! Just start chatting with your local AI and have fun!
|
||||||
|
|
||||||
Go through the welcome dialog one by one, you should read their content, but you don't have to.
|
|
||||||
|
|
||||||
|  |  |  |
|
|
||||||
|-|-|-|
|
|
||||||
|
|
||||||
After going through that, you'll get a small snack bar notifying you that you have to set the host. For that, open the sidebar (swipe from the left to right or click the icon in the top left corner) and click on settings. There you'll find all app-related settings, you should go through them, but for the initial setup, only the first one is important.
|
|
||||||
|
|
||||||
In the big host text field, you have to enter the base URL of your instance. The port is required unless the port number matches the protocol (443 for HTTPS or 80 for HTTP). After that, click the save icon right next to the text field.
|
|
||||||
|
|
||||||
To learn more about the host setting, visit [Settings](#host)
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> The Ollama server needs additional steps to be asccessible from without the local machine. To learn more, visit issue #5
|
|
||||||
|
|
||||||
That's it, you can now just chat. Enter a message into the box at the bottom and click the send icon.
|
|
||||||
|
|
||||||
## Chat View
|
|
||||||
|
|
||||||
This is the main view of the app, simple and, most importantly, working.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> To start chatting, you first have to select a model in the [Model Selector](#model-selector). Do that first, then come back here.
|
|
||||||
|
|
||||||
The chat mode is straightforward. Just write a message, wait a few moments, and the answer will get sent into the chat.
|
|
||||||
|
|
||||||
The chat view hides a few useful features already included by default. Messages can be deleted by double-tapping the message you desire to wipe from the earth, and all messages sent since then, including the message itself, will get deleted.
|
|
||||||
|
|
||||||
Editing a message is almost as simple. After enabling message editing in [Interface Settings](#interface), simply long press on a message and a popup will open and ask for the new content.
|
|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_19.png" alt="message editing dialog" height="720" />
|
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Messages (almost) fully support markdown syntax. That means the AI will be able to recieve and send back the message in markdown.
|
> The new Voice Mode is now avaliable as an experimental feature. Learn more about it in [the documentation](https://github.com/JHubi1/ollama-app/wiki/Components#voice).
|
||||||
|
|
||||||
### Multimodal Input
|
## Documentation
|
||||||
|
|
||||||
Ollama App supports multimodal models, models that support input via an image. Models supporting the technology are marked with an image icon next to their name in the [Model Selector](#model-selector).
|
The documentation for components, functions, etc. has moved to the [Wiki Page](https://github.com/JHubi1/ollama-app/wiki) of this repository. These steps there will be updated with future versions.
|
||||||
|
|
||||||
After selecting a multimodal model, a new icon appears at the bottom left of the message bar; a camera icon. Clicking on it reveals the following bottom sheet:
|
## Translations and Contribution
|
||||||
|
|
||||||
|  |  |  |
|
You want to help me make this project even better? Great, help is always appresheated.
|
||||||
|-|-|-|
|
|
||||||
|
|
||||||
Select one of them, take or select your photo and it'll get added to the chat. Adding multiple images is allowed, just repeat the steps.
|
Ollama App is created using [Flutter](https://flutter.dev), a modern and robust frontend framework designed to make a single codebase run on multiple target platforms. The framework itself is based on the [Dart](https://dart.dev) programming language.
|
||||||
|
|
||||||
Even though the images will appear in the chat already after sending, they won't be submitted to the AI until a new text message is sent. When you send a message the AI will answer the message in consideration of the image.
|
Read more in the [Contribution Guide](https://github.com/JHubi1/ollama-app/wiki/Contributing).
|
||||||
|
|
||||||
## Model Selector
|
## Star History
|
||||||
|
|
||||||
You can access the model selector by tapping on the `<selector>` text in the top middle or the name of the currently selected model in the same spot. Then you'll get the following popup dialog:
|

|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_15.png" alt="model selector" height="720" />
|
|
||||||
|
|
||||||
This will display all the models currently installed on your Ollama server instance.
|
|
||||||
|
|
||||||
The models with a star next to them are recommended models. They have been selected by me (hehe) to be listed as that. Read more under [Custom Builds](#custom-builds).
|
|
||||||
|
|
||||||
The `Add` button does nothing at the moment, it just opens a snack bar listing steps on how to add a model to an instance. For safety reasons, I didn't add the ability to add a model directly via name in the app.
|
|
||||||
|
|
||||||
Models supporting [Multimodal Input](#multimodal-input) are marked with an image icon next to their name, like `llava` in the image above.
|
|
||||||
|
|
||||||
## Side Menu
|
|
||||||
|
|
||||||
The button on the top left opens the menu. In it, you have two options: `New Chat` and `Settings`. The first option creates a new chat, and the second one opens the [Settings](#settings) where you can change how everything works.
|
|
||||||
|
|
||||||
Below that are all the chats. To delete one, swipe it from left to right. To rename the chat tab and hold it until a popup dialog appears. In it, you can change the title or tab the sparkle icon to let AI find one for you. This is not affected by the "generate title" setting.
|
|
||||||
|
|
||||||
| |  |  |
|
|
||||||
|-|-|-|
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The button on the top right corner deletes the chat. It has the same effect as swiping the chat in the sidebar.
|
|
||||||
|
|
||||||
## Settings
|
|
||||||
|
|
||||||
Ollama App offers a lot of configuration options. We'll go through every option one by one.
|
|
||||||
|
|
||||||
### Host
|
|
||||||
|
|
||||||
The host is the main address of your Ollama server. It may include a port, a protocol and a hostname. Paths are not recommended.
|
|
||||||
|
|
||||||
|  |  |  |
|
|
||||||
|-|-|-|
|
|
||||||
|
|
||||||
The port is required unless the port number matches the protocol (443 for HTTPS or 80 for HTTP). After that, click the save icon right next to the text field to set the host.
|
|
||||||
|
|
||||||
The host address will be checked, so no worry about entering the wrong one. If you set the host once, and your server is offline, the requests will fail, but the host will stay saved if you don't change it yourself. To do that, just go into the side menu and open settings to do so.
|
|
||||||
|
|
||||||
#### Custom headers
|
|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_27.png" alt="settings screen" height="720" />
|
|
||||||
|
|
||||||
Ollama App supports adding custom headers. This can be useful in case you want to secure your instance with authentification or something similar. Simply press the plus icon next to the host input and set one as a JSON object. This could be for example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Authorization": "Bearer <token>"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Behavior
|
|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_06.png" alt="settings screen" height="720" />
|
|
||||||
|
|
||||||
The behavior settings include settings connected to the system prompt. They won't be applied until you create a new chat.
|
|
||||||
|
|
||||||
The system prompt is sent to the assistant at the start of the conversation. It leads the assistant in a direction and it'll talk like you told him to in this message. To reset the system prompt to default, empty its value, click the save icon and close the screen.
|
|
||||||
|
|
||||||
The option to disable markdown is not safe and the assistant can still potentially add markdown to its response.
|
|
||||||
|
|
||||||
### Interface
|
|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_07.png" alt="settings screen" height="720" />
|
|
||||||
|
|
||||||
The interface settings are focused, as the name might imply, on the interface of the Ollama App. The following list will document all options
|
|
||||||
|
|
||||||
1. Targeted at the [Model Selector](#model-selector).
|
|
||||||
1. Show model tags in the model selector. This can be useful if you have multiple versions of the same model installed
|
|
||||||
2. Clear the chat if the model is changed. This is highly recommended, disabling this option could lead to unintended behavior
|
|
||||||
2. Used in the [Chat View](#chat-view)
|
|
||||||
1. Set the request mode. Streaming is recommended, but sometimes it's not available, then select "Request"
|
|
||||||
2. Whether to generate titles of chats with the Ollama AI or not. Could higher potential quota costs
|
|
||||||
3. Whether long-pressing messages opens the edit dialog or not
|
|
||||||
4. Whether to ask before deletion of chats. Useful if important data is potentially stored in chats or not
|
|
||||||
5. Whether to show tips in the main sidebar
|
|
||||||
3. Backend loading options
|
|
||||||
1. Keep model always loaded (`keep_alive` to `-1`)
|
|
||||||
2. Never keep model (`keep_alive` to `0`)
|
|
||||||
3. Time to keep models alive
|
|
||||||
4. Appearance settings
|
|
||||||
1. Whether to enable haptic feedback or not
|
|
||||||
2. Whether to start windows maximized (only desktop)
|
|
||||||
3. Theme of app
|
|
||||||
4. Follow the device theme/color
|
|
||||||
|
|
||||||
### Voice
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> This is still an experimental feature! Some functions may not work as intended!
|
|
||||||
|
|
||||||
|  |  |  |
|
|
||||||
|-|-|-|
|
|
||||||
|
|
||||||
Tap the "Permissions not granted" button to allow the needed permissions. They're needed to allow Speach To Text to function.
|
|
||||||
|
|
||||||
After that, enable Voice Mode by switching the toggle. To bring it to work, you now have to press "No language selected" and select a language in the language dialog. That's it.
|
|
||||||
|
|
||||||
Then, press the button on the spot where the attachment icon would be with a multimodal model or press the photo icon and the "Voice" button.
|
|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_29.png" alt="settings screen" height="720" />
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Documentation will be properly added once this feature leaves experimental phase
|
|
||||||
|
|
||||||
### Export
|
|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_13.png" alt="settings screen" height="720" />
|
|
||||||
|
|
||||||
The export function allows you to export and save all chats to a file. This can be very useful if you want to back up your data or want to sync it between devices.
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> The import functionallity deletes all currently saved chats from disk and replaces them with the ones from the file. This cannot be undone.
|
|
||||||
|
|
||||||
### About (and Updates)
|
|
||||||
|
|
||||||
<img src="assets/screenshots/flutter_14.png" alt="settings screen" height="720" />
|
|
||||||
|
|
||||||
The About screen holds a lot of useful information.
|
|
||||||
|
|
||||||
You can access the GitHub repository or the issue page of the app directly from this screen.
|
|
||||||
|
|
||||||
One more useful thing is the update checker. It looks for updates in the repo and will prompt you to download them directly from GitHub. Be careful though, the used GitHub API has a rate limit. You can only send a few requests before the rate limit kicks in.
|
|
||||||
|
|
||||||
## Multilingual Interface
|
|
||||||
|
|
||||||
Ollama App does support multiple languages. Currently available are:
|
|
||||||
|
|
||||||
- [x] English (fallback)
|
|
||||||
- [x] German
|
|
||||||
- [ ] Chinese (simplified)
|
|
||||||
- [ ] Italian
|
|
||||||
- [ ] Turkish
|
|
||||||
|
|
||||||
The tick is set if the language is translated with 100%. This might not be up to date, check the project page to get the latest progress.
|
|
||||||
|
|
||||||
In case the language you're looking for isn't listed, you can check if the development is in progress on the [Crowdin project page](https://crowdin.com/project/ollama-app). If not, you can always contribute.
|
|
||||||
|
|
||||||
## Custom Builds
|
|
||||||
|
|
||||||
Now it's going to get interesting. The app is built in a way so you can easily create custom builds. Currently, there are these values that can be customized:
|
|
||||||
|
|
||||||
```
|
|
||||||
// use host or not, if false dialog is shown
|
|
||||||
const useHost = false;
|
|
||||||
// host of ollama, must be accessible from the client, without trailing slash, will always be accepted as valid
|
|
||||||
const fixedHost = "http://example.com:11434";
|
|
||||||
// use model or not, if false selector is shown
|
|
||||||
const useModel = false;
|
|
||||||
// model name as string, must be valid ollama model!
|
|
||||||
const fixedModel = "gemma";
|
|
||||||
// recommended models, shown with as star in model selector
|
|
||||||
const recommendedModels = ["gemma", "llama3"];
|
|
||||||
// allow opening of settings
|
|
||||||
const allowSettings = true;
|
|
||||||
// allow multiple chats
|
|
||||||
const allowMultipleChats = true;
|
|
||||||
```
|
|
||||||
|
|
||||||
They can be found at the top of `lib/main.dart`. `useHost` and `useModel` decide whether you want `fixedHost` and `fixedModel` to control anything. `fixedHost` and `fixedModel` decide about the value that has to be used. That can be practical in case you try to create an app specific to your instance.
|
|
||||||
|
|
||||||
`recommendedModels` is a list of models that will be listed as recommended in the [Model Selector](#model-selector). They are more like personal preferences. If empty, no model will be preferred.
|
|
||||||
|
|
||||||
`allowSettings` will disable the settings screen. But it will also disable the welcome dialog at first startup and the ability to rename chats.
|
|
||||||
|
|
||||||
`allowMultipleChats` simply removes the `New Chat` option in the [Side Menu](#side-menu). And will load up the only available chat on app startup.
|
|
||||||
|
|
||||||
### Actually Building
|
|
||||||
|
|
||||||
But how do you create a custom build?
|
|
||||||
|
|
||||||
First, follow [the Flutter installation guide](https://docs.flutter.dev/get-started/install) by selecting Android as the first app type. Then follow [these steps](https://docs.flutter.dev/deployment/android#signing-the-app) till you have your custom `key.properties`. Place it into the `android` folder at the root of the project.
|
|
||||||
|
|
||||||
Make sure dart is available as a command or added as the default program for `.dart`. Then execute `scripts/build.dart` and wait for it to finish processing. Then go to `build/.output`. There you'll find everything you need, the normal Android app and the experimental Windows build.
|
|
||||||
|
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 166 KiB |
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 98 KiB |
|
@ -473,6 +473,26 @@
|
||||||
"description": "Text displayed for cancel button, should be capitalized",
|
"description": "Text displayed for cancel button, should be capitalized",
|
||||||
"context": "Visible in the settings view"
|
"context": "Visible in the settings view"
|
||||||
},
|
},
|
||||||
|
"settingsTemporaryFixes": "Temporary interface fixes",
|
||||||
|
"@settingsTemporaryFixes": {
|
||||||
|
"description": "Text displayed as description for temporary fixes section",
|
||||||
|
"context": "Visible in the settings view"
|
||||||
|
},
|
||||||
|
"settingsTemporaryFixesDescription": "Enable temporary fixes for interface issues.\nLong press on the individual options to learn more.",
|
||||||
|
"@settingsTemporaryFixesDescription": {
|
||||||
|
"description": "Description of the temporary fixes section",
|
||||||
|
"context": "Visible in the settings view"
|
||||||
|
},
|
||||||
|
"settingsTemporaryFixesInstructions": "Do not toggle any of these settings unless you know what you are doing! The given solutions might not work as expected.\nThey cannot be seen as final or should be judged as such. Issues might occur.",
|
||||||
|
"@settingsTemporaryFixesInstructions": {
|
||||||
|
"description": "Instructions and warnings for the temporary fixes",
|
||||||
|
"context": "Visible in the settings view"
|
||||||
|
},
|
||||||
|
"settingsTemporaryFixesNoFixes": "No fixes available",
|
||||||
|
"@settingsTemporaryFixesNoFixes": {
|
||||||
|
"description": "Text displayed when no fixes are available",
|
||||||
|
"context": "Visible in the settings view"
|
||||||
|
},
|
||||||
"settingsVoicePermissionLoading": "Loading voice permissions ...",
|
"settingsVoicePermissionLoading": "Loading voice permissions ...",
|
||||||
"@settingsVoicePermissionLoading": {
|
"@settingsVoicePermissionLoading": {
|
||||||
"description": "Text displayed while loading voice permissions",
|
"description": "Text displayed while loading voice permissions",
|
||||||
|
|
246
lib/main.dart
|
@ -4,7 +4,6 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
@ -15,6 +14,7 @@ import 'worker/setter.dart';
|
||||||
import 'worker/haptic.dart';
|
import 'worker/haptic.dart';
|
||||||
import 'worker/sender.dart';
|
import 'worker/sender.dart';
|
||||||
import 'worker/desktop.dart';
|
import 'worker/desktop.dart';
|
||||||
|
import 'worker/theme.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
|
@ -23,7 +23,6 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
// import 'package:http/http.dart' as http;
|
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
import 'package:markdown/markdown.dart' as md;
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
@ -38,26 +37,24 @@ import 'package:url_launcher/url_launcher.dart';
|
||||||
// client configuration
|
// client configuration
|
||||||
|
|
||||||
// use host or not, if false dialog is shown
|
// use host or not, if false dialog is shown
|
||||||
const useHost = false;
|
const bool useHost = false;
|
||||||
// host of ollama, must be accessible from the client, without trailing slash
|
// host of ollama, must be accessible from the client, without trailing slash
|
||||||
// ! will always be accepted as valid, even if [useHost] is false
|
// ! will always be accepted as valid, even if [useHost] is false
|
||||||
const fixedHost = "http://example.com:11434";
|
const String fixedHost = "http://example.com:11434";
|
||||||
// use model or not, if false selector is shown
|
// use model or not, if false selector is shown
|
||||||
const useModel = false;
|
const bool useModel = false;
|
||||||
// model name as string, must be valid ollama model!
|
// model name as string, must be valid ollama model!
|
||||||
const fixedModel = "gemma";
|
const String fixedModel = "gemma";
|
||||||
// recommended models, shown with as star in model selector
|
// recommended models, shown with a star in model selector
|
||||||
const recommendedModels = ["gemma", "llama3"];
|
const List<String> recommendedModels = ["gemma", "llama3"];
|
||||||
// allow opening of settings
|
// allow opening of settings
|
||||||
const allowSettings = true;
|
const bool allowSettings = true;
|
||||||
// allow multiple chats
|
// allow multiple chats
|
||||||
const allowMultipleChats = true;
|
const bool allowMultipleChats = true;
|
||||||
|
|
||||||
// client configuration end
|
// client configuration end
|
||||||
|
|
||||||
SharedPreferences? prefs;
|
SharedPreferences? prefs;
|
||||||
ThemeData? theme;
|
|
||||||
ThemeData? themeDark;
|
|
||||||
|
|
||||||
String? model;
|
String? model;
|
||||||
String? host;
|
String? host;
|
||||||
|
@ -77,6 +74,7 @@ bool desktopTitleVisible = true;
|
||||||
bool logoVisible = true;
|
bool logoVisible = true;
|
||||||
bool menuVisible = false;
|
bool menuVisible = false;
|
||||||
bool sendable = false;
|
bool sendable = false;
|
||||||
|
double sidebarIconSize = 1;
|
||||||
|
|
||||||
SpeechToText speech = SpeechToText();
|
SpeechToText speech = SpeechToText();
|
||||||
FlutterTts voice = FlutterTts();
|
FlutterTts voice = FlutterTts();
|
||||||
|
@ -146,79 +144,8 @@ class _AppState extends State<App> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||||
if (!(prefs?.getBool("useDeviceTheme") ?? false) ||
|
colorSchemeLight = lightDynamic;
|
||||||
lightDynamic == null ||
|
colorSchemeDark = darkDynamic;
|
||||||
darkDynamic == null) {
|
|
||||||
theme = ThemeData.from(
|
|
||||||
colorScheme: const ColorScheme(
|
|
||||||
brightness: Brightness.light,
|
|
||||||
primary: Colors.black,
|
|
||||||
onPrimary: Colors.white,
|
|
||||||
secondary: Colors.white,
|
|
||||||
onSecondary: Colors.black,
|
|
||||||
error: Colors.red,
|
|
||||||
onError: Colors.white,
|
|
||||||
surface: Colors.white,
|
|
||||||
onSurface: Colors.black));
|
|
||||||
themeDark = ThemeData.from(
|
|
||||||
colorScheme: const ColorScheme(
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
primary: Colors.white,
|
|
||||||
onPrimary: Colors.black,
|
|
||||||
secondary: Colors.black,
|
|
||||||
onSecondary: Colors.white,
|
|
||||||
error: Colors.red,
|
|
||||||
onError: Colors.black,
|
|
||||||
surface: Colors.black,
|
|
||||||
onSurface: Colors.white));
|
|
||||||
} else {
|
|
||||||
theme = ThemeData.from(colorScheme: lightDynamic);
|
|
||||||
themeDark = ThemeData.from(colorScheme: darkDynamic);
|
|
||||||
}
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
|
||||||
() {
|
|
||||||
// invert colors used, because brightness not updated yet
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
|
||||||
(prefs?.getString("brightness") ?? "system") == "system"
|
|
||||||
? ((MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? (themeDark ?? ThemeData.dark()).colorScheme.surface
|
|
||||||
: (theme ?? ThemeData()).colorScheme.surface)
|
|
||||||
: (prefs?.getString("brightness") == "dark"
|
|
||||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
|
||||||
: (theme ?? ThemeData.dark()).colorScheme.surface),
|
|
||||||
systemNavigationBarIconBrightness:
|
|
||||||
(((prefs?.getString("brightness") ?? "system") == "system" &&
|
|
||||||
MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.dark) ||
|
|
||||||
prefs?.getString("brightness") == "light")
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light));
|
|
||||||
};
|
|
||||||
|
|
||||||
// brightness changed function not run at first startup
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
|
||||||
(prefs?.getString("brightness") ?? "system") == "system"
|
|
||||||
? ((MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? (theme ?? ThemeData.dark()).colorScheme.surface
|
|
||||||
: (themeDark ?? ThemeData()).colorScheme.surface)
|
|
||||||
: (prefs?.getString("brightness") == "dark"
|
|
||||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
|
||||||
: (theme ?? ThemeData.dark()).colorScheme.surface),
|
|
||||||
systemNavigationBarIconBrightness:
|
|
||||||
(((prefs?.getString("brightness") ?? "system") == "system" &&
|
|
||||||
MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light) ||
|
|
||||||
prefs?.getString("brightness") == "light")
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light));
|
|
||||||
});
|
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
@ -234,13 +161,9 @@ class _AppState extends State<App> {
|
||||||
return const Locale("en");
|
return const Locale("en");
|
||||||
},
|
},
|
||||||
title: "Ollama",
|
title: "Ollama",
|
||||||
theme: theme,
|
theme: themeLight(),
|
||||||
darkTheme: themeDark,
|
darkTheme: themeDark(),
|
||||||
themeMode: ((prefs?.getString("brightness") ?? "system") == "system")
|
themeMode: themeMode(),
|
||||||
? ThemeMode.system
|
|
||||||
: ((prefs!.getString("brightness") == "dark")
|
|
||||||
? ThemeMode.dark
|
|
||||||
: ThemeMode.light),
|
|
||||||
home: const MainApp());
|
home: const MainApp());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -267,15 +190,36 @@ class _MainAppState extends State<MainApp> {
|
||||||
: (Padding(
|
: (Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
enableFeedback: false,
|
||||||
customBorder: const RoundedRectangleBorder(
|
customBorder: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(50))),
|
borderRadius: BorderRadius.all(Radius.circular(50))),
|
||||||
onTap: () {},
|
onTap: () async {
|
||||||
|
// ester egg? gimmick? not sure if it should be kept
|
||||||
|
return;
|
||||||
|
// ignore: dead_code
|
||||||
|
if (sidebarIconSize != 1) return;
|
||||||
|
setState(() {
|
||||||
|
sidebarIconSize = 0.8;
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
|
setState(() {
|
||||||
|
sidebarIconSize = 1.2;
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
|
setState(() {
|
||||||
|
sidebarIconSize = 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 16, right: 12),
|
padding: const EdgeInsets.only(left: 16, right: 12),
|
||||||
child: ImageIcon(AssetImage("assets/logo512.png"))),
|
child: AnimatedScale(
|
||||||
|
scale: sidebarIconSize,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
child: const ImageIcon(
|
||||||
|
AssetImage("assets/logo512.png")))),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(AppLocalizations.of(context)!.appTitle,
|
child: Text(AppLocalizations.of(context)!.appTitle,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
|
@ -296,6 +240,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
? (Padding(
|
? (Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
enableFeedback: false,
|
||||||
customBorder: const RoundedRectangleBorder(
|
customBorder: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(50))),
|
borderRadius: BorderRadius.all(Radius.circular(50))),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -329,6 +274,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
? (Padding(
|
? (Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
enableFeedback: false,
|
||||||
customBorder: const RoundedRectangleBorder(
|
customBorder: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(50))),
|
borderRadius: BorderRadius.all(Radius.circular(50))),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -373,6 +319,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
: (Padding(
|
: (Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
enableFeedback: false,
|
||||||
customBorder: const RoundedRectangleBorder(
|
customBorder: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(50))),
|
borderRadius: BorderRadius.all(Radius.circular(50))),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -451,6 +398,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
var child = Padding(
|
var child = Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
enableFeedback: false,
|
||||||
customBorder: const RoundedRectangleBorder(
|
customBorder: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(50))),
|
borderRadius: BorderRadius.all(Radius.circular(50))),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -681,28 +629,22 @@ class _MainAppState extends State<MainApp> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!chatAllowed) return;
|
if (!chatAllowed) return;
|
||||||
showDialog(
|
if (!desktopLayoutRequired(
|
||||||
|
context)) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Dialog(
|
return Padding(
|
||||||
alignment:
|
|
||||||
Alignment.bottomLeft,
|
|
||||||
insetPadding:
|
|
||||||
const EdgeInsets.only(
|
|
||||||
left: 12,
|
|
||||||
bottom: 12),
|
|
||||||
child: Container(
|
|
||||||
width: 100,
|
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets
|
const EdgeInsets.only(
|
||||||
.only(
|
|
||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
top: 16),
|
top: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize:
|
mainAxisSize:
|
||||||
MainAxisSize
|
MainAxisSize.min,
|
||||||
.min,
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double
|
width: double
|
||||||
|
@ -711,7 +653,8 @@ class _MainAppState extends State<MainApp> {
|
||||||
.icon(
|
.icon(
|
||||||
onPressed:
|
onPressed:
|
||||||
() {
|
() {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context)
|
||||||
|
.pop();
|
||||||
if (prefs!.getBool("askBeforeDeletion") ??
|
if (prefs!.getBool("askBeforeDeletion") ??
|
||||||
false) {
|
false) {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
@ -754,7 +697,9 @@ class _MainAppState extends State<MainApp> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
for (var i = 0;
|
||||||
|
i < (prefs!.getStringList("chats") ?? []).length;
|
||||||
|
i++) {
|
||||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == jsonDecode(item)["uuid"]) {
|
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == jsonDecode(item)["uuid"]) {
|
||||||
List<String> tmp = prefs!.getStringList("chats")!;
|
List<String> tmp = prefs!.getStringList("chats")!;
|
||||||
tmp.removeAt(i);
|
tmp.removeAt(i);
|
||||||
|
@ -762,7 +707,8 @@ class _MainAppState extends State<MainApp> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chatUuid == jsonDecode(item)["uuid"]) {
|
if (chatUuid ==
|
||||||
|
jsonDecode(item)["uuid"]) {
|
||||||
messages = [];
|
messages = [];
|
||||||
chatUuid = null;
|
chatUuid = null;
|
||||||
if (!desktopLayoutRequired(context)) {
|
if (!desktopLayoutRequired(context)) {
|
||||||
|
@ -774,8 +720,8 @@ class _MainAppState extends State<MainApp> {
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons
|
icon: const Icon(Icons
|
||||||
.delete_forever_rounded),
|
.delete_forever_rounded),
|
||||||
label:
|
label: Text(
|
||||||
Text(AppLocalizations.of(context)!.deleteChat))),
|
AppLocalizations.of(context)!.deleteChat))),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8),
|
height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
@ -785,11 +731,13 @@ class _MainAppState extends State<MainApp> {
|
||||||
.icon(
|
.icon(
|
||||||
onPressed:
|
onPressed:
|
||||||
() async {
|
() async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context)
|
||||||
|
.pop();
|
||||||
String
|
String
|
||||||
oldTitle =
|
oldTitle =
|
||||||
jsonDecode(item)["title"];
|
jsonDecode(item)["title"];
|
||||||
var newTitle = await prompt(context,
|
var newTitle = await prompt(
|
||||||
|
context,
|
||||||
title: AppLocalizations.of(context)!.dialogEnterNewTitle,
|
title: AppLocalizations.of(context)!.dialogEnterNewTitle,
|
||||||
value: oldTitle,
|
value: oldTitle,
|
||||||
uuid: jsonDecode(item)["uuid"]);
|
uuid: jsonDecode(item)["uuid"]);
|
||||||
|
@ -798,32 +746,36 @@ class _MainAppState extends State<MainApp> {
|
||||||
for (var i = 0;
|
for (var i = 0;
|
||||||
i < tmp.length;
|
i < tmp.length;
|
||||||
i++) {
|
i++) {
|
||||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == jsonDecode(item)["uuid"]) {
|
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
|
||||||
|
jsonDecode(item)["uuid"]) {
|
||||||
var tmp2 = jsonDecode(tmp[i]);
|
var tmp2 = jsonDecode(tmp[i]);
|
||||||
tmp2["title"] = newTitle;
|
tmp2["title"] = newTitle;
|
||||||
tmp[i] = jsonEncode(tmp2);
|
tmp[i] = jsonEncode(tmp2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefs!.setStringList("chats",
|
prefs!.setStringList(
|
||||||
|
"chats",
|
||||||
tmp);
|
tmp);
|
||||||
setState(() {});
|
setState(
|
||||||
|
() {});
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons
|
icon: const Icon(Icons
|
||||||
.edit_rounded),
|
.edit_rounded),
|
||||||
label:
|
label: Text(
|
||||||
Text(AppLocalizations.of(context)!.renameChat))),
|
AppLocalizations.of(context)!.renameChat))),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16)
|
height: 16)
|
||||||
])),
|
]));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
icon: Transform.translate(
|
icon: Transform.translate(
|
||||||
offset: const Offset(-8, -8),
|
offset: const Offset(-8, -8),
|
||||||
child: const Icon(allowMultipleChats
|
// ignore const suggestion, because values could be not const
|
||||||
|
// ignore: prefer_const_constructors
|
||||||
|
child: Icon(allowMultipleChats
|
||||||
? allowSettings
|
? allowSettings
|
||||||
? Icons.more_horiz_rounded
|
? Icons.more_horiz_rounded
|
||||||
: Icons.close_rounded
|
: Icons.close_rounded
|
||||||
|
@ -937,15 +889,19 @@ class _MainAppState extends State<MainApp> {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return const PopScope(
|
// ignore: prefer_const_constructors
|
||||||
|
return PopScope(
|
||||||
canPop: false,
|
canPop: false,
|
||||||
|
// ignore: prefer_const_constructors
|
||||||
child: Dialog.fullscreen(
|
child: Dialog.fullscreen(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
|
// ignore: prefer_const_constructors
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
// ignore: prefer_const_constructors
|
||||||
child: Text(
|
child: Text(
|
||||||
"*Build Error:*\n\nuseHost: $useHost\nallowSettings: $allowSettings\n\nYou created this build? One of them must be set to true or the app is not functional!\n\nYou received this build by someone else? Please contact them and report the issue.",
|
"*Build Error:*\n\nuseHost: $useHost\nallowSettings: $allowSettings\n\nYou created this build? One of them must be set to true or the app is not functional!\n\nYou received this build by someone else? Please contact them and report the issue.",
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontFamily: "monospace")))));
|
fontFamily: "monospace")))));
|
||||||
});
|
});
|
||||||
|
@ -978,6 +934,8 @@ class _MainAppState extends State<MainApp> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
resetSystemNavigation(context);
|
||||||
|
|
||||||
Widget selector = InkWell(
|
Widget selector = InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
|
@ -1020,7 +978,6 @@ class _MainAppState extends State<MainApp> {
|
||||||
children: desktopFeature()
|
children: desktopFeature()
|
||||||
? desktopLayoutRequired(context)
|
? desktopLayoutRequired(context)
|
||||||
? [
|
? [
|
||||||
// bottom left tile
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 304, height: 200, child: MoveWindow()),
|
width: 304, height: 200, child: MoveWindow()),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
@ -1251,7 +1208,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
String text = p0.text;
|
String text = p0.text;
|
||||||
if (text.trim() == "") {
|
if (text.trim() == "") {
|
||||||
text =
|
text =
|
||||||
"_Empty AI response, try restarting conversation_"; // Warning icon: U+26A0
|
"_Empty AI response, try restarting conversation_";
|
||||||
greyed = true;
|
greyed = true;
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
|
@ -1462,8 +1419,6 @@ class _MainAppState extends State<MainApp> {
|
||||||
data: ""));
|
data: ""));
|
||||||
},
|
},
|
||||||
disableImageGallery: true,
|
disableImageGallery: true,
|
||||||
// keyboardDismissBehavior:
|
|
||||||
// ScrollViewKeyboardDismissBehavior.onDrag,
|
|
||||||
emptyState: Center(
|
emptyState: Center(
|
||||||
child: VisibilityDetector(
|
child: VisibilityDetector(
|
||||||
key: const Key("logoVisible"),
|
key: const Key("logoVisible"),
|
||||||
|
@ -1797,8 +1752,7 @@ class _MainAppState extends State<MainApp> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
l10n: ChatL10nEn(
|
l10n: ChatL10nEn(
|
||||||
inputPlaceholder:
|
inputPlaceholder: AppLocalizations.of(context)!
|
||||||
AppLocalizations.of(context)!
|
|
||||||
.messageInputPlaceholder,
|
.messageInputPlaceholder,
|
||||||
attachmentButtonAccessibilityLabel:
|
attachmentButtonAccessibilityLabel:
|
||||||
AppLocalizations.of(context)!
|
AppLocalizations.of(context)!
|
||||||
|
@ -1823,10 +1777,10 @@ class _MainAppState extends State<MainApp> {
|
||||||
theme: (Theme.of(context).brightness ==
|
theme: (Theme.of(context).brightness ==
|
||||||
Brightness.light)
|
Brightness.light)
|
||||||
? DefaultChatTheme(
|
? DefaultChatTheme(
|
||||||
backgroundColor: (theme ?? ThemeData())
|
backgroundColor:
|
||||||
.colorScheme
|
themeLight().colorScheme.surface,
|
||||||
.surface,
|
primaryColor:
|
||||||
primaryColor: (theme ?? ThemeData()).colorScheme.primary,
|
themeLight().colorScheme.primary,
|
||||||
attachmentButtonIcon: !multimodal
|
attachmentButtonIcon: !multimodal
|
||||||
? (prefs?.getBool("voiceModeEnabled") ?? false)
|
? (prefs?.getBool("voiceModeEnabled") ?? false)
|
||||||
? Icon(Icons.headphones_rounded, color: Theme.of(context).iconTheme.color)
|
? Icon(Icons.headphones_rounded, color: Theme.of(context).iconTheme.color)
|
||||||
|
@ -1851,9 +1805,9 @@ class _MainAppState extends State<MainApp> {
|
||||||
),
|
),
|
||||||
sendButtonMargin: EdgeInsets.zero,
|
sendButtonMargin: EdgeInsets.zero,
|
||||||
attachmentButtonMargin: EdgeInsets.zero,
|
attachmentButtonMargin: EdgeInsets.zero,
|
||||||
inputBackgroundColor: (theme ?? ThemeData()).colorScheme.onSurface.withAlpha(10),
|
inputBackgroundColor: themeLight().colorScheme.onSurface.withAlpha(10),
|
||||||
inputTextColor: (theme ?? ThemeData()).colorScheme.onSurface,
|
inputTextColor: themeLight().colorScheme.onSurface,
|
||||||
inputBorderRadius: const BorderRadius.all(Radius.circular(64)),
|
inputBorderRadius: BorderRadius.circular(32),
|
||||||
inputPadding: const EdgeInsets.all(16),
|
inputPadding: const EdgeInsets.all(16),
|
||||||
inputMargin: EdgeInsets.only(left: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 8 : 6, right: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 8 : 6, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 0 : 8),
|
inputMargin: EdgeInsets.only(left: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 8 : 6, right: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 8 : 6, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 0 : 8),
|
||||||
messageMaxWidth: (MediaQuery.of(context).size.width >= 1000)
|
messageMaxWidth: (MediaQuery.of(context).size.width >= 1000)
|
||||||
|
@ -1864,9 +1818,9 @@ class _MainAppState extends State<MainApp> {
|
||||||
: 700
|
: 700
|
||||||
: 440)
|
: 440)
|
||||||
: DarkChatTheme(
|
: DarkChatTheme(
|
||||||
backgroundColor: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
backgroundColor: themeDark().colorScheme.surface,
|
||||||
primaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(40),
|
primaryColor: themeDark().colorScheme.primary.withAlpha(40),
|
||||||
secondaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(20),
|
secondaryColor: themeDark().colorScheme.primary.withAlpha(20),
|
||||||
attachmentButtonIcon: !multimodal
|
attachmentButtonIcon: !multimodal
|
||||||
? (prefs?.getBool("voiceModeEnabled") ?? false)
|
? (prefs?.getBool("voiceModeEnabled") ?? false)
|
||||||
? Icon(Icons.headphones_rounded, color: Theme.of(context).iconTheme.color)
|
? Icon(Icons.headphones_rounded, color: Theme.of(context).iconTheme.color)
|
||||||
|
@ -1891,8 +1845,8 @@ class _MainAppState extends State<MainApp> {
|
||||||
),
|
),
|
||||||
sendButtonMargin: EdgeInsets.zero,
|
sendButtonMargin: EdgeInsets.zero,
|
||||||
attachmentButtonMargin: EdgeInsets.zero,
|
attachmentButtonMargin: EdgeInsets.zero,
|
||||||
inputBackgroundColor: (themeDark ?? ThemeData()).colorScheme.onSurface.withAlpha(40),
|
inputBackgroundColor: themeDark().colorScheme.onSurface.withAlpha(40),
|
||||||
inputTextColor: (themeDark ?? ThemeData()).colorScheme.onSurface,
|
inputTextColor: themeDark().colorScheme.onSurface,
|
||||||
inputBorderRadius: const BorderRadius.all(Radius.circular(64)),
|
inputBorderRadius: const BorderRadius.all(Radius.circular(64)),
|
||||||
inputPadding: const EdgeInsets.all(16),
|
inputPadding: const EdgeInsets.all(16),
|
||||||
inputMargin: EdgeInsets.only(left: 8, right: 8, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 0 : 8),
|
inputMargin: EdgeInsets.only(left: 8, right: 8, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 0 : 8),
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:ollama_app/worker/theme.dart';
|
||||||
|
|
||||||
import 'main.dart';
|
import 'main.dart';
|
||||||
import 'worker/haptic.dart';
|
import 'worker/haptic.dart';
|
||||||
import 'worker/update.dart';
|
import 'worker/update.dart';
|
||||||
import 'worker/desktop.dart';
|
import 'worker/desktop.dart';
|
||||||
import 'package:ollama_app/worker/setter.dart';
|
import 'worker/setter.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'settings/behavior.dart';
|
import 'settings/behavior.dart';
|
||||||
|
@ -31,6 +32,7 @@ Widget toggle(BuildContext context, String text, bool value,
|
||||||
var space = ""; // Invisible character: U+2063
|
var space = ""; // Invisible character: U+2063
|
||||||
var spacePlus = " $space";
|
var spacePlus = " $space";
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
enableFeedback: false,
|
||||||
splashFactory: NoSplash.splashFactory,
|
splashFactory: NoSplash.splashFactory,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
|
@ -71,26 +73,24 @@ Widget toggle(BuildContext context, String text, bool value,
|
||||||
color: disabled ? Colors.grey : null,
|
color: disabled ? Colors.grey : null,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
(Theme.of(context).brightness == Brightness.light)
|
(Theme.of(context).brightness == Brightness.light)
|
||||||
? (theme ?? ThemeData()).colorScheme.surface
|
? themeLight().colorScheme.surface
|
||||||
: (themeDark ?? ThemeData.dark())
|
: themeDark().colorScheme.surface))),
|
||||||
.colorScheme
|
|
||||||
.surface))),
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
color: (Theme.of(context).brightness == Brightness.light)
|
color: (Theme.of(context).brightness == Brightness.light)
|
||||||
? (theme ?? ThemeData()).colorScheme.surface
|
? themeLight().colorScheme.surface
|
||||||
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
: themeDark().colorScheme.surface,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Switch(
|
child: Switch(
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: disabled
|
onChanged: disabled
|
||||||
|
? (onDisabledTap != null)
|
||||||
? (p0) {
|
? (p0) {
|
||||||
selectionHaptic();
|
selectionHaptic();
|
||||||
if (onDisabledTap != null) {
|
|
||||||
onDisabledTap();
|
onDisabledTap();
|
||||||
}
|
}
|
||||||
}
|
: null
|
||||||
: onChanged,
|
: onChanged,
|
||||||
activeTrackColor: disabled
|
activeTrackColor: disabled
|
||||||
? Theme.of(context).colorScheme.primary.withAlpha(50)
|
? Theme.of(context).colorScheme.primary.withAlpha(50)
|
||||||
|
@ -171,6 +171,7 @@ Widget button(String text, IconData? icon, void Function()? onPressed,
|
||||||
? const EdgeInsets.only(top: 8, bottom: 8)
|
? const EdgeInsets.only(top: 8, bottom: 8)
|
||||||
: EdgeInsets.zero,
|
: EdgeInsets.zero,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
enableFeedback: false,
|
||||||
onTap: disabled
|
onTap: disabled
|
||||||
? () {
|
? () {
|
||||||
selectionHaptic();
|
selectionHaptic();
|
||||||
|
@ -178,6 +179,10 @@ Widget button(String text, IconData? icon, void Function()? onPressed,
|
||||||
onDisabledTap();
|
onDisabledTap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
: (onPressed == null && (onLongTap != null || onDoubleTap != null))
|
||||||
|
? () {
|
||||||
|
selectionHaptic();
|
||||||
|
}
|
||||||
: onPressed,
|
: onPressed,
|
||||||
onLongPress: (description != null && context != null)
|
onLongPress: (description != null && context != null)
|
||||||
? desktopLayoutNotRequired(context)
|
? desktopLayoutNotRequired(context)
|
||||||
|
@ -305,7 +310,11 @@ class _ScreenSettingsState extends State<ScreenSettings> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
if ((Uri.parse(hostInputController.text.trim().removeSuffix("/").trim())
|
||||||
|
.toString() !=
|
||||||
|
fixedHost)) {
|
||||||
checkHost();
|
checkHost();
|
||||||
|
}
|
||||||
updatesSupported(setState, true);
|
updatesSupported(setState, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:ollama_app/worker/haptic.dart';
|
|
||||||
import 'package:ollama_app/worker/setter.dart';
|
|
||||||
|
|
||||||
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
||||||
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||||
import 'package:datetime_loop/datetime_loop.dart';
|
import 'package:datetime_loop/datetime_loop.dart';
|
||||||
// import 'package:volume_controller/volume_controller.dart';
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'main.dart';
|
import 'main.dart';
|
||||||
import 'worker/sender.dart';
|
import 'worker/sender.dart';
|
||||||
|
import 'worker/haptic.dart';
|
||||||
|
import 'worker/setter.dart';
|
||||||
|
import 'worker/theme.dart';
|
||||||
import 'settings/voice.dart';
|
import 'settings/voice.dart';
|
||||||
|
|
||||||
class ScreenVoice extends StatefulWidget {
|
class ScreenVoice extends StatefulWidget {
|
||||||
|
@ -34,47 +35,6 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
|
|
||||||
bool intendedStop = false;
|
bool intendedStop = false;
|
||||||
|
|
||||||
void setBrightness() {
|
|
||||||
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
|
||||||
() {
|
|
||||||
// invert colors used, because brightness not updated yet
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
|
||||||
(prefs!.getString("brightness") ?? "system") == "system"
|
|
||||||
? ((MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? (themeDark ?? ThemeData.dark()).colorScheme.surface
|
|
||||||
: (theme ?? ThemeData()).colorScheme.surface)
|
|
||||||
: (prefs!.getString("brightness") == "dark"
|
|
||||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
|
||||||
: (theme ?? ThemeData.dark()).colorScheme.surface),
|
|
||||||
systemNavigationBarIconBrightness:
|
|
||||||
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
|
||||||
MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.dark) ||
|
|
||||||
prefs!.getString("brightness") == "light")
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light));
|
|
||||||
};
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
|
||||||
(prefs!.getString("brightness") ?? "system") == "system"
|
|
||||||
? ((MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? (theme ?? ThemeData.dark()).colorScheme.surface
|
|
||||||
: (themeDark ?? ThemeData()).colorScheme.surface)
|
|
||||||
: (prefs!.getString("brightness") == "dark"
|
|
||||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
|
||||||
: (theme ?? ThemeData.dark()).colorScheme.surface),
|
|
||||||
systemNavigationBarIconBrightness:
|
|
||||||
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
|
||||||
MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light) ||
|
|
||||||
prefs!.getString("brightness") == "light")
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light));
|
|
||||||
}
|
|
||||||
|
|
||||||
void process() async {
|
void process() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
speaking = true;
|
speaking = true;
|
||||||
|
@ -171,75 +131,19 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
aiText = currentText;
|
aiText = currentText;
|
||||||
lightHaptic();
|
lightHaptic();
|
||||||
});
|
});
|
||||||
|
|
||||||
// var volume = await VolumeController().getVolume();
|
|
||||||
// var voicesTmp1 = await voice.getLanguages;
|
|
||||||
// var voices = jsonEncode(voicesTmp1);
|
|
||||||
// var isVoiceAvailable = (await voice.isLanguageAvailable(
|
|
||||||
// (prefs!.getString("voiceLanguage") ?? "en_US")
|
|
||||||
// .replaceAll("_", "-")))
|
|
||||||
// .toString();
|
|
||||||
// var voices2Tmp1 = await speech.locales();
|
|
||||||
// var voices2Tmp2 = [];
|
|
||||||
// for (var voice in voices2Tmp1) {
|
|
||||||
// voices2Tmp2.add(voice.localeId.replaceAll("_", "-"));
|
|
||||||
// }
|
|
||||||
// var voices2 = jsonEncode(voices2Tmp2);
|
|
||||||
// await showDialog(
|
|
||||||
// // ignore: use_build_context_synchronously
|
|
||||||
// context: context,
|
|
||||||
// builder: (context) {
|
|
||||||
// return Dialog.fullscreen(
|
|
||||||
// child: ListView(children: [
|
|
||||||
// const Row(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
// mainAxisSize: MainAxisSize.max,
|
|
||||||
// children: [
|
|
||||||
// Expanded(child: Divider(color: Colors.red)),
|
|
||||||
// SizedBox(width: 8),
|
|
||||||
// Text("START", style: TextStyle(color: Colors.red)),
|
|
||||||
// SizedBox(width: 8),
|
|
||||||
// Expanded(child: Divider(color: Colors.red))
|
|
||||||
// ]),
|
|
||||||
// Text((prefs!.getString("voiceLanguage") ?? "en_US")
|
|
||||||
// .replaceAll("_", "-")),
|
|
||||||
// const Divider(),
|
|
||||||
// Text(volume.toString()),
|
|
||||||
// const Divider(),
|
|
||||||
// Text(voices),
|
|
||||||
// const Divider(),
|
|
||||||
// Text(voicesTmp1
|
|
||||||
// .contains((prefs!.getString("voiceLanguage") ?? "en_US")
|
|
||||||
// .replaceAll("_", "-"))
|
|
||||||
// .toString()),
|
|
||||||
// const Divider(),
|
|
||||||
// Text(isVoiceAvailable),
|
|
||||||
// const Divider(),
|
|
||||||
// Text(voices2),
|
|
||||||
// const Divider(),
|
|
||||||
// Text(voices2Tmp2
|
|
||||||
// .contains((prefs!.getString("voiceLanguage") ?? "en_US")
|
|
||||||
// .replaceAll("_", "-"))
|
|
||||||
// .toString()),
|
|
||||||
// const Divider(),
|
|
||||||
// Text(speech.isAvailable.toString()),
|
|
||||||
// const Row(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
// mainAxisSize: MainAxisSize.max,
|
|
||||||
// children: [
|
|
||||||
// Expanded(child: Divider(color: Colors.red)),
|
|
||||||
// SizedBox(width: 8),
|
|
||||||
// Text("END", style: TextStyle(color: Colors.red)),
|
|
||||||
// SizedBox(width: 8),
|
|
||||||
// Expanded(child: Divider(color: Colors.red))
|
|
||||||
// ])
|
|
||||||
// ]));
|
|
||||||
// });
|
|
||||||
|
|
||||||
if (done) {
|
if (done) {
|
||||||
aiThinking = false;
|
aiThinking = false;
|
||||||
heavyHaptic();
|
heavyHaptic();
|
||||||
|
|
||||||
|
if (currentText.isEmpty) {
|
||||||
|
text = "";
|
||||||
|
speaking = false;
|
||||||
|
try {
|
||||||
|
setState(() {});
|
||||||
|
} catch (_) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((await voice.getLanguages as List).contains(
|
if ((await voice.getLanguages as List).contains(
|
||||||
(prefs!.getString("voiceLanguage") ?? "en_US")
|
(prefs!.getString("voiceLanguage") ?? "en_US")
|
||||||
.replaceAll("_", "-"))) {
|
.replaceAll("_", "-"))) {
|
||||||
|
@ -270,21 +174,10 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
resetSystemNavigation(context,
|
||||||
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
statusBarColor: themeDark().colorScheme.surface,
|
||||||
() {
|
systemNavigationBarColor: themeDark().colorScheme.surface,
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
delay: const Duration(milliseconds: 10));
|
||||||
systemNavigationBarColor:
|
|
||||||
(themeDark ?? ThemeData.dark()).colorScheme.surface,
|
|
||||||
systemNavigationBarIconBrightness: Brightness.dark));
|
|
||||||
};
|
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
|
||||||
(themeDark ?? ThemeData.dark()).colorScheme.surface,
|
|
||||||
systemNavigationBarIconBrightness: Brightness.dark));
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
|
|
||||||
void load() async {
|
void load() async {
|
||||||
var tmp = await speech.locales();
|
var tmp = await speech.locales();
|
||||||
|
@ -306,10 +199,11 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Theme(
|
return Theme(
|
||||||
data: themeDark!,
|
data: themeDark(),
|
||||||
child: PopScope(
|
child: PopScope(
|
||||||
canPop: !aiThinking,
|
canPop: !aiThinking,
|
||||||
onPopInvoked: (didPop) {
|
onPopInvoked: (didPop) {
|
||||||
|
if (!didPop) return;
|
||||||
speaking = false;
|
speaking = false;
|
||||||
voice.stop();
|
voice.stop();
|
||||||
if (chatUuid != null) {
|
if (chatUuid != null) {
|
||||||
|
@ -317,13 +211,12 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
}
|
}
|
||||||
settingsOpen = false;
|
settingsOpen = false;
|
||||||
logoVisible = true;
|
logoVisible = true;
|
||||||
setBrightness();
|
resetSystemNavigation(context);
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
speaking = false;
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close_rounded,
|
icon: const Icon(Icons.close_rounded,
|
||||||
|
@ -332,7 +225,11 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(model!.split(":")[0],
|
child: Text(
|
||||||
|
(model ??
|
||||||
|
AppLocalizations.of(context)!
|
||||||
|
.noSelectedModel)
|
||||||
|
.split(":")[0],
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
@ -345,11 +242,11 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
speaking = false;
|
speaking = false;
|
||||||
settingsOpen = false;
|
settingsOpen = false;
|
||||||
logoVisible = true;
|
logoVisible = true;
|
||||||
setBrightness();
|
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
const ScreenSettingsVoice()));
|
const ScreenSettingsVoice()));
|
||||||
|
resetSystemNavigation(context);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.settings_rounded,
|
Icons.settings_rounded,
|
||||||
|
@ -411,8 +308,9 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
process();
|
process();
|
||||||
},
|
},
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: themeDark!
|
backgroundColor: themeDark()
|
||||||
.colorScheme.primary
|
.colorScheme
|
||||||
|
.primary
|
||||||
.withAlpha(
|
.withAlpha(
|
||||||
!speaking ? 200 : 255),
|
!speaking ? 200 : 255),
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
|
@ -421,19 +319,23 @@ class _ScreenVoiceState extends State<ScreenVoice> {
|
||||||
child: speaking
|
child: speaking
|
||||||
? aiThinking
|
? aiThinking
|
||||||
? Icon(Icons.auto_awesome_rounded,
|
? Icon(Icons.auto_awesome_rounded,
|
||||||
color: themeDark!
|
color: themeDark()
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondary,
|
.secondary,
|
||||||
key: const ValueKey(
|
key: const ValueKey(
|
||||||
"aiThinking"))
|
"aiThinking"))
|
||||||
: sttDone
|
: sttDone
|
||||||
? Icon(Icons.volume_up_rounded,
|
? Icon(Icons.volume_up_rounded,
|
||||||
color: themeDark!
|
color: themeDark()
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondary,
|
.secondary,
|
||||||
key: const ValueKey(
|
key: const ValueKey(
|
||||||
"tts"))
|
"tts"))
|
||||||
: Icon(Icons.mic_rounded, color: themeDark!.colorScheme.secondary, key: const ValueKey("stt"))
|
: Icon(Icons.mic_rounded,
|
||||||
|
color: themeDark()
|
||||||
|
.colorScheme
|
||||||
|
.secondary,
|
||||||
|
key: const ValueKey("stt"))
|
||||||
: null)))),
|
: null)))),
|
||||||
);
|
);
|
||||||
}))),
|
}))),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'main.dart';
|
import 'main.dart';
|
||||||
|
import 'worker/theme.dart';
|
||||||
|
|
||||||
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||||
import 'package:transparent_image/transparent_image.dart';
|
import 'package:transparent_image/transparent_image.dart';
|
||||||
|
@ -18,35 +18,15 @@ class _ScreenWelcomeState extends State<ScreenWelcome> {
|
||||||
int page = 0;
|
int page = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
Widget build(BuildContext context) {
|
||||||
super.initState();
|
precacheImage(const AssetImage("assets/welcome/1.png"), context);
|
||||||
|
precacheImage(const AssetImage("assets/welcome/2.png"), context);
|
||||||
|
precacheImage(const AssetImage("assets/welcome/3.png"), context);
|
||||||
|
precacheImage(const AssetImage("assets/welcome/1dark.png"), context);
|
||||||
|
precacheImage(const AssetImage("assets/welcome/2dark.png"), context);
|
||||||
|
precacheImage(const AssetImage("assets/welcome/3dark.png"), context);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
resetSystemNavigation(context,
|
||||||
await Future.delayed(const Duration(milliseconds: 10));
|
|
||||||
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
|
||||||
() {
|
|
||||||
// invert colors used, because brightness not updated yet
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
|
||||||
(prefs!.getString("brightness") ?? "system") == "system"
|
|
||||||
? ((MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? Colors.grey[900]
|
|
||||||
: Colors.grey[100])
|
|
||||||
: (prefs!.getString("brightness") == "dark"
|
|
||||||
? Colors.grey[900]
|
|
||||||
: Colors.grey[100]),
|
|
||||||
systemNavigationBarIconBrightness:
|
|
||||||
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
|
||||||
MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.dark) ||
|
|
||||||
prefs!.getString("brightness") == "light")
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light));
|
|
||||||
};
|
|
||||||
|
|
||||||
// brightness changed function not run at first startup
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
systemNavigationBarColor:
|
||||||
(prefs!.getString("brightness") ?? "system") == "system"
|
(prefs!.getString("brightness") ?? "system") == "system"
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
|
@ -57,25 +37,8 @@ class _ScreenWelcomeState extends State<ScreenWelcome> {
|
||||||
: (prefs!.getString("brightness") == "dark"
|
: (prefs!.getString("brightness") == "dark"
|
||||||
? Colors.grey[900]
|
? Colors.grey[900]
|
||||||
: Colors.grey[100]),
|
: Colors.grey[100]),
|
||||||
systemNavigationBarIconBrightness:
|
delay: const Duration(milliseconds: 10));
|
||||||
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light) ||
|
|
||||||
prefs!.getString("brightness") == "light")
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
precacheImage(const AssetImage("assets/welcome/1.png"), context);
|
|
||||||
precacheImage(const AssetImage("assets/welcome/2.png"), context);
|
|
||||||
precacheImage(const AssetImage("assets/welcome/3.png"), context);
|
|
||||||
precacheImage(const AssetImage("assets/welcome/1dark.png"), context);
|
|
||||||
precacheImage(const AssetImage("assets/welcome/2dark.png"), context);
|
|
||||||
precacheImage(const AssetImage("assets/welcome/3dark.png"), context);
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
bottomNavigationBar: BottomSheet(
|
bottomNavigationBar: BottomSheet(
|
||||||
enableDrag: false,
|
enableDrag: false,
|
||||||
|
@ -94,10 +57,8 @@ class _ScreenWelcomeState extends State<ScreenWelcome> {
|
||||||
effect: ExpandingDotsEffect(
|
effect: ExpandingDotsEffect(
|
||||||
activeDotColor: (Theme.of(context).brightness ==
|
activeDotColor: (Theme.of(context).brightness ==
|
||||||
Brightness.light)
|
Brightness.light)
|
||||||
? (theme ?? ThemeData()).colorScheme.primary
|
? themeLight().colorScheme.primary
|
||||||
: (themeDark ?? ThemeData.dark())
|
: themeDark().colorScheme.primary)),
|
||||||
.colorScheme
|
|
||||||
.primary)),
|
|
||||||
]));
|
]));
|
||||||
}),
|
}),
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||||
|
@ -109,58 +70,13 @@ class _ScreenWelcomeState extends State<ScreenWelcome> {
|
||||||
curve: Curves.easeInOut);
|
curve: Curves.easeInOut);
|
||||||
} else {
|
} else {
|
||||||
prefs!.setBool("welcomeFinished", true);
|
prefs!.setBool("welcomeFinished", true);
|
||||||
|
|
||||||
WidgetsBinding.instance.platformDispatcher
|
|
||||||
.onPlatformBrightnessChanged = () {
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
|
||||||
(prefs!.getString("brightness") ?? "system") == "system"
|
|
||||||
? ((MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? (themeDark ?? ThemeData.dark())
|
|
||||||
.colorScheme
|
|
||||||
.surface
|
|
||||||
: (theme ?? ThemeData()).colorScheme.surface)
|
|
||||||
: (prefs!.getString("brightness") == "dark"
|
|
||||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
|
||||||
: (theme ?? ThemeData.dark())
|
|
||||||
.colorScheme
|
|
||||||
.surface),
|
|
||||||
systemNavigationBarIconBrightness:
|
|
||||||
(((prefs!.getString("brightness") ?? "system") ==
|
|
||||||
"system" &&
|
|
||||||
MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.dark) ||
|
|
||||||
prefs!.getString("brightness") == "light")
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light));
|
|
||||||
};
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
systemNavigationBarColor:
|
|
||||||
(prefs!.getString("brightness") ?? "system") == "system"
|
|
||||||
? ((MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? (theme ?? ThemeData.dark()).colorScheme.surface
|
|
||||||
: (themeDark ?? ThemeData()).colorScheme.surface)
|
|
||||||
: (prefs!.getString("brightness") == "dark"
|
|
||||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
|
||||||
: (theme ?? ThemeData.dark())
|
|
||||||
.colorScheme
|
|
||||||
.surface),
|
|
||||||
systemNavigationBarIconBrightness:
|
|
||||||
(((prefs!.getString("brightness") ?? "system") ==
|
|
||||||
"system" &&
|
|
||||||
MediaQuery.of(context).platformBrightness ==
|
|
||||||
Brightness.light) ||
|
|
||||||
prefs!.getString("brightness") == "light")
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light));
|
|
||||||
Navigator.pushReplacement(context,
|
Navigator.pushReplacement(context,
|
||||||
MaterialPageRoute(builder: (context) => const MainApp()));
|
MaterialPageRoute(builder: (context) => const MainApp()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Icon((page < 2) ? Icons.arrow_forward : Icons.check_rounded),
|
child: (page < 2)
|
||||||
),
|
? const Icon(Icons.arrow_forward)
|
||||||
|
: const Icon(Icons.check_rounded)),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(children: [
|
child: Column(children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
|
@ -12,6 +12,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:restart_app/restart_app.dart';
|
import 'package:restart_app/restart_app.dart';
|
||||||
import 'package:duration_picker/duration_picker.dart';
|
import 'package:duration_picker/duration_picker.dart';
|
||||||
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class ScreenSettingsInterface extends StatefulWidget {
|
class ScreenSettingsInterface extends StatefulWidget {
|
||||||
|
@ -132,6 +134,7 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
||||||
.settingsKeepModelLoadedAlways,
|
.settingsKeepModelLoadedAlways,
|
||||||
int.parse(prefs!.getString("keepAlive") ?? "300") == -1,
|
int.parse(prefs!.getString("keepAlive") ?? "300") == -1,
|
||||||
(value) {
|
(value) {
|
||||||
|
selectionHaptic();
|
||||||
setState(() {
|
setState(() {
|
||||||
if (value) {
|
if (value) {
|
||||||
prefs!.setString("keepAlive", "-1");
|
prefs!.setString("keepAlive", "-1");
|
||||||
|
@ -146,6 +149,7 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
||||||
.settingsKeepModelLoadedNever,
|
.settingsKeepModelLoadedNever,
|
||||||
int.parse(prefs!.getString("keepAlive") ?? "300") == 0,
|
int.parse(prefs!.getString("keepAlive") ?? "300") == 0,
|
||||||
(value) {
|
(value) {
|
||||||
|
selectionHaptic();
|
||||||
setState(() {
|
setState(() {
|
||||||
if (value) {
|
if (value) {
|
||||||
prefs!.setString("keepAlive", "0");
|
prefs!.setString("keepAlive", "0");
|
||||||
|
@ -165,6 +169,7 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
||||||
: AppLocalizations.of(context)!
|
: AppLocalizations.of(context)!
|
||||||
.settingsKeepModelLoadedFor,
|
.settingsKeepModelLoadedFor,
|
||||||
Icons.snooze_rounded, () async {
|
Icons.snooze_rounded, () async {
|
||||||
|
selectionHaptic();
|
||||||
bool loaded = false;
|
bool loaded = false;
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -412,22 +417,77 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
||||||
})
|
})
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
titleDivider(),
|
titleDivider(),
|
||||||
toggle(context, "Fix to code block not scrollable",
|
button(AppLocalizations.of(context)!.settingsTemporaryFixes,
|
||||||
(prefs!.getBool("fixCodeblockScroll") ?? false),
|
Icons.fast_forward_rounded, () {
|
||||||
(value) {
|
selectionHaptic();
|
||||||
prefs!.setBool("fixCodeblockScroll", value);
|
showModalBottomSheet(
|
||||||
if ((prefs!.getBool("fixCodeblockScroll") ?? false) ==
|
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) {
|
false) {
|
||||||
prefs!.remove("fixCodeblockScroll");
|
prefs!.remove("fixCodeblockScroll");
|
||||||
}
|
}
|
||||||
selectionHaptic();
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}, onLongTap: () {
|
}, onLongTap: () {
|
||||||
|
selectionHaptic();
|
||||||
launchUrl(Uri.parse(
|
launchUrl(Uri.parse(
|
||||||
"https://github.com/JHubi1/ollama-app/issues/26"));
|
"https://github.com/JHubi1/ollama-app/issues/26"));
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 16)
|
const SizedBox(height: 16)
|
||||||
]),
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 16)
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
]))),
|
]))),
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:ollama_app/worker/haptic.dart';
|
import 'package:ollama_app/worker/haptic.dart';
|
||||||
|
import 'package:ollama_app/worker/theme.dart';
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
// import '../worker/haptic.dart';
|
// import '../worker/haptic.dart';
|
||||||
|
@ -281,21 +282,21 @@ class _ScreenSettingsVoiceState extends State<ScreenSettingsVoice> {
|
||||||
!(prefs?.getBool("useDeviceTheme") ??
|
!(prefs?.getBool("useDeviceTheme") ??
|
||||||
false))
|
false))
|
||||||
? ((MediaQuery.of(context).platformBrightness == Brightness.light)
|
? ((MediaQuery.of(context).platformBrightness == Brightness.light)
|
||||||
? (theme ?? ThemeData()).colorScheme.secondary
|
? themeLight().colorScheme.secondary
|
||||||
: (themeDark ?? ThemeData.dark()).colorScheme.secondary)
|
: themeDark().colorScheme.secondary)
|
||||||
: null,
|
: null,
|
||||||
labelStyle: (usedIndex == index &&
|
labelStyle: (usedIndex == index &&
|
||||||
!(prefs?.getBool("useDeviceTheme") ??
|
!(prefs?.getBool("useDeviceTheme") ??
|
||||||
false))
|
false))
|
||||||
? TextStyle(
|
? TextStyle(
|
||||||
color: (MediaQuery.of(context).platformBrightness == Brightness.light) ? (theme ?? ThemeData()).colorScheme.secondary : (themeDark ?? ThemeData.dark()).colorScheme.secondary)
|
color: (MediaQuery.of(context).platformBrightness == Brightness.light) ? themeLight().colorScheme.secondary : themeDark().colorScheme.secondary)
|
||||||
: null,
|
: null,
|
||||||
selectedColor: (prefs?.getBool("useDeviceTheme") ??
|
selectedColor: (prefs?.getBool("useDeviceTheme") ??
|
||||||
false)
|
false)
|
||||||
? null
|
? null
|
||||||
: (MediaQuery.of(context).platformBrightness == Brightness.light)
|
: (MediaQuery.of(context).platformBrightness == Brightness.light)
|
||||||
? (theme ?? ThemeData()).colorScheme.primary
|
? themeLight().colorScheme.primary
|
||||||
: (themeDark ?? ThemeData.dark()).colorScheme.primary,
|
: themeDark().colorScheme.primary,
|
||||||
onSelected:
|
onSelected:
|
||||||
(bool
|
(bool
|
||||||
selected) {
|
selected) {
|
||||||
|
|
|
@ -111,6 +111,9 @@ Future<String> send(String value, BuildContext context, Function setState,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)!.noHostSelected),
|
content: Text(AppLocalizations.of(context)!.noHostSelected),
|
||||||
showCloseIcon: true));
|
showCloseIcon: true));
|
||||||
|
if (onStream != null) {
|
||||||
|
onStream("", true);
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +123,9 @@ Future<String> send(String value, BuildContext context, Function setState,
|
||||||
content: Text(AppLocalizations.of(context)!.noModelSelected),
|
content: Text(AppLocalizations.of(context)!.noModelSelected),
|
||||||
showCloseIcon: true));
|
showCloseIcon: true));
|
||||||
}
|
}
|
||||||
|
if (onStream != null) {
|
||||||
|
onStream("", true);
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'desktop.dart';
|
||||||
import 'haptic.dart';
|
import 'haptic.dart';
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import 'sender.dart';
|
import 'sender.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
import 'package:dartx/dartx.dart';
|
import 'package:dartx/dartx.dart';
|
||||||
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||||
|
@ -215,10 +216,10 @@ void setModel(BuildContext context, Function setState) {
|
||||||
? ((MediaQuery.of(context)
|
? ((MediaQuery.of(context)
|
||||||
.platformBrightness ==
|
.platformBrightness ==
|
||||||
Brightness.light)
|
Brightness.light)
|
||||||
? (theme ?? ThemeData())
|
? themeLight()
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondary
|
.secondary
|
||||||
: (themeDark ?? ThemeData.dark())
|
: themeDark()
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondary)
|
.secondary)
|
||||||
: null,
|
: null,
|
||||||
|
@ -230,11 +231,10 @@ void setModel(BuildContext context, Function setState) {
|
||||||
color: (MediaQuery.of(context)
|
color: (MediaQuery.of(context)
|
||||||
.platformBrightness ==
|
.platformBrightness ==
|
||||||
Brightness.light)
|
Brightness.light)
|
||||||
? (theme ?? ThemeData())
|
? themeLight()
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondary
|
.secondary
|
||||||
: (themeDark ??
|
: themeDark()
|
||||||
ThemeData.dark())
|
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondary)
|
.secondary)
|
||||||
: null,
|
: null,
|
||||||
|
@ -245,10 +245,10 @@ void setModel(BuildContext context, Function setState) {
|
||||||
: (MediaQuery.of(context)
|
: (MediaQuery.of(context)
|
||||||
.platformBrightness ==
|
.platformBrightness ==
|
||||||
Brightness.light)
|
Brightness.light)
|
||||||
? (theme ?? ThemeData())
|
?themeLight()
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.primary
|
.primary
|
||||||
: (themeDark ?? ThemeData.dark())
|
: themeDark()
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.primary,
|
.primary,
|
||||||
onSelected: (bool selected) {
|
onSelected: (bool selected) {
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../main.dart';
|
||||||
|
|
||||||
|
ColorScheme? colorSchemeLight;
|
||||||
|
ColorScheme? colorSchemeDark;
|
||||||
|
|
||||||
|
void resetSystemNavigation(BuildContext context,
|
||||||
|
{Color? color,
|
||||||
|
Color? statusBarColor,
|
||||||
|
Color? systemNavigationBarColor,
|
||||||
|
Duration? delay}) {
|
||||||
|
ColorScheme getColorScheme() {
|
||||||
|
final ColorScheme schemeLight = themeLight().colorScheme;
|
||||||
|
final ColorScheme schemeDark = themeDark().colorScheme;
|
||||||
|
if (themeMode() == ThemeMode.system) {
|
||||||
|
if (MediaQuery.of(context).platformBrightness == Brightness.light) {
|
||||||
|
return schemeLight;
|
||||||
|
} else {
|
||||||
|
return schemeDark;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (themeMode() == ThemeMode.light) {
|
||||||
|
return schemeLight;
|
||||||
|
} else {
|
||||||
|
return schemeDark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
if (delay != null) {
|
||||||
|
await Future.delayed(delay);
|
||||||
|
}
|
||||||
|
color ??= getColorScheme().surface;
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
statusBarIconBrightness:
|
||||||
|
(((statusBarColor != null) ? statusBarColor : color)!
|
||||||
|
.computeLuminance() >
|
||||||
|
0.179)
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light,
|
||||||
|
statusBarColor:
|
||||||
|
(((statusBarColor != null) ? statusBarColor : color)!.value !=
|
||||||
|
getColorScheme().surface.value)
|
||||||
|
? (statusBarColor != null)
|
||||||
|
? statusBarColor
|
||||||
|
: color
|
||||||
|
: Colors.transparent,
|
||||||
|
systemNavigationBarColor:
|
||||||
|
(systemNavigationBarColor != null) ? systemNavigationBarColor : color,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData themeLight() {
|
||||||
|
if (!(prefs?.getBool("useDeviceTheme") ?? false) ||
|
||||||
|
colorSchemeLight == null) {
|
||||||
|
return ThemeData.from(
|
||||||
|
colorScheme: const ColorScheme(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
primary: Colors.black,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
secondary: Colors.white,
|
||||||
|
onSecondary: Colors.black,
|
||||||
|
error: Colors.red,
|
||||||
|
onError: Colors.white,
|
||||||
|
surface: Colors.white,
|
||||||
|
onSurface: Colors.black));
|
||||||
|
} else {
|
||||||
|
return ThemeData.from(colorScheme: colorSchemeLight!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData themeDark() {
|
||||||
|
if (!(prefs?.getBool("useDeviceTheme") ?? false) || colorSchemeDark == null) {
|
||||||
|
return ThemeData.from(
|
||||||
|
colorScheme: const ColorScheme(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
primary: Colors.white,
|
||||||
|
onPrimary: Colors.black,
|
||||||
|
secondary: Colors.black,
|
||||||
|
onSecondary: Colors.white,
|
||||||
|
error: Colors.red,
|
||||||
|
onError: Colors.black,
|
||||||
|
surface: Colors.black,
|
||||||
|
onSurface: Colors.white));
|
||||||
|
} else {
|
||||||
|
return ThemeData.from(colorScheme: colorSchemeDark!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeMode themeMode() {
|
||||||
|
return ((prefs?.getString("brightness") ?? "system") == "system")
|
||||||
|
? ThemeMode.system
|
||||||
|
: ((prefs!.getString("brightness") == "dark")
|
||||||
|
? ThemeMode.dark
|
||||||
|
: ThemeMode.light);
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
void main(List<String> args) {
|
||||||
|
print(base64Encode(utf8.encode(args.join(" "))));
|
||||||
|
}
|
|
@ -3,10 +3,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
if (Directory.current.path.endsWith('scripts')) {
|
Directory.current = Directory(Platform.script.toFilePath()).parent.parent;
|
||||||
Directory.current = Directory.current.parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
String flutterExecutable = Platform.isWindows ? 'flutter.bat' : 'flutter';
|
String flutterExecutable = Platform.isWindows ? 'flutter.bat' : 'flutter';
|
||||||
|
|
||||||
print("Build script for Ollama App by JHubi1");
|
print("Build script for Ollama App by JHubi1");
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
String random(int length) {
|
||||||
|
var rand = Random();
|
||||||
|
var codeUnits = List.generate(length, (index) {
|
||||||
|
return rand.nextInt(33) + 89;
|
||||||
|
});
|
||||||
|
|
||||||
|
return String.fromCharCodes(codeUnits);
|
||||||
|
}
|
|
@ -1,30 +1,48 @@
|
||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:ollama_app/main.dart';
|
import 'functions.dart';
|
||||||
|
import 'package:ollama_app/screen_settings.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets("Widget: button", (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
String text = random(10);
|
||||||
await tester.pumpWidget(const MainApp());
|
bool clicked = false;
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: button(text, Icons.add_rounded, () {
|
||||||
|
clicked = true;
|
||||||
|
}))));
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
expect(find.text(text), findsOneWidget);
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
|
||||||
expect(find.text('1'), findsNothing);
|
expect(clicked, false);
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
await tester.tap(find.text(text));
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
expect(clicked, true);
|
||||||
|
});
|
||||||
|
testWidgets("Widget: toggle", (WidgetTester tester) async {
|
||||||
|
String text = random(10);
|
||||||
|
bool toggled = false;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(home: Scaffold(body: Builder(builder: (context) {
|
||||||
|
return toggle(context, text, toggled, (value) {
|
||||||
|
toggled = value;
|
||||||
|
});
|
||||||
|
}))));
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
expect(find.textContaining(text), findsOneWidget);
|
||||||
expect(find.text('0'), findsNothing);
|
expect(find.byType(Switch), findsOneWidget);
|
||||||
expect(find.text('1'), findsOneWidget);
|
expect(toggled, false);
|
||||||
|
|
||||||
|
await tester.tap(find.textContaining(text));
|
||||||
|
await tester.pump();
|
||||||
|
expect(toggled, true);
|
||||||
|
|
||||||
|
toggled = false;
|
||||||
|
await tester.tap(find.byType(Switch));
|
||||||
|
await tester.pump();
|
||||||
|
expect(toggled, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
"settingsUseSystem",
|
"settingsUseSystem",
|
||||||
"settingsUseSystemDescription",
|
"settingsUseSystemDescription",
|
||||||
"settingsPreloadModels",
|
"settingsPreloadModels",
|
||||||
|
"settingsTemporaryFixes",
|
||||||
|
"settingsTemporaryFixesDescription",
|
||||||
|
"settingsTemporaryFixesInstructions",
|
||||||
|
"settingsTemporaryFixesNoFixes",
|
||||||
"settingsVoiceTtsNotSupported",
|
"settingsVoiceTtsNotSupported",
|
||||||
"settingsVoiceTtsNotSupportedDescription",
|
"settingsVoiceTtsNotSupportedDescription",
|
||||||
"settingsVoiceNotEnabled"
|
"settingsVoiceNotEnabled"
|
||||||
|
@ -26,6 +30,10 @@
|
||||||
"settingsUseSystem",
|
"settingsUseSystem",
|
||||||
"settingsUseSystemDescription",
|
"settingsUseSystemDescription",
|
||||||
"settingsPreloadModels",
|
"settingsPreloadModels",
|
||||||
|
"settingsTemporaryFixes",
|
||||||
|
"settingsTemporaryFixesDescription",
|
||||||
|
"settingsTemporaryFixesInstructions",
|
||||||
|
"settingsTemporaryFixesNoFixes",
|
||||||
"settingsVoiceTtsNotSupported",
|
"settingsVoiceTtsNotSupported",
|
||||||
"settingsVoiceTtsNotSupportedDescription",
|
"settingsVoiceTtsNotSupportedDescription",
|
||||||
"settingsVoiceNotEnabled"
|
"settingsVoiceNotEnabled"
|
||||||
|
@ -42,6 +50,10 @@
|
||||||
"settingsUseSystem",
|
"settingsUseSystem",
|
||||||
"settingsUseSystemDescription",
|
"settingsUseSystemDescription",
|
||||||
"settingsPreloadModels",
|
"settingsPreloadModels",
|
||||||
|
"settingsTemporaryFixes",
|
||||||
|
"settingsTemporaryFixesDescription",
|
||||||
|
"settingsTemporaryFixesInstructions",
|
||||||
|
"settingsTemporaryFixesNoFixes",
|
||||||
"settingsVoiceTtsNotSupported",
|
"settingsVoiceTtsNotSupported",
|
||||||
"settingsVoiceTtsNotSupportedDescription",
|
"settingsVoiceTtsNotSupportedDescription",
|
||||||
"settingsVoiceNotEnabled"
|
"settingsVoiceNotEnabled"
|
||||||
|
@ -58,6 +70,10 @@
|
||||||
"settingsUseSystem",
|
"settingsUseSystem",
|
||||||
"settingsUseSystemDescription",
|
"settingsUseSystemDescription",
|
||||||
"settingsPreloadModels",
|
"settingsPreloadModels",
|
||||||
|
"settingsTemporaryFixes",
|
||||||
|
"settingsTemporaryFixesDescription",
|
||||||
|
"settingsTemporaryFixesInstructions",
|
||||||
|
"settingsTemporaryFixesNoFixes",
|
||||||
"settingsVoiceTtsNotSupported",
|
"settingsVoiceTtsNotSupported",
|
||||||
"settingsVoiceTtsNotSupportedDescription",
|
"settingsVoiceTtsNotSupportedDescription",
|
||||||
"settingsVoiceNotEnabled"
|
"settingsVoiceNotEnabled"
|
||||||
|
|