ChatDB, complete UI, formatting, everything else

This commit is contained in:
JHubi1 2026-08-07 23:26:12 +02:00
parent ba63e0fc50
commit 27e31e2ab1
No known key found for this signature in database
GPG Key ID: F538DC3FC5B07498
71 changed files with 24608 additions and 4036 deletions

22
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "ollama-app",
"request": "launch",
"type": "dart",
},
{
"name": "ollama-app (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "ollama-app (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View File

@ -16,19 +16,18 @@ linter:
- avoid_multiple_declarations_per_line
- eol_at_end_of_file
- leading_newlines_in_multiline_strings
- no_wildcard_variable_uses
- prefer_final_locals
- unnecessary_breaks
- unnecessary_final
- unnecessary_lambdas
- unnecessary_parenthesis
# Async, Errors, Null Safety
- avoid_catching_errors
- avoid_null_checks_in_equality_operators
- avoid_void_async
- cancel_subscriptions
- cast_nullable_to_non_nullable
- unnecessary_await_in_return
- use_if_null_to_convert_nulls_to_bools
- use_to_and_as_if_applicable
# Usage Patterns

View File

@ -3,7 +3,6 @@ import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
@ -17,15 +16,11 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.freakurl.apps.ollama"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973" // flutter.ndkVersion
ndkVersion = "28.2.13676358" // flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
@ -54,6 +49,13 @@ android {
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}

View File

@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip

View File

@ -18,8 +18,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

Binary file not shown.

View File

@ -1,4 +1,5 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- shared_preferences: true
- shared_preferences: true
- drift: true

View File

@ -1,10 +1,12 @@
{
"@@locale": "de",
"appTitle": "Ollama",
"appTitle": "{env, select, short{Ollama} integrated{{title} - Ollama App} other{Ollama App}}",
"optionNewChat": "Neuer Chat",
"optionSettings": "Einstellungen",
"optionInstallPwa": "Webapp installieren",
"optionNoChatFound": "Keine Chats gefunden",
"optionNoChatFound": "Keine Chats",
"optionNoChatFoundSearch": "Keinen Chat gefunden für Suche {query}",
"optionSearchChats": "Chats durchsuchen",
"tipPrefix": "Tipp: ",
"tip0": "Bearbeite Nachrichten durch langes Tippen",
"tip1": "Lösche Nachrichten durch Doppeltippen",
@ -15,37 +17,32 @@
"renameChat": "Umbenennen",
"takeImage": "Bild Aufnehmen",
"uploadImage": "Bild Hochladen",
"notAValidImage": "Kein gültiges Bild",
"imageOnlyConversation": "Nur Bild Unterhaltung",
"messageInputPlaceholder": "Nachricht",
"tooltipAttachment": "Anhang hinzufügen",
"messageInputPlaceholder": "{model} fragen",
"messageInputPlaceholderModelPlaceholder": "Modell",
"tooltipMessageOptions": "Nachrichtenoptionen",
"tooltipSend": "Senden",
"tooltipSave": "Speichern",
"tooltipLetAIThink": "Lass KI denken",
"tooltipLetAIThink": "Mit {model} generieren",
"tooltipAddHostHeaders": "Host-Header festlegen",
"tooltipReset": "Aktuellen Chat zurücksetzen",
"tooltipOptions": "Optionen anzeigen",
"noModelSelected": "Kein Modell ausgewählt",
"noHostSelected": "Kein Host ausgewählt, öffne zum Auswählen die Einstellungen",
"noSelectedModel": "<selektor>",
"noHostSelected": "Kein Host ausgewählt, öffne zum Auswählen die Einstellungen.",
"noSelectedModel": "<model>",
"newChatTitle": "Unbenannter Chat",
"modelDialogAddModel": "Hinzufügen",
"modelDialogAddPromptTitle": "Neues Modell hinzufügen",
"modelDialogAddPromptDescription": "Das kann entweder ein normaler Name (z.B. 'llama3') oder Name und Tag (z.B. 'llama3:70b') sein.",
"modelDialogAddPromptAlreadyExists": "Modell existiert bereits",
"modelDialogAddPromptInvalid": "Ungültiger Modellname",
"modelDialogAddAllowanceTitle": "Proxy erlauben",
"modelDialogAddAllowanceDescription": "Ollama App muss überprüfen, ob das eingegebene Modell gültig ist. Dafür senden wir normalerweise eine Webanfrage an die Ollama-Modellliste und überprüfen den Statuscode, aber da gerade der Webclient verwendet wird, können wir das nicht direkt tun. Stattdessen sendet die App die Anfrage an eine andere API, gehostet von JHubi1, um dies für uns zu überprüfen.\nDies ist eine einmalige Anfrage und wird nur gesendet, wenn du ein neues Modell hinzufügst.\nIhre IP-Adresse wird mit der Anfrage gesendet und kann bis zu zehn Minuten gespeichert werden, um Spamming mit potenziell schädlichen Absichten zu verhindern.\nWenn du zustimmst, wird deine Auswahl für die Zukunft gespeichert; wenn nicht, wird nichts gesendet und das Modell wird nicht hinzugefügt.",
"modelDialogAddAllowanceAllow": "Erlauben",
"modelDialogAddAllowanceDeny": "Ablehnen",
"modelDialogAddAssuranceTitle": "{model} hinzufügen?",
"modelDialogAddAssuranceDescription": "Durch Drücken von 'Hinzufügen' wird das Modell '{model}' direkt vom Ollama-Server auf deinen Host heruntergeladen.\nJe nach Internetverbindung kann dies eine Weile dauern. Der Vorgang kann nicht abgebrochen werden.\nWenn die App während des Downloads geschlossen wird, wird der Download fortgesetzt, wenn du den Namen erneut in den Modelldialog eingibst.",
"modelDialogAddAssuranceAdd": "Hinzufügen",
"modelDialogAddAssuranceCancel": "Abbrechen",
"modelDialogAddDownloadPercentLoading": "lade Fortschritt",
"modelDialogAddDownloadPercent": "Download bei {percent}%",
"learnMore": "Mehr erfahren",
"modelDialogAdd": "Hinzufügen",
"modelDialogAddPromptTitle": "Modell hinzufügen",
"modelDialogAddPromptDescription": "Das kann entweder ein normaler Name (z.B. „llama3“) oder ein Name zusammen mit einem Tag (z.B. „llama3:70b“) sein.", "modelDialogAddCurrently": "Wird gerade heruntergeladen:", "modelDialogAddPromptAlreadyExists": "Modell existiert bereits.",
"modelDialogAddPromptInvalid": "Ungültiger Modellname.",
"modelDialogAddPromptNotFound": "Das Modell konnte auf dem Server nicht gefunden werden. Bitte überprüfe die Schreibweise und versuche es erneut.",
"modelDialogAddPromptNetworkError": "Netzwerkfehler beim Überprüfen des Modells. Bitte überprüfe deine Verbindung und versuche es erneut.",
"modelDialogAddPromptCorruptionError": "Die API hat „EOF“ zurückgegeben; dein Server wurde wahrscheinlich neu gestartet, während das Modell zuvor heruntergeladen wurde.",
"modelDialogAddPromptCorruptionErrorSolution": "Alternativ kannst du den folgenden Befehl direkt auf deinem Linux-Host ausführen:\nWenn du eine neue Fehlermeldung siehst, versuche den Befehl mit „sudo“ davor auszuführen.",
"modelDialogAddCancelTitle": "Download abbrechen?",
"modelDialogAddCancelConfirm": "Abbrechen",
"modelDialogAddCancelHide": "Ausblenden",
"modelDialogAddDownloadFailed": "Getrennt, versuche es erneut",
"modelDialogAddDownloadSuccess": "Download erfolgreich",
"modelDialogAddDownloadSuccess": "Download erfolgreich. Als aktuelles Modell festlegen.",
"deleteDialogTitle": "Chat löschen",
"deleteDialogDescription": "Bist du sicher, dass du fortfahren möchtest? Dies wird alle Erinnerungen dieses Chats löschen und kann nicht rückgängig gemacht werden.\nUm diesen Dialog zu deaktivieren, besuche die Einstellungen.",
"deleteDialogDelete": "Löschen",
@ -73,12 +70,11 @@
"settingsExperimentalDeprecatedDescription": "Diese Funktion ist veraltet und wird in einer zukünftigen Version entfernt werden.\nEs funktioniert möglicherweise nicht wie beabsichtigt oder erwartet. Benutzung auf eigenes Gefahr.",
"settingsExperimentalDeprecatedFeature": "Veraltete Funktion, halte, um mehr zu erfahren",
"settingsHost": "Host",
"settingsHostMissing": "Kein Host festgelegt",
"settingsHostValid": "Gültiger Host",
"settingsHostChecking": "Host wird Überprüft",
"settingsHostInvalid": "Fehler: {type, select, url{Ungültige URL} host{Ungültiger Host} timeout{Request Fehlgeschlagen. Server Fehler} ratelimit{Zu viele Anfragen} other{Request Fehlgeschlagen}}",
"settingsHostHeaderTitle": "Host-Header festlegen",
"settingsHostHeaderInvalid": "Der eingegebene Text ist kein gültiges Header-JSON-Objekt",
"settingsHostInvalidDetailed": "{type, select, url{Die eingegebene URL ist ungültig. Es handelt sich nicht um ein standardisiertes URL-Format.} other{Der eingegebene Host ist ungültig. Er kann nicht erreicht werden. Bitte überprüfe den Host und versuche es erneut.}}",
"settingsHostInvalid": "{type, select, invalidUrl{Ungültige URL} unreachable{Host nicht erreichbar} undetectable{Host kein Ollama-Server} timeout{Verbindung abgelaufen} outdated{Veraltete Ollama-Version} other{Ungültiger Host}}",
"settingsHostInvalidDetailed": "{type, select, invalidUrl{Die eingegebene URL hat kein gültiges Format.} unreachable{Der Host ist ungültig oder nicht erreichbar, möglicherweise aufgrund einer falschen Adresse oder eines Netzwerkproblems.} undetectable{Der Host ist erreichbar, antwortet aber nicht wie von Ollama App erwartet.\nEr ist möglicherweise kein Ollama-Server. Bist du sicher, dass du ihn richtig eingegeben hast?} timeout{Der Host konnte nicht rechtzeitig erreicht werden.\nVersuche, den Timeout-Multiplikator in den Oberflächen-Einstellungen zu erhöhen.} outdated{Der Host verwendet eine Ollama-Version, die mit dieser App nicht kompatibel ist.\nBitte aktualisiere sowohl den Host als auch diese App, wenn möglich.} other{Der eingegebene Host ist ungültig.}}",
"settingsSystemMessage": "Systemnachricht",
"settingsUseSystem": "Systemnachricht verwenden",
"settingsUseSystemDescription": "Deaktiviere das Setzen der obigen Systemnachricht und benutze stattdessen die des Modells. Kann nützlich für Modelle mit Model-Files sein",

View File

@ -1,8 +1,24 @@
{
"@@locale": "en",
"appTitle": "Ollama",
"appTitle": "{env, select, short{Ollama} integrated{{title} - Ollama App} other{Ollama App}}",
"@appTitle": {
"description": "Title of the application",
"description": "Title of the application. Should stay untranslated in most cases",
"context": "app:sidebar",
"placeholders": {
"env": {
"type": "String",
"description": "The environment the app is running in. Can be 'short' on mobile platforms, 'integrated' on web if the app is currently displaying a page, or 'other' in every other case.",
"example": "integrated"
},
"title": {
"type": "String",
"description": "Title of the chat or settings page currently visible, if visible"
}
}
},
"learnMore": "Learn more",
"@learnMore": {
"description": "Text displayed for learn more button",
"context": "app:sidebar"
},
"optionNewChat": "New Chat",
@ -20,9 +36,35 @@
"description": "Text displayed for install PWA option",
"context": "app:sidebar"
},
"optionNoChatFound": "No chats found",
"optionNoChatFound": "No chats",
"@optionNoChatFound": {
"description": "Text displayed when no chats are found",
"description": "Text displayed when there are no chats",
"context": "app:sidebar"
},
"optionNoChatFoundSearch": "No chats found for {query}",
"@optionNoChatFoundSearch": {
"description": "Text displayed when no chats are found for the search query",
"context": "app:sidebar",
"placeholders": {
"query": {
"type": "String",
"description": "The search query that returned no results"
}
}
},
"optionSearchChats": "Search through chats",
"@optionSearchChats": {
"description": "Placeholder for search chats input",
"context": "app:sidebar"
},
"optionChatDetails": "Chat details",
"@optionChatDetails": {
"description": "Text displayed as title for chat details sidebar",
"context": "app:sidebar"
},
"optionChatDetailsNoChat": "No chat selected",
"@optionChatDetailsNoChat": {
"description": "Text displayed when no chat is selected in the chat details sidebar",
"context": "app:sidebar"
},
"tipPrefix": "Tip: ",
@ -65,34 +107,100 @@
"description": "Text displayed for rename chat option",
"context": "app:sidebar"
},
"takeImage": "Take Image",
"takeImage": "Camera",
"@takeImage": {
"description": "Text displayed for take image button",
"context": "app:chat:attachment"
},
"uploadImage": "Upload Image",
"uploadImage": "Gallery",
"@uploadImage": {
"description": "Text displayed for image upload button",
"context": "app:chat:attachment"
},
"notAValidImage": "Not a valid image",
"@notAValidImage": {
"description": "Text displayed when an image is not valid",
"context": "app:chat:attachment"
"newChatGreeting1": "Whats on your mind, {user}?",
"@newChatGreeting1": {
"description": "First greeting displayed in a new chat",
"context": "app:chat:greeting",
"placeholders": {
"user": {
"type": "String",
"description": "Name of the user. If unavailable, 'user' will be used instead.",
"example": "User"
}
}
},
"imageOnlyConversation": "Image Only Conversation",
"@imageOnlyConversation": {
"description": "Title, if 'Generate Title' is executed on a conversation with no text messages",
"context": "app:chat:title"
"newChatGreeting2": "Good to see you, {user}.",
"@newChatGreeting2": {
"description": "Second greeting displayed in a new chat",
"context": "app:chat:greeting",
"placeholders": {
"user": {
"type": "String",
"description": "Name of the user. If unavailable, 'user' will be used instead.",
"example": "User"
}
}
},
"messageInputPlaceholder": "Message",
"newChatGreeting3": "What can I do for you, {user}?",
"@newChatGreeting3": {
"description": "Third greeting displayed in a new chat",
"context": "app:chat:greeting",
"placeholders": {
"user": {
"type": "String",
"description": "Name of the user. If unavailable, 'user' will be used instead.",
"example": "User"
}
}
},
"newChatGreeting4": "How can I help you today, {user}?",
"@newChatGreeting4": {
"description": "Fourth greeting displayed in a new chat",
"context": "app:chat:greeting",
"placeholders": {
"user": {
"type": "String",
"description": "Name of the user. If unavailable, 'user' will be used instead.",
"example": "User"
}
}
},
"newChatGreeting5": "What would you like to talk about, {user}?",
"@newChatGreeting5": {
"description": "Fifth greeting displayed in a new chat",
"context": "app:chat:greeting",
"placeholders": {
"user": {
"type": "String",
"description": "Name of the user. If unavailable, 'user' will be used instead.",
"example": "User"
}
}
},
"newChatGreetingNameFallback": "user",
"@newChatGreetingNameFallback": {
"description": "Fallback name for the user if no name is set",
"context": "app:chat:greeting"
},
"messageInputPlaceholder": "Ask {model}",
"@messageInputPlaceholder": {
"description": "Placeholder text for message input",
"context": "app:chat:input",
"placeholders": {
"model": {
"type": "String",
"description": "Friendly name of the model without its tag, so 'gemma3' or 'gpt-oss' for example"
}
}
},
"messageInputPlaceholderModelPlaceholder": "model",
"@messageInputPlaceholderModelPlaceholder": {
"description": "Model name used in 'messageInputPlaceholder' if no model is available, should fit into capitalization and context",
"context": "app:chat:input"
},
"tooltipAttachment": "Add attachment",
"@tooltipAttachment": {
"description": "Tooltip for attachment button",
"tooltipMessageOptions": "Message options",
"@tooltipMessageOptions": {
"description": "Tooltip for message options button; options here being attachments, etc.",
"context": "app:chat:tooltips"
},
"tooltipSend": "Send",
@ -100,14 +208,20 @@
"description": "Tooltip for send button",
"context": "app:chat:tooltips"
},
"tooltipSave": "Save",
"@tooltipSave": {
"description": "Tooltip for save button",
"context": "app:chat:tooltips"
},
"tooltipLetAIThink": "Let AI think",
"tooltipLetAIThink": "Generate with {model}",
"@tooltipLetAIThink": {
"description": "Tooltip for let AI think button",
"description": "Tooltip for generate with model button",
"context": "app:chat:tooltips",
"placeholders": {
"model": {
"type": "String",
"description": "Friendly name of the model without its tag, so 'gemma3' or 'gpt-oss' for example"
}
}
},
"tooltipAddHostHeaders": "Add host headers",
"@tooltipAddHostHeaders": {
"description": "Tooltip for add host headers button",
"context": "app:chat:tooltips"
},
"tooltipReset": "Reset current chat",
@ -125,120 +239,97 @@
"description": "Text displayed when no model is selected",
"context": "app:chat"
},
"noHostSelected": "No host selected, open settings to set one",
"noHostSelected": "No host selected, open settings to set one.",
"@noHostSelected": {
"description": "Text displayed when no host is selected",
"context": "app:chat"
},
"noSelectedModel": "<selector>",
"noSelectedModel": "<model>",
"@noSelectedModel": {
"description": "Text displayed when no model is selected",
"description": "Text displayed when no model is selected. Should be lower case even if the language would capitalize it, to fit better into the app's design.",
"context": "app:chat"
},
"newChatTitle": "Unnamed Chat",
"newChatTitle": "Untitled Chat",
"@newChatTitle": {
"description": "Title of a new chat",
"context": "app:chat:title"
},
"modelDialogAddModel": "Add",
"@modelDialogAddModel": {
"modelDialogTitle": "Select a model",
"@modelDialogTitle": {
"description": "Title of the model selection dialog",
"context": "app:modelDialog"
},
"modelDialogAdd": "Add",
"@modelDialogAdd": {
"description": "Text displayed for add model button",
"context": "app:modelDialog"
},
"modelDialogAddPromptTitle": "Add new model",
"modelDialogRefresh": "Refresh",
"@modelDialogRefresh": {
"description": "Text displayed for refresh models button",
"context": "app:modelDialog"
},
"modelDialogAddPromptTitle": "Add model",
"@modelDialogAddPromptTitle": {
"description": "Title of the add model dialog",
"context": "app:modelDialog"
},
"modelDialogAddPromptDescription": "This can either be a normal name (e.g. 'llama3') or a name and tag (e.g. 'llama3:70b').",
"modelDialogAddPromptDescription": "This can either be a normal name (e.g. “llama3”) or a name together with a tag (e.g. “llama3:70b”).",
"@modelDialogAddPromptDescription": {
"description": "Description of the add model dialog",
"context": "app:modelDialog"
},
"modelDialogAddPromptAlreadyExists": "Model already exists",
"modelDialogAddCurrently": "Currently downloading:",
"@modelDialogAddCurrently": {
"description": "Text displayed when there are currently downloading models",
"context": "app:modelDialog"
},
"modelDialogAddPromptAlreadyExists": "Model already exists. Try refreshing if it was deleted.",
"@modelDialogAddPromptAlreadyExists": {
"description": "Text displayed when the model already exists",
"context": "app:modelDialog"
},
"modelDialogAddPromptInvalid": "Invalid model name",
"modelDialogAddPromptInvalid": "Couldnt read your model name correctly. Please check the spelling and try again.",
"@modelDialogAddPromptInvalid": {
"description": "Text displayed when the model name is invalid",
"context": "app:modelDialog"
},
"modelDialogAddAllowanceTitle": "Allow Proxy",
"@modelDialogAddAllowanceTitle": {
"description": "Title of the allow proxy dialog",
"modelDialogAddPromptNotFound": "Couldnt find your model on the server. Please check the spelling and try again.",
"@modelDialogAddPromptNotFound": {
"description": "Text displayed when the model could not be found",
"context": "app:modelDialog"
},
"modelDialogAddAllowanceDescription": "Ollama App must check if the entered model is valid. To do that, we normally send a web request to the Ollama model list and check the status code, but because you're using the web client, we can't do that directly. Instead, the app will send the request to a different API, hosted by JHubi1, to check for us.\nThis is a one-time request and will only be sent when you add a new model.\nYour IP address will be sent with the request and might be stored for up to ten minutes to prevent spamming with potentially harmful intentions.\nIf you accept, your selection will be remembered for the future; if not, nothing will be sent and the model won't be added.",
"@modelDialogAddAllowanceDescription": {
"description": "Description of the allow proxy dialog",
"modelDialogAddPromptNetworkError": "Network error while checking model. Please check your connection and try again.",
"@modelDialogAddPromptNetworkError": {
"description": "Text displayed when there was a network error while checking the model",
"context": "app:modelDialog"
},
"modelDialogAddAllowanceAllow": "Allow",
"@modelDialogAddAllowanceAllow": {
"description": "Text displayed for allow button, should be capitalized",
"modelDialogAddPromptCorruptionError": "The API returned “EOF”; your server was likely restarted while downloading the model previously.",
"@modelDialogAddPromptCorruptionError": {
"description": "Text displayed when the model is corrupted on the server",
"context": "app:modelDialog"
},
"modelDialogAddAllowanceDeny": "Deny",
"@modelDialogAddAllowanceDeny": {
"description": "Text displayed for deny button, should be capitalized",
"modelDialogAddPromptCorruptionErrorSolution": "Alternatively, you can run the following command on your Linux host directly:\nIf you see a new error, try running the command with “sudo” placed in front of it.",
"@modelDialogAddPromptCorruptionErrorSolution": {
"description": "Text displayed as solution for the corrupted model error",
"context": "app:modelDialog"
},
"modelDialogAddAssuranceTitle": "Add {model}?",
"@modelDialogAddAssuranceTitle": {
"description": "Title of the add model assurance dialog",
"context": "app:modelDialog",
"placeholders": {
"model": {
"type": "String",
"description": "Name of the model to be added"
}
}
},
"modelDialogAddAssuranceDescription": "Pressing 'Add' will download the model '{model}' directly from the Ollama server to your host.\nThis can take a while depending on your internet connection. The action cannot be canceled.\nIf the app is closed during the download, it will resume if you enter the name into the model dialog again.",
"@modelDialogAddAssuranceDescription": {
"description": "Description of the add model assurance dialog",
"context": "app:modelDialog",
"placeholders": {
"model": {
"type": "String",
"description": "Name of the model to be added"
}
}
},
"modelDialogAddAssuranceAdd": "Add",
"@modelDialogAddAssuranceAdd": {
"description": "Text displayed for add button, should be capitalized",
"modelDialogAddCancelTitle": "Cancel download?",
"@modelDialogAddCancelTitle": {
"description": "Title of the cancel download dialog",
"context": "app:modelDialog"
},
"modelDialogAddAssuranceCancel": "Cancel",
"@modelDialogAddAssuranceCancel": {
"description": "Text displayed for cancel button, should be capitalized",
"modelDialogAddCancelConfirm": "Cancel",
"@modelDialogAddCancelConfirm": {
"description": "Text displayed for cancel download button",
"context": "app:modelDialog"
},
"modelDialogAddDownloadPercentLoading": "loading progress",
"@modelDialogAddDownloadPercentLoading": {
"description": "Text displayed while loading the download progress",
"context": "app:modelDialog:download"
"modelDialogAddCancelHide": "Hide",
"@modelDialogAddCancelHide": {
"description": "Text displayed for hide button in cancel download dialog. Hide in the sense of 'hide this dialog'",
"context": "app:modelDialog"
},
"modelDialogAddDownloadPercent": "download at {percent}%",
"@modelDialogAddDownloadPercent": {
"description": "Text displayed while downloading a model",
"context": "app:modelDialog:download",
"placeholders": {
"percent": {
"type": "String",
"description": "Percentage of the download"
}
}
},
"modelDialogAddDownloadFailed": "Disconnected, try again",
"@modelDialogAddDownloadFailed": {
"description": "Text displayed when the download of a model fails",
"context": "app:modelDialog:download"
},
"modelDialogAddDownloadSuccess": "Download successful",
"modelDialogAddDownloadSuccess": "Download successful. Set as current model.",
"@modelDialogAddDownloadSuccess": {
"description": "Text displayed when the download of a model is successful",
"context": "app:modelDialog:download"
@ -298,6 +389,16 @@
"description": "Title of the edit message dialog",
"context": "app:editMessageDialog"
},
"settingsTitleOverview": "Overview",
"@settingsTitleOverview": {
"description": "Title of the overview settings section",
"context": "app:settings"
},
"settingsDescriptionOverview": "General settings regarding your host.",
"@settingsDescriptionOverview": {
"description": "Description of the overview settings section",
"context": "app:settings"
},
"settingsTitleBehavior": "Behavior",
"@settingsTitleBehavior": {
"description": "Title of the behavior settings section",
@ -348,17 +449,12 @@
"description": "Description of the about settings section",
"context": "app:settings"
},
"settingsSavedAutomatically": "Settings are saved automatically",
"@settingsSavedAutomatically": {
"description": "Text displayed when settings are saved automatically",
"context": "app:settings"
},
"settingsExperimentalAlpha": "alpha",
"@settingsExperimentalAlpha": {
"description": "Text displayed when a feature is in alpha phase",
"context": "app:settings"
},
"settingsExperimentalAlphaDescription": "This feature is in alpha and may not work as intended or expected.\nCritical issues and/or permanent critical damage to device and/or used services cannot be ruled out.\nUse at your own risk. No liability on the part of the app author.",
"settingsExperimentalAlphaDescription": "This feature is in alpha and may not work as intended or expected.\nCritical issues and/or permanent critical damage to device and/or connected services cannot be ruled out.\nUse at your own risk. No liability on the part of the app author.",
"@settingsExperimentalAlphaDescription": {
"description": "Description of the alpha feature",
"context": "app:settings"
@ -373,7 +469,7 @@
"description": "Text displayed when a feature is in beta",
"context": "app:settings"
},
"settingsExperimentalBetaDescription": "This feature is in beta and may not work as intended or expected.\nLess severe issues may or may not occur. Damage shouldn't be critical.\nUse at your own risk.",
"settingsExperimentalBetaDescription": "This feature is in beta and may not work as intended or expected.\nLess severe issues may or may not occur. Damage shouldnt be critical.\nUse at your own risk.",
"@settingsExperimentalBetaDescription": {
"description": "Description of the beta feature",
"context": "app:settings"
@ -403,6 +499,11 @@
"description": "Text displayed as description for host input",
"context": "app:settings:host"
},
"settingsHostMissing": "No host set",
"@settingsHostMissing": {
"description": "Text displayed when no host is set",
"context": "app:settings:host"
},
"settingsHostValid": "Valid Host",
"@settingsHostValid": {
"description": "Text displayed when the host is valid",
@ -413,40 +514,57 @@
"description": "Text displayed when the host is being checked",
"context": "app:settings:host"
},
"settingsHostInvalid": "Issue: {type, select, url{Invalid URL} host{Invalid Host} timeout{Request failed. Server issues} ratelimit{Too many requests} other{Request Failed}}",
"settingsHostInvalid": "{type, select, invalidUrl{Invalid URL} unreachable{Host unreachable} undetectable{Host not an Ollama server} timeout{Connection timed out} outdated{Outdated Ollama version} other{Invalid host}}",
"@settingsHostInvalid": {
"description": "Text displayed when the host is invalid",
"description": "Short title displayed when the host is invalid",
"context": "app:settings:host",
"placeholders": {
"type": {
"type": "String",
"description": "Type of the issue, either 'url', 'host', 'timeout', 'ratelimit' or 'other' (preferably 'host')"
"description": "Type of the issue, either 'invalidUrl', 'unreachable', 'undetectable', 'timeout' or 'outdated'. See `lib/services/host.dart` for more information."
}
}
},
"tooltipAddHostHeaders": "Add host headers",
"@tooltipAddHostHeaders": {
"description": "Tooltip for add host headers button",
"context": "app:settings:host"
},
"settingsHostHeaderTitle": "Set host header",
"@settingsHostHeaderTitle": {
"description": "Text displayed as description for host header input",
"context": "app:settings:host"
},
"settingsHostHeaderInvalid": "The entered text isn't a valid header JSON object",
"@settingsHostHeaderInvalid": {
"description": "Text displayed when the host header is invalid",
"context": "app:settings:host"
},
"settingsHostInvalidDetailed": "{type, select, url{The URL you entered is invalid. It isn't in a standardized URL format.} other{The host you entered is invalid. It cannot be reached. Please check the host and try again.}}",
"settingsHostInvalidDetailed": "{type, select, invalidUrl{The URL you entered isnt in a valid format.} unreachable{The host is invalid or unreachable, possibly due to a wrong address or network issue.} undetectable{The host is reachable but doesnt respond as expected by Ollama App.\nIt may not be an Ollama server. Are you sure you entered it correctly?} timeout{The host couldnt be reached in time.\nTry increasing the timeout multiplier in interface settings.} outdated{The host is running an Ollama version incompatible with this app.\nPlease update both the host and this app, if possible.} other{The host you entered is invalid.}}",
"@settingsHostInvalidDetailed": {
"description": "Text displayed when the host is invalid",
"description": "Detailed text displayed when the host is invalid",
"context": "app:settings:host",
"placeholders": {
"type": {
"type": "String",
"description": "Type of the issue, either 'url' or 'other' (preferably 'host')"
"description": "Type of the issue, either 'invalidUrl', 'unreachable', 'undetectable', 'timeout' or 'outdated'. See `lib/services/host.dart` for more information."
}
}
},
"settingsHostHeaderHeaderName": "Header",
"@settingsHostHeaderHeaderName": {
"description": "Text displayed as description for host header name input",
"context": "app:settings:host"
},
"settingsHostHeaderHeaderValue": "Value",
"@settingsHostHeaderHeaderValue": {
"description": "Text displayed as description for host header value input",
"context": "app:settings:host"
},
"settingsHostHeaderUnsupported": "The header “{header}” may not be supported.",
"@settingsHostHeaderUnsupported": {
"description": "Text displayed when a host header is unsupported",
"context": "app:settings:host",
"placeholders": {
"header": {
"type": "String",
"description": "The name of the unsupported header"
}
}
},
"settingsHostHeaderDuplicate": "The header “{header}” is already set.",
"@settingsHostHeaderDuplicate": {
"description": "Text displayed when a host header is a duplicate",
"context": "app:settings:host",
"placeholders": {
"header": {
"type": "String",
"description": "The name of the duplicate header"
}
}
},
@ -460,7 +578,7 @@
"description": "Text displayed as description for use system message toggle",
"context": "app:settings:behavior"
},
"settingsUseSystemDescription": "Disables setting the system message above and uses the one from the model's Modelfile instead. Can be useful for models with model files.",
"settingsUseSystemDescription": "Disables setting the system message above and uses the one from the models Modelfile instead. Can be useful for models with model files.",
"@settingsUseSystemDescription": {
"description": "Description of the use system message toggle",
"context": "app:settings:behavior"
@ -525,7 +643,7 @@
"description": "Text displayed as description for keep model loaded always toggle",
"context": "app:settings:interface:keepModelLoaded"
},
"settingsKeepModelLoadedNever": "Don't keep model loaded",
"settingsKeepModelLoadedNever": "Dont keep model loaded",
"@settingsKeepModelLoadedNever": {
"description": "Text displayed as description for don't keep model loaded toggle",
"context": "app:settings:interface:keepModelLoaded"
@ -606,7 +724,7 @@
"description": "Description of the temporary fixes section",
"context": "app:settings:interface:temporaryFixes"
},
"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": "Do not toggle any of these settings unless you know what you are doing! The given solutions might not work as expected.\nThey should not be considered final or judged as such. Issues might occur.",
"@settingsTemporaryFixesInstructions": {
"description": "Instructions and warnings for the temporary fixes",
"context": "app:settings:interface:temporaryFixes"
@ -616,7 +734,7 @@
"description": "Text displayed when no fixes are available",
"context": "app:settings:interface:temporaryFixes"
},
"settingsVoicePermissionLoading": "Loading voice permissions ...",
"settingsVoicePermissionLoading": "Loading voice permissions...",
"@settingsVoicePermissionLoading": {
"description": "Text displayed while loading voice permissions",
"context": "app:settings:voice"
@ -706,12 +824,12 @@
"description": "Text displayed when chats are imported successfully",
"context": "app:settings:export"
},
"settingsExportInfo": "These options allow you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or back up your chat history",
"settingsExportInfo": "These options allow you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or back up your chat history.",
"@settingsExportInfo": {
"description": "Information displayed for export and import options",
"context": "app:settings:export"
},
"settingsExportWarning": "Multiple chat histories won't be merged! You'll lose your current chat history if you import a new one.",
"settingsExportWarning": "Multiple chat histories wont be merged! Youll lose your current chat history if you import a new one.",
"@settingsExportWarning": {
"description": "Warning displayed for export and import options",
"context": "app:settings:export"
@ -721,7 +839,7 @@
"description": "Text displayed as description for check for updates button",
"context": "app:settings:info"
},
"settingsUpdateChecking": "Checking for updates ...",
"settingsUpdateChecking": "Checking for updates...",
"@settingsUpdateChecking": {
"description": "Text displayed while looking for updates",
"context": "app:settings:info"
@ -742,7 +860,7 @@
}
}
},
"settingsUpdateRateLimit": "Can't check, API rate limit exceeded",
"settingsUpdateRateLimit": "Cant check, API rate limit exceeded",
"@settingsUpdateRateLimit": {
"description": "Text displayed when the API rate limit is exceeded",
"context": "app:settings:info"
@ -777,7 +895,7 @@
"description": "Text displayed for cancel button, should be capitalized",
"context": "app:settings:info"
},
"settingsCheckForUpdates": "Check for updates on open",
"settingsCheckForUpdates": "Check for updates on startup",
"@settingsCheckForUpdates": {
"description": "Text displayed as description for check for updates toggle",
"context": "app:settings:info"

View File

@ -106,11 +106,17 @@ abstract class AppLocalizations {
Locale('zh'),
];
/// Title of the application
/// Title of the application. Should stay untranslated in most cases
///
/// In en, this message translates to:
/// **'Ollama'**
String get appTitle;
/// **'{env, select, short{Ollama} integrated{{title} - Ollama App} other{Ollama App}}'**
String appTitle(String env, String title);
/// Text displayed for learn more button
///
/// In en, this message translates to:
/// **'Learn more'**
String get learnMore;
/// Text displayed for new chat option
///
@ -130,12 +136,36 @@ abstract class AppLocalizations {
/// **'Install Webapp'**
String get optionInstallPwa;
/// Text displayed when no chats are found
/// Text displayed when there are no chats
///
/// In en, this message translates to:
/// **'No chats found'**
/// **'No chats'**
String get optionNoChatFound;
/// Text displayed when no chats are found for the search query
///
/// In en, this message translates to:
/// **'No chats found for {query}'**
String optionNoChatFoundSearch(String query);
/// Placeholder for search chats input
///
/// In en, this message translates to:
/// **'Search through chats'**
String get optionSearchChats;
/// Text displayed as title for chat details sidebar
///
/// In en, this message translates to:
/// **'Chat details'**
String get optionChatDetails;
/// Text displayed when no chat is selected in the chat details sidebar
///
/// In en, this message translates to:
/// **'No chat selected'**
String get optionChatDetailsNoChat;
/// Prefix for tips
///
/// In en, this message translates to:
@ -187,38 +217,68 @@ abstract class AppLocalizations {
/// Text displayed for take image button
///
/// In en, this message translates to:
/// **'Take Image'**
/// **'Camera'**
String get takeImage;
/// Text displayed for image upload button
///
/// In en, this message translates to:
/// **'Upload Image'**
/// **'Gallery'**
String get uploadImage;
/// Text displayed when an image is not valid
/// First greeting displayed in a new chat
///
/// In en, this message translates to:
/// **'Not a valid image'**
String get notAValidImage;
/// **'Whats on your mind, {user}?'**
String newChatGreeting1(String user);
/// Title, if 'Generate Title' is executed on a conversation with no text messages
/// Second greeting displayed in a new chat
///
/// In en, this message translates to:
/// **'Image Only Conversation'**
String get imageOnlyConversation;
/// **'Good to see you, {user}.'**
String newChatGreeting2(String user);
/// Third greeting displayed in a new chat
///
/// In en, this message translates to:
/// **'What can I do for you, {user}?'**
String newChatGreeting3(String user);
/// Fourth greeting displayed in a new chat
///
/// In en, this message translates to:
/// **'How can I help you today, {user}?'**
String newChatGreeting4(String user);
/// Fifth greeting displayed in a new chat
///
/// In en, this message translates to:
/// **'What would you like to talk about, {user}?'**
String newChatGreeting5(String user);
/// Fallback name for the user if no name is set
///
/// In en, this message translates to:
/// **'user'**
String get newChatGreetingNameFallback;
/// Placeholder text for message input
///
/// In en, this message translates to:
/// **'Message'**
String get messageInputPlaceholder;
/// **'Ask {model}'**
String messageInputPlaceholder(String model);
/// Tooltip for attachment button
/// Model name used in 'messageInputPlaceholder' if no model is available, should fit into capitalization and context
///
/// In en, this message translates to:
/// **'Add attachment'**
String get tooltipAttachment;
/// **'model'**
String get messageInputPlaceholderModelPlaceholder;
/// Tooltip for message options button; options here being attachments, etc.
///
/// In en, this message translates to:
/// **'Message options'**
String get tooltipMessageOptions;
/// Tooltip for send button
///
@ -226,17 +286,17 @@ abstract class AppLocalizations {
/// **'Send'**
String get tooltipSend;
/// Tooltip for save button
/// Tooltip for generate with model button
///
/// In en, this message translates to:
/// **'Save'**
String get tooltipSave;
/// **'Generate with {model}'**
String tooltipLetAIThink(String model);
/// Tooltip for let AI think button
/// Tooltip for add host headers button
///
/// In en, this message translates to:
/// **'Let AI think'**
String get tooltipLetAIThink;
/// **'Add host headers'**
String get tooltipAddHostHeaders;
/// Tooltip for reset button
///
@ -259,121 +319,115 @@ abstract class AppLocalizations {
/// Text displayed when no host is selected
///
/// In en, this message translates to:
/// **'No host selected, open settings to set one'**
/// **'No host selected, open settings to set one.'**
String get noHostSelected;
/// Text displayed when no model is selected
/// Text displayed when no model is selected. Should be lower case even if the language would capitalize it, to fit better into the app's design.
///
/// In en, this message translates to:
/// **'<selector>'**
/// **'<model>'**
String get noSelectedModel;
/// Title of a new chat
///
/// In en, this message translates to:
/// **'Unnamed Chat'**
/// **'Untitled Chat'**
String get newChatTitle;
/// Title of the model selection dialog
///
/// In en, this message translates to:
/// **'Select a model'**
String get modelDialogTitle;
/// Text displayed for add model button
///
/// In en, this message translates to:
/// **'Add'**
String get modelDialogAddModel;
String get modelDialogAdd;
/// Text displayed for refresh models button
///
/// In en, this message translates to:
/// **'Refresh'**
String get modelDialogRefresh;
/// Title of the add model dialog
///
/// In en, this message translates to:
/// **'Add new model'**
/// **'Add model'**
String get modelDialogAddPromptTitle;
/// Description of the add model dialog
///
/// In en, this message translates to:
/// **'This can either be a normal name (e.g. \'llama3\') or a name and tag (e.g. \'llama3:70b\').'**
/// **'This can either be a normal name (e.g. “llama3”) or a name together with a tag (e.g. “llama3:70b”).'**
String get modelDialogAddPromptDescription;
/// Text displayed when there are currently downloading models
///
/// In en, this message translates to:
/// **'Currently downloading:'**
String get modelDialogAddCurrently;
/// Text displayed when the model already exists
///
/// In en, this message translates to:
/// **'Model already exists'**
/// **'Model already exists. Try refreshing if it was deleted.'**
String get modelDialogAddPromptAlreadyExists;
/// Text displayed when the model name is invalid
///
/// In en, this message translates to:
/// **'Invalid model name'**
/// **'Couldnt read your model name correctly. Please check the spelling and try again.'**
String get modelDialogAddPromptInvalid;
/// Title of the allow proxy dialog
/// Text displayed when the model could not be found
///
/// In en, this message translates to:
/// **'Allow Proxy'**
String get modelDialogAddAllowanceTitle;
/// **'Couldnt find your model on the server. Please check the spelling and try again.'**
String get modelDialogAddPromptNotFound;
/// Description of the allow proxy dialog
/// Text displayed when there was a network error while checking the model
///
/// In en, this message translates to:
/// **'Ollama App must check if the entered model is valid. To do that, we normally send a web request to the Ollama model list and check the status code, but because you\'re using the web client, we can\'t do that directly. Instead, the app will send the request to a different API, hosted by JHubi1, to check for us.\nThis is a one-time request and will only be sent when you add a new model.\nYour IP address will be sent with the request and might be stored for up to ten minutes to prevent spamming with potentially harmful intentions.\nIf you accept, your selection will be remembered for the future; if not, nothing will be sent and the model won\'t be added.'**
String get modelDialogAddAllowanceDescription;
/// **'Network error while checking model. Please check your connection and try again.'**
String get modelDialogAddPromptNetworkError;
/// Text displayed for allow button, should be capitalized
/// Text displayed when the model is corrupted on the server
///
/// In en, this message translates to:
/// **'Allow'**
String get modelDialogAddAllowanceAllow;
/// **'The API returned “EOF”; your server was likely restarted while downloading the model previously.'**
String get modelDialogAddPromptCorruptionError;
/// Text displayed for deny button, should be capitalized
/// Text displayed as solution for the corrupted model error
///
/// In en, this message translates to:
/// **'Deny'**
String get modelDialogAddAllowanceDeny;
/// **'Alternatively, you can run the following command on your Linux host directly:\nIf you see a new error, try running the command with “sudo” placed in front of it.'**
String get modelDialogAddPromptCorruptionErrorSolution;
/// Title of the add model assurance dialog
/// Title of the cancel download dialog
///
/// In en, this message translates to:
/// **'Add {model}?'**
String modelDialogAddAssuranceTitle(String model);
/// **'Cancel download?'**
String get modelDialogAddCancelTitle;
/// Description of the add model assurance dialog
///
/// In en, this message translates to:
/// **'Pressing \'Add\' will download the model \'{model}\' directly from the Ollama server to your host.\nThis can take a while depending on your internet connection. The action cannot be canceled.\nIf the app is closed during the download, it will resume if you enter the name into the model dialog again.'**
String modelDialogAddAssuranceDescription(String model);
/// Text displayed for add button, should be capitalized
///
/// In en, this message translates to:
/// **'Add'**
String get modelDialogAddAssuranceAdd;
/// Text displayed for cancel button, should be capitalized
/// Text displayed for cancel download button
///
/// In en, this message translates to:
/// **'Cancel'**
String get modelDialogAddAssuranceCancel;
String get modelDialogAddCancelConfirm;
/// Text displayed while loading the download progress
/// Text displayed for hide button in cancel download dialog. Hide in the sense of 'hide this dialog'
///
/// In en, this message translates to:
/// **'loading progress'**
String get modelDialogAddDownloadPercentLoading;
/// Text displayed while downloading a model
///
/// In en, this message translates to:
/// **'download at {percent}%'**
String modelDialogAddDownloadPercent(String percent);
/// Text displayed when the download of a model fails
///
/// In en, this message translates to:
/// **'Disconnected, try again'**
String get modelDialogAddDownloadFailed;
/// **'Hide'**
String get modelDialogAddCancelHide;
/// Text displayed when the download of a model is successful
///
/// In en, this message translates to:
/// **'Download successful'**
/// **'Download successful. Set as current model.'**
String get modelDialogAddDownloadSuccess;
/// Title of the delete dialog
@ -442,6 +496,18 @@ abstract class AppLocalizations {
/// **'Edit message'**
String get dialogEditMessageTitle;
/// Title of the overview settings section
///
/// In en, this message translates to:
/// **'Overview'**
String get settingsTitleOverview;
/// Description of the overview settings section
///
/// In en, this message translates to:
/// **'General settings regarding your host.'**
String get settingsDescriptionOverview;
/// Title of the behavior settings section
///
/// In en, this message translates to:
@ -502,12 +568,6 @@ abstract class AppLocalizations {
/// **'Check for updates and learn more about Ollama App.'**
String get settingsDescriptionAbout;
/// Text displayed when settings are saved automatically
///
/// In en, this message translates to:
/// **'Settings are saved automatically'**
String get settingsSavedAutomatically;
/// Text displayed when a feature is in alpha phase
///
/// In en, this message translates to:
@ -517,7 +577,7 @@ abstract class AppLocalizations {
/// Description of the alpha feature
///
/// In en, this message translates to:
/// **'This feature is in alpha and may not work as intended or expected.\nCritical issues and/or permanent critical damage to device and/or used services cannot be ruled out.\nUse at your own risk. No liability on the part of the app author.'**
/// **'This feature is in alpha and may not work as intended or expected.\nCritical issues and/or permanent critical damage to device and/or connected services cannot be ruled out.\nUse at your own risk. No liability on the part of the app author.'**
String get settingsExperimentalAlphaDescription;
/// Text displayed when a feature is in alpha
@ -535,7 +595,7 @@ abstract class AppLocalizations {
/// Description of the beta feature
///
/// In en, this message translates to:
/// **'This feature is in beta and may not work as intended or expected.\nLess severe issues may or may not occur. Damage shouldn\'t be critical.\nUse at your own risk.'**
/// **'This feature is in beta and may not work as intended or expected.\nLess severe issues may or may not occur. Damage shouldnt be critical.\nUse at your own risk.'**
String get settingsExperimentalBetaDescription;
/// Text displayed when a feature is in beta
@ -568,6 +628,12 @@ abstract class AppLocalizations {
/// **'Host'**
String get settingsHost;
/// Text displayed when no host is set
///
/// In en, this message translates to:
/// **'No host set'**
String get settingsHostMissing;
/// Text displayed when the host is valid
///
/// In en, this message translates to:
@ -580,36 +646,42 @@ abstract class AppLocalizations {
/// **'Checking Host'**
String get settingsHostChecking;
/// Text displayed when the host is invalid
/// Short title displayed when the host is invalid
///
/// In en, this message translates to:
/// **'Issue: {type, select, url{Invalid URL} host{Invalid Host} timeout{Request failed. Server issues} ratelimit{Too many requests} other{Request Failed}}'**
/// **'{type, select, invalidUrl{Invalid URL} unreachable{Host unreachable} undetectable{Host not an Ollama server} timeout{Connection timed out} outdated{Outdated Ollama version} other{Invalid host}}'**
String settingsHostInvalid(String type);
/// Tooltip for add host headers button
/// Detailed text displayed when the host is invalid
///
/// In en, this message translates to:
/// **'Add host headers'**
String get tooltipAddHostHeaders;
/// Text displayed as description for host header input
///
/// In en, this message translates to:
/// **'Set host header'**
String get settingsHostHeaderTitle;
/// Text displayed when the host header is invalid
///
/// In en, this message translates to:
/// **'The entered text isn\'t a valid header JSON object'**
String get settingsHostHeaderInvalid;
/// Text displayed when the host is invalid
///
/// In en, this message translates to:
/// **'{type, select, url{The URL you entered is invalid. It isn\'t in a standardized URL format.} other{The host you entered is invalid. It cannot be reached. Please check the host and try again.}}'**
/// **'{type, select, invalidUrl{The URL you entered isnt in a valid format.} unreachable{The host is invalid or unreachable, possibly due to a wrong address or network issue.} undetectable{The host is reachable but doesnt respond as expected by Ollama App.\nIt may not be an Ollama server. Are you sure you entered it correctly?} timeout{The host couldnt be reached in time.\nTry increasing the timeout multiplier in interface settings.} outdated{The host is running an Ollama version incompatible with this app.\nPlease update both the host and this app, if possible.} other{The host you entered is invalid.}}'**
String settingsHostInvalidDetailed(String type);
/// Text displayed as description for host header name input
///
/// In en, this message translates to:
/// **'Header'**
String get settingsHostHeaderHeaderName;
/// Text displayed as description for host header value input
///
/// In en, this message translates to:
/// **'Value'**
String get settingsHostHeaderHeaderValue;
/// Text displayed when a host header is unsupported
///
/// In en, this message translates to:
/// **'The header “{header}” may not be supported.'**
String settingsHostHeaderUnsupported(String header);
/// Text displayed when a host header is a duplicate
///
/// In en, this message translates to:
/// **'The header “{header}” is already set.'**
String settingsHostHeaderDuplicate(String header);
/// Text displayed as description for system message input
///
/// In en, this message translates to:
@ -625,7 +697,7 @@ abstract class AppLocalizations {
/// Description of the use system message toggle
///
/// In en, this message translates to:
/// **'Disables setting the system message above and uses the one from the model\'s Modelfile instead. Can be useful for models with model files.'**
/// **'Disables setting the system message above and uses the one from the models Modelfile instead. Can be useful for models with model files.'**
String get settingsUseSystemDescription;
/// Text displayed as description for disable markdown toggle
@ -703,7 +775,7 @@ abstract class AppLocalizations {
/// Text displayed as description for don't keep model loaded toggle
///
/// In en, this message translates to:
/// **'Don\'t keep model loaded'**
/// **'Dont keep model loaded'**
String get settingsKeepModelLoadedNever;
/// Text displayed as description for keep model loaded for toggle
@ -793,7 +865,7 @@ abstract class AppLocalizations {
/// Instructions and warnings for the temporary fixes
///
/// In en, this message translates to:
/// **'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.'**
/// **'Do not toggle any of these settings unless you know what you are doing! The given solutions might not work as expected.\nThey should not be considered final or judged as such. Issues might occur.'**
String get settingsTemporaryFixesInstructions;
/// Text displayed when no fixes are available
@ -805,7 +877,7 @@ abstract class AppLocalizations {
/// Text displayed while loading voice permissions
///
/// In en, this message translates to:
/// **'Loading voice permissions ...'**
/// **'Loading voice permissions...'**
String get settingsVoicePermissionLoading;
/// Text displayed when text-to-speech is not supported
@ -913,13 +985,13 @@ abstract class AppLocalizations {
/// Information displayed for export and import options
///
/// In en, this message translates to:
/// **'These options allow you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or back up your chat history'**
/// **'These options allow you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or back up your chat history.'**
String get settingsExportInfo;
/// Warning displayed for export and import options
///
/// In en, this message translates to:
/// **'Multiple chat histories won\'t be merged! You\'ll lose your current chat history if you import a new one.'**
/// **'Multiple chat histories wont be merged! Youll lose your current chat history if you import a new one.'**
String get settingsExportWarning;
/// Text displayed as description for check for updates button
@ -931,7 +1003,7 @@ abstract class AppLocalizations {
/// Text displayed while looking for updates
///
/// In en, this message translates to:
/// **'Checking for updates ...'**
/// **'Checking for updates...'**
String get settingsUpdateChecking;
/// Text displayed when the app is up to date
@ -949,7 +1021,7 @@ abstract class AppLocalizations {
/// Text displayed when the API rate limit is exceeded
///
/// In en, this message translates to:
/// **'Can\'t check, API rate limit exceeded'**
/// **'Cant check, API rate limit exceeded'**
String get settingsUpdateRateLimit;
/// Text displayed when an issue occurs while checking for updates
@ -991,7 +1063,7 @@ abstract class AppLocalizations {
/// Text displayed as description for check for updates toggle
///
/// In en, this message translates to:
/// **'Check for updates on open'**
/// **'Check for updates on startup'**
String get settingsCheckForUpdates;
/// Text displayed as description for GitHub button

View File

@ -9,7 +9,17 @@ class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get appTitle => 'Ollama';
String appTitle(String env, String title) {
String _temp0 = intl.Intl.selectLogic(env, {
'short': 'Ollama',
'integrated': '$title - Ollama App',
'other': 'Ollama App',
});
return '$_temp0';
}
@override
String get learnMore => 'Mehr erfahren';
@override
String get optionNewChat => 'Neuer Chat';
@ -21,7 +31,21 @@ class AppLocalizationsDe extends AppLocalizations {
String get optionInstallPwa => 'Webapp installieren';
@override
String get optionNoChatFound => 'Keine Chats gefunden';
String get optionNoChatFound => 'Keine Chats';
@override
String optionNoChatFoundSearch(String query) {
return 'Keinen Chat gefunden für Suche $query';
}
@override
String get optionSearchChats => 'Chats durchsuchen';
@override
String get optionChatDetails => 'Chat details';
@override
String get optionChatDetailsNoChat => 'No chat selected';
@override
String get tipPrefix => 'Tipp: ';
@ -54,25 +78,54 @@ class AppLocalizationsDe extends AppLocalizations {
String get uploadImage => 'Bild Hochladen';
@override
String get notAValidImage => 'Kein gültiges Bild';
String newChatGreeting1(String user) {
return 'Whats on your mind, $user?';
}
@override
String get imageOnlyConversation => 'Nur Bild Unterhaltung';
String newChatGreeting2(String user) {
return 'Good to see you, $user.';
}
@override
String get messageInputPlaceholder => 'Nachricht';
String newChatGreeting3(String user) {
return 'What can I do for you, $user?';
}
@override
String get tooltipAttachment => 'Anhang hinzufügen';
String newChatGreeting4(String user) {
return 'How can I help you today, $user?';
}
@override
String newChatGreeting5(String user) {
return 'What would you like to talk about, $user?';
}
@override
String get newChatGreetingNameFallback => 'user';
@override
String messageInputPlaceholder(String model) {
return '$model fragen';
}
@override
String get messageInputPlaceholderModelPlaceholder => 'Modell';
@override
String get tooltipMessageOptions => 'Nachrichtenoptionen';
@override
String get tooltipSend => 'Senden';
@override
String get tooltipSave => 'Speichern';
String tooltipLetAIThink(String model) {
return 'Mit $model generieren';
}
@override
String get tooltipLetAIThink => 'Lass KI denken';
String get tooltipAddHostHeaders => 'Host-Header festlegen';
@override
String get tooltipReset => 'Aktuellen Chat zurücksetzen';
@ -85,72 +138,67 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get noHostSelected =>
'Kein Host ausgewählt, öffne zum Auswählen die Einstellungen';
'Kein Host ausgewählt, öffne zum Auswählen die Einstellungen.';
@override
String get noSelectedModel => '<selektor>';
String get noSelectedModel => '<model>';
@override
String get newChatTitle => 'Unbenannter Chat';
@override
String get modelDialogAddModel => 'Hinzufügen';
String get modelDialogTitle => 'Select a model';
@override
String get modelDialogAddPromptTitle => 'Neues Modell hinzufügen';
String get modelDialogAdd => 'Hinzufügen';
@override
String get modelDialogRefresh => 'Refresh';
@override
String get modelDialogAddPromptTitle => 'Modell hinzufügen';
@override
String get modelDialogAddPromptDescription =>
'Das kann entweder ein normaler Name (z.B. \'llama3\') oder Name und Tag (z.B. \'llama3:70b\') sein.';
'Das kann entweder ein normaler Name (z.B. „llama3“) oder ein Name zusammen mit einem Tag (z.B. „llama3:70b“) sein.';
@override
String get modelDialogAddPromptAlreadyExists => 'Modell existiert bereits';
String get modelDialogAddCurrently => 'Wird gerade heruntergeladen:';
@override
String get modelDialogAddPromptInvalid => 'Ungültiger Modellname';
String get modelDialogAddPromptAlreadyExists => 'Modell existiert bereits.';
@override
String get modelDialogAddAllowanceTitle => 'Proxy erlauben';
String get modelDialogAddPromptInvalid => 'Ungültiger Modellname.';
@override
String get modelDialogAddAllowanceDescription =>
'Ollama App muss überprüfen, ob das eingegebene Modell gültig ist. Dafür senden wir normalerweise eine Webanfrage an die Ollama-Modellliste und überprüfen den Statuscode, aber da gerade der Webclient verwendet wird, können wir das nicht direkt tun. Stattdessen sendet die App die Anfrage an eine andere API, gehostet von JHubi1, um dies für uns zu überprüfen.\nDies ist eine einmalige Anfrage und wird nur gesendet, wenn du ein neues Modell hinzufügst.\nIhre IP-Adresse wird mit der Anfrage gesendet und kann bis zu zehn Minuten gespeichert werden, um Spamming mit potenziell schädlichen Absichten zu verhindern.\nWenn du zustimmst, wird deine Auswahl für die Zukunft gespeichert; wenn nicht, wird nichts gesendet und das Modell wird nicht hinzugefügt.';
String get modelDialogAddPromptNotFound =>
'Das Modell konnte auf dem Server nicht gefunden werden. Bitte überprüfe die Schreibweise und versuche es erneut.';
@override
String get modelDialogAddAllowanceAllow => 'Erlauben';
String get modelDialogAddPromptNetworkError =>
'Netzwerkfehler beim Überprüfen des Modells. Bitte überprüfe deine Verbindung und versuche es erneut.';
@override
String get modelDialogAddAllowanceDeny => 'Ablehnen';
String get modelDialogAddPromptCorruptionError =>
'Die API hat „EOF“ zurückgegeben; dein Server wurde wahrscheinlich neu gestartet, während das Modell zuvor heruntergeladen wurde.';
@override
String modelDialogAddAssuranceTitle(String model) {
return '$model hinzufügen?';
}
String get modelDialogAddPromptCorruptionErrorSolution =>
'Alternativ kannst du den folgenden Befehl direkt auf deinem Linux-Host ausführen:\nWenn du eine neue Fehlermeldung siehst, versuche den Befehl mit „sudo“ davor auszuführen.';
@override
String modelDialogAddAssuranceDescription(String model) {
return 'Durch Drücken von \'Hinzufügen\' wird das Modell \'$model\' direkt vom Ollama-Server auf deinen Host heruntergeladen.\nJe nach Internetverbindung kann dies eine Weile dauern. Der Vorgang kann nicht abgebrochen werden.\nWenn die App während des Downloads geschlossen wird, wird der Download fortgesetzt, wenn du den Namen erneut in den Modelldialog eingibst.';
}
String get modelDialogAddCancelTitle => 'Download abbrechen?';
@override
String get modelDialogAddAssuranceAdd => 'Hinzufügen';
String get modelDialogAddCancelConfirm => 'Abbrechen';
@override
String get modelDialogAddAssuranceCancel => 'Abbrechen';
String get modelDialogAddCancelHide => 'Ausblenden';
@override
String get modelDialogAddDownloadPercentLoading => 'lade Fortschritt';
@override
String modelDialogAddDownloadPercent(String percent) {
return 'Download bei $percent%';
}
@override
String get modelDialogAddDownloadFailed => 'Getrennt, versuche es erneut';
@override
String get modelDialogAddDownloadSuccess => 'Download erfolgreich';
String get modelDialogAddDownloadSuccess =>
'Download erfolgreich. Als aktuelles Modell festlegen.';
@override
String get deleteDialogTitle => 'Chat löschen';
@ -186,6 +234,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get dialogEditMessageTitle => 'Nachricht bearbeiten';
@override
String get settingsTitleOverview => 'Overview';
@override
String get settingsDescriptionOverview =>
'General settings regarding your host.';
@override
String get settingsTitleBehavior => 'Verhalten';
@ -221,10 +276,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsDescriptionAbout =>
'Suche nach Updates und erfahre mehr über Ollama App.';
@override
String get settingsSavedAutomatically =>
'Einstellungen werden automatisch gespeichert';
@override
String get settingsExperimentalAlpha => 'alpha';
@ -261,6 +312,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settingsHost => 'Host';
@override
String get settingsHostMissing => 'Kein Host festgelegt';
@override
String get settingsHostValid => 'Gültiger Host';
@ -270,36 +324,49 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String settingsHostInvalid(String type) {
String _temp0 = intl.Intl.selectLogic(type, {
'url': 'Ungültige URL',
'host': 'Ungültiger Host',
'timeout': 'Request Fehlgeschlagen. Server Fehler',
'ratelimit': 'Zu viele Anfragen',
'other': 'Request Fehlgeschlagen',
'invalidUrl': 'Ungültige URL',
'unreachable': 'Host nicht erreichbar',
'undetectable': 'Host kein Ollama-Server',
'timeout': 'Verbindung abgelaufen',
'outdated': 'Veraltete Ollama-Version',
'other': 'Ungültiger Host',
});
return 'Fehler: $_temp0';
return '$_temp0';
}
@override
String get tooltipAddHostHeaders => 'Host-Header festlegen';
@override
String get settingsHostHeaderTitle => 'Host-Header festlegen';
@override
String get settingsHostHeaderInvalid =>
'Der eingegebene Text ist kein gültiges Header-JSON-Objekt';
@override
String settingsHostInvalidDetailed(String type) {
String _temp0 = intl.Intl.selectLogic(type, {
'url':
'Die eingegebene URL ist ungültig. Es handelt sich nicht um ein standardisiertes URL-Format.',
'other':
'Der eingegebene Host ist ungültig. Er kann nicht erreicht werden. Bitte überprüfe den Host und versuche es erneut.',
'invalidUrl': 'Die eingegebene URL hat kein gültiges Format.',
'unreachable':
'Der Host ist ungültig oder nicht erreichbar, möglicherweise aufgrund einer falschen Adresse oder eines Netzwerkproblems.',
'undetectable':
'Der Host ist erreichbar, antwortet aber nicht wie von Ollama App erwartet.\nEr ist möglicherweise kein Ollama-Server. Bist du sicher, dass du ihn richtig eingegeben hast?',
'timeout':
'Der Host konnte nicht rechtzeitig erreicht werden.\nVersuche, den Timeout-Multiplikator in den Oberflächen-Einstellungen zu erhöhen.',
'outdated':
'Der Host verwendet eine Ollama-Version, die mit dieser App nicht kompatibel ist.\nBitte aktualisiere sowohl den Host als auch diese App, wenn möglich.',
'other': 'Der eingegebene Host ist ungültig.',
});
return '$_temp0';
}
@override
String get settingsHostHeaderHeaderName => 'Header';
@override
String get settingsHostHeaderHeaderValue => 'Value';
@override
String settingsHostHeaderUnsupported(String header) {
return 'The header “$header” may not be supported.';
}
@override
String settingsHostHeaderDuplicate(String header) {
return 'The header “$header” is already set.';
}
@override
String get settingsSystemMessage => 'Systemnachricht';

View File

@ -9,7 +9,17 @@ class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'Ollama';
String appTitle(String env, String title) {
String _temp0 = intl.Intl.selectLogic(env, {
'short': 'Ollama',
'integrated': '$title - Ollama App',
'other': 'Ollama App',
});
return '$_temp0';
}
@override
String get learnMore => 'Learn more';
@override
String get optionNewChat => 'New Chat';
@ -21,7 +31,21 @@ class AppLocalizationsEn extends AppLocalizations {
String get optionInstallPwa => 'Install Webapp';
@override
String get optionNoChatFound => 'No chats found';
String get optionNoChatFound => 'No chats';
@override
String optionNoChatFoundSearch(String query) {
return 'No chats found for $query';
}
@override
String get optionSearchChats => 'Search through chats';
@override
String get optionChatDetails => 'Chat details';
@override
String get optionChatDetailsNoChat => 'No chat selected';
@override
String get tipPrefix => 'Tip: ';
@ -48,31 +72,60 @@ class AppLocalizationsEn extends AppLocalizations {
String get renameChat => 'Rename';
@override
String get takeImage => 'Take Image';
String get takeImage => 'Camera';
@override
String get uploadImage => 'Upload Image';
String get uploadImage => 'Gallery';
@override
String get notAValidImage => 'Not a valid image';
String newChatGreeting1(String user) {
return 'Whats on your mind, $user?';
}
@override
String get imageOnlyConversation => 'Image Only Conversation';
String newChatGreeting2(String user) {
return 'Good to see you, $user.';
}
@override
String get messageInputPlaceholder => 'Message';
String newChatGreeting3(String user) {
return 'What can I do for you, $user?';
}
@override
String get tooltipAttachment => 'Add attachment';
String newChatGreeting4(String user) {
return 'How can I help you today, $user?';
}
@override
String newChatGreeting5(String user) {
return 'What would you like to talk about, $user?';
}
@override
String get newChatGreetingNameFallback => 'user';
@override
String messageInputPlaceholder(String model) {
return 'Ask $model';
}
@override
String get messageInputPlaceholderModelPlaceholder => 'model';
@override
String get tooltipMessageOptions => 'Message options';
@override
String get tooltipSend => 'Send';
@override
String get tooltipSave => 'Save';
String tooltipLetAIThink(String model) {
return 'Generate with $model';
}
@override
String get tooltipLetAIThink => 'Let AI think';
String get tooltipAddHostHeaders => 'Add host headers';
@override
String get tooltipReset => 'Reset current chat';
@ -84,72 +137,69 @@ class AppLocalizationsEn extends AppLocalizations {
String get noModelSelected => 'No model selected';
@override
String get noHostSelected => 'No host selected, open settings to set one';
String get noHostSelected => 'No host selected, open settings to set one.';
@override
String get noSelectedModel => '<selector>';
String get noSelectedModel => '<model>';
@override
String get newChatTitle => 'Unnamed Chat';
String get newChatTitle => 'Untitled Chat';
@override
String get modelDialogAddModel => 'Add';
String get modelDialogTitle => 'Select a model';
@override
String get modelDialogAddPromptTitle => 'Add new model';
String get modelDialogAdd => 'Add';
@override
String get modelDialogRefresh => 'Refresh';
@override
String get modelDialogAddPromptTitle => 'Add model';
@override
String get modelDialogAddPromptDescription =>
'This can either be a normal name (e.g. \'llama3\') or a name and tag (e.g. \'llama3:70b\').';
'This can either be a normal name (e.g. “llama3”) or a name together with a tag (e.g. “llama3:70b”).';
@override
String get modelDialogAddPromptAlreadyExists => 'Model already exists';
String get modelDialogAddCurrently => 'Currently downloading:';
@override
String get modelDialogAddPromptInvalid => 'Invalid model name';
String get modelDialogAddPromptAlreadyExists =>
'Model already exists. Try refreshing if it was deleted.';
@override
String get modelDialogAddAllowanceTitle => 'Allow Proxy';
String get modelDialogAddPromptInvalid =>
'Couldnt read your model name correctly. Please check the spelling and try again.';
@override
String get modelDialogAddAllowanceDescription =>
'Ollama App must check if the entered model is valid. To do that, we normally send a web request to the Ollama model list and check the status code, but because you\'re using the web client, we can\'t do that directly. Instead, the app will send the request to a different API, hosted by JHubi1, to check for us.\nThis is a one-time request and will only be sent when you add a new model.\nYour IP address will be sent with the request and might be stored for up to ten minutes to prevent spamming with potentially harmful intentions.\nIf you accept, your selection will be remembered for the future; if not, nothing will be sent and the model won\'t be added.';
String get modelDialogAddPromptNotFound =>
'Couldnt find your model on the server. Please check the spelling and try again.';
@override
String get modelDialogAddAllowanceAllow => 'Allow';
String get modelDialogAddPromptNetworkError =>
'Network error while checking model. Please check your connection and try again.';
@override
String get modelDialogAddAllowanceDeny => 'Deny';
String get modelDialogAddPromptCorruptionError =>
'The API returned “EOF”; your server was likely restarted while downloading the model previously.';
@override
String modelDialogAddAssuranceTitle(String model) {
return 'Add $model?';
}
String get modelDialogAddPromptCorruptionErrorSolution =>
'Alternatively, you can run the following command on your Linux host directly:\nIf you see a new error, try running the command with “sudo” placed in front of it.';
@override
String modelDialogAddAssuranceDescription(String model) {
return 'Pressing \'Add\' will download the model \'$model\' directly from the Ollama server to your host.\nThis can take a while depending on your internet connection. The action cannot be canceled.\nIf the app is closed during the download, it will resume if you enter the name into the model dialog again.';
}
String get modelDialogAddCancelTitle => 'Cancel download?';
@override
String get modelDialogAddAssuranceAdd => 'Add';
String get modelDialogAddCancelConfirm => 'Cancel';
@override
String get modelDialogAddAssuranceCancel => 'Cancel';
String get modelDialogAddCancelHide => 'Hide';
@override
String get modelDialogAddDownloadPercentLoading => 'loading progress';
@override
String modelDialogAddDownloadPercent(String percent) {
return 'download at $percent%';
}
@override
String get modelDialogAddDownloadFailed => 'Disconnected, try again';
@override
String get modelDialogAddDownloadSuccess => 'Download successful';
String get modelDialogAddDownloadSuccess =>
'Download successful. Set as current model.';
@override
String get deleteDialogTitle => 'Delete Chat';
@ -185,6 +235,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get dialogEditMessageTitle => 'Edit message';
@override
String get settingsTitleOverview => 'Overview';
@override
String get settingsDescriptionOverview =>
'General settings regarding your host.';
@override
String get settingsTitleBehavior => 'Behavior';
@ -220,15 +277,12 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsDescriptionAbout =>
'Check for updates and learn more about Ollama App.';
@override
String get settingsSavedAutomatically => 'Settings are saved automatically';
@override
String get settingsExperimentalAlpha => 'alpha';
@override
String get settingsExperimentalAlphaDescription =>
'This feature is in alpha and may not work as intended or expected.\nCritical issues and/or permanent critical damage to device and/or used services cannot be ruled out.\nUse at your own risk. No liability on the part of the app author.';
'This feature is in alpha and may not work as intended or expected.\nCritical issues and/or permanent critical damage to device and/or connected services cannot be ruled out.\nUse at your own risk. No liability on the part of the app author.';
@override
String get settingsExperimentalAlphaFeature =>
@ -239,7 +293,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsExperimentalBetaDescription =>
'This feature is in beta and may not work as intended or expected.\nLess severe issues may or may not occur. Damage shouldn\'t be critical.\nUse at your own risk.';
'This feature is in beta and may not work as intended or expected.\nLess severe issues may or may not occur. Damage shouldnt be critical.\nUse at your own risk.';
@override
String get settingsExperimentalBetaFeature =>
@ -259,6 +313,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsHost => 'Host';
@override
String get settingsHostMissing => 'No host set';
@override
String get settingsHostValid => 'Valid Host';
@ -268,36 +325,49 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String settingsHostInvalid(String type) {
String _temp0 = intl.Intl.selectLogic(type, {
'url': 'Invalid URL',
'host': 'Invalid Host',
'timeout': 'Request failed. Server issues',
'ratelimit': 'Too many requests',
'other': 'Request Failed',
'invalidUrl': 'Invalid URL',
'unreachable': 'Host unreachable',
'undetectable': 'Host not an Ollama server',
'timeout': 'Connection timed out',
'outdated': 'Outdated Ollama version',
'other': 'Invalid host',
});
return 'Issue: $_temp0';
return '$_temp0';
}
@override
String get tooltipAddHostHeaders => 'Add host headers';
@override
String get settingsHostHeaderTitle => 'Set host header';
@override
String get settingsHostHeaderInvalid =>
'The entered text isn\'t a valid header JSON object';
@override
String settingsHostInvalidDetailed(String type) {
String _temp0 = intl.Intl.selectLogic(type, {
'url':
'The URL you entered is invalid. It isn\'t in a standardized URL format.',
'other':
'The host you entered is invalid. It cannot be reached. Please check the host and try again.',
'invalidUrl': 'The URL you entered isnt in a valid format.',
'unreachable':
'The host is invalid or unreachable, possibly due to a wrong address or network issue.',
'undetectable':
'The host is reachable but doesnt respond as expected by Ollama App.\nIt may not be an Ollama server. Are you sure you entered it correctly?',
'timeout':
'The host couldnt be reached in time.\nTry increasing the timeout multiplier in interface settings.',
'outdated':
'The host is running an Ollama version incompatible with this app.\nPlease update both the host and this app, if possible.',
'other': 'The host you entered is invalid.',
});
return '$_temp0';
}
@override
String get settingsHostHeaderHeaderName => 'Header';
@override
String get settingsHostHeaderHeaderValue => 'Value';
@override
String settingsHostHeaderUnsupported(String header) {
return 'The header “$header” may not be supported.';
}
@override
String settingsHostHeaderDuplicate(String header) {
return 'The header “$header” is already set.';
}
@override
String get settingsSystemMessage => 'System message';
@ -306,7 +376,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsUseSystemDescription =>
'Disables setting the system message above and uses the one from the model\'s Modelfile instead. Can be useful for models with model files.';
'Disables setting the system message above and uses the one from the models Modelfile instead. Can be useful for models with model files.';
@override
String get settingsDisableMarkdown => 'Disable markdown';
@ -346,7 +416,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsKeepModelLoadedAlways => 'Keep model always loaded';
@override
String get settingsKeepModelLoadedNever => 'Don\'t keep model loaded';
String get settingsKeepModelLoadedNever => 'Dont keep model loaded';
@override
String get settingsKeepModelLoadedFor =>
@ -397,13 +467,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get 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.';
'Do not toggle any of these settings unless you know what you are doing! The given solutions might not work as expected.\nThey should not be considered final or judged as such. Issues might occur.';
@override
String get settingsTemporaryFixesNoFixes => 'No fixes available';
@override
String get settingsVoicePermissionLoading => 'Loading voice permissions ...';
String get settingsVoicePermissionLoading => 'Loading voice permissions...';
@override
String get settingsVoiceTtsNotSupported => 'Text-to-speech not supported';
@ -460,17 +530,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsExportInfo =>
'These options allow you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or back up your chat history';
'These options allow you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or back up your chat history.';
@override
String get settingsExportWarning =>
'Multiple chat histories won\'t be merged! You\'ll lose your current chat history if you import a new one.';
'Multiple chat histories wont be merged! Youll lose your current chat history if you import a new one.';
@override
String get settingsUpdateCheck => 'Check for updates';
@override
String get settingsUpdateChecking => 'Checking for updates ...';
String get settingsUpdateChecking => 'Checking for updates...';
@override
String get settingsUpdateLatest => 'You are on the latest version';
@ -481,7 +551,7 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get settingsUpdateRateLimit => 'Can\'t check, API rate limit exceeded';
String get settingsUpdateRateLimit => 'Cant check, API rate limit exceeded';
@override
String get settingsUpdateIssue => 'An issue occurred';
@ -503,7 +573,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsUpdateDialogCancel => 'Cancel';
@override
String get settingsCheckForUpdates => 'Check for updates on open';
String get settingsCheckForUpdates => 'Check for updates on startup';
@override
String get settingsGithub => 'GitHub';

View File

@ -9,7 +9,12 @@ class AppLocalizationsFa extends AppLocalizations {
AppLocalizationsFa([String locale = 'fa']) : super(locale);
@override
String get appTitle => 'Ollama';
String appTitle(String env, String title) {
return 'Ollama';
}
@override
String get learnMore => 'Learn more';
@override
String get optionNewChat => 'New Chat';
@ -23,6 +28,20 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get optionNoChatFound => 'No chats found';
@override
String optionNoChatFoundSearch(String query) {
return 'No chats found for $query';
}
@override
String get optionSearchChats => 'Search through chats';
@override
String get optionChatDetails => 'Chat details';
@override
String get optionChatDetailsNoChat => 'No chat selected';
@override
String get tipPrefix => 'Tip: ';
@ -54,25 +73,54 @@ class AppLocalizationsFa extends AppLocalizations {
String get uploadImage => 'Upload Image';
@override
String get notAValidImage => 'Not a valid image';
String newChatGreeting1(String user) {
return 'Whats on your mind, $user?';
}
@override
String get imageOnlyConversation => 'Image Only Conversation';
String newChatGreeting2(String user) {
return 'Good to see you, $user.';
}
@override
String get messageInputPlaceholder => 'Message';
String newChatGreeting3(String user) {
return 'What can I do for you, $user?';
}
@override
String get tooltipAttachment => 'Add attachment';
String newChatGreeting4(String user) {
return 'How can I help you today, $user?';
}
@override
String newChatGreeting5(String user) {
return 'What would you like to talk about, $user?';
}
@override
String get newChatGreetingNameFallback => 'user';
@override
String messageInputPlaceholder(String model) {
return 'Message';
}
@override
String get messageInputPlaceholderModelPlaceholder => 'model';
@override
String get tooltipMessageOptions => 'Message options';
@override
String get tooltipSend => 'Send';
@override
String get tooltipSave => 'Save';
String tooltipLetAIThink(String model) {
return 'Let AI think';
}
@override
String get tooltipLetAIThink => 'Let AI think';
String get tooltipAddHostHeaders => 'Add host headers';
@override
String get tooltipReset => 'Reset current chat';
@ -93,7 +141,13 @@ class AppLocalizationsFa extends AppLocalizations {
String get newChatTitle => 'Unnamed Chat';
@override
String get modelDialogAddModel => 'Add';
String get modelDialogTitle => 'Select a model';
@override
String get modelDialogAdd => 'Add';
@override
String get modelDialogRefresh => 'Refresh';
@override
String get modelDialogAddPromptTitle => 'Add new model';
@ -102,6 +156,9 @@ class AppLocalizationsFa extends AppLocalizations {
String get modelDialogAddPromptDescription =>
'This can have either be a normal name (e.g. \'llama3\') or name and tag (e.g. \'llama3:70b\').';
@override
String get modelDialogAddCurrently => 'Currently downloading:';
@override
String get modelDialogAddPromptAlreadyExists => 'Model already exists';
@ -109,44 +166,29 @@ class AppLocalizationsFa extends AppLocalizations {
String get modelDialogAddPromptInvalid => 'Invalid model name';
@override
String get modelDialogAddAllowanceTitle => 'Allow Proxy';
String get modelDialogAddPromptNotFound =>
'Couldnt find your model on the server. Please check the spelling and try again.';
@override
String get modelDialogAddAllowanceDescription =>
'Ollama App must check if the entered model is valid. For that, we normally send a web request to the Ollama model list and check the status code, but because you\'re using the web client, we can\'t do that directly. Instead, the app will send the request to a different api, hosted by JHubi1, to check for us.\nThis is a one-time request and will only be sent when you add a new model.\nYour IP address will be sent with the request and might be stored for up to ten minutes to prevent spamming with potential harmful intentions.\nIf you accept, your selection will be remembered in the future; if not, nothing will be sent and the model won\'t be added.';
String get modelDialogAddPromptNetworkError =>
'Network error while checking model. Please check your connection and try again.';
@override
String get modelDialogAddAllowanceAllow => 'Allow';
String get modelDialogAddPromptCorruptionError =>
'The API returned “EOF”; your server was likely restarted while downloading the model previously.';
@override
String get modelDialogAddAllowanceDeny => 'Deny';
String get modelDialogAddPromptCorruptionErrorSolution =>
'Alternatively, you can run the following command on your Linux host directly:\nIf you see a new error, try running the command with “sudo” placed in front of it.';
@override
String modelDialogAddAssuranceTitle(String model) {
return 'Add $model?';
}
String get modelDialogAddCancelTitle => 'Cancel download?';
@override
String modelDialogAddAssuranceDescription(String model) {
return 'Pressing \'Add\' will download the model \'$model\' directly from the Ollama server to your host.\nThis can take a while depending on your internet connection. The action cannot be canceled.\nIf the app is closed during the download, it\'ll resume if you enter the name into the model dialog again.';
}
String get modelDialogAddCancelConfirm => 'Cancel';
@override
String get modelDialogAddAssuranceAdd => 'Add';
@override
String get modelDialogAddAssuranceCancel => 'Cancel';
@override
String get modelDialogAddDownloadPercentLoading => 'loading progress';
@override
String modelDialogAddDownloadPercent(String percent) {
return 'download at $percent%';
}
@override
String get modelDialogAddDownloadFailed => 'Disconnected, try again';
String get modelDialogAddCancelHide => 'Hide';
@override
String get modelDialogAddDownloadSuccess => 'Download successful';
@ -185,6 +227,13 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get dialogEditMessageTitle => 'Edit message';
@override
String get settingsTitleOverview => 'Overview';
@override
String get settingsDescriptionOverview =>
'General settings regarding your host.';
@override
String get settingsTitleBehavior => 'Behavior';
@ -220,9 +269,6 @@ class AppLocalizationsFa extends AppLocalizations {
String get settingsDescriptionAbout =>
'Check for updates and learn more about Ollama App.';
@override
String get settingsSavedAutomatically => 'Settings are saved automatically';
@override
String get settingsExperimentalAlpha => 'alpha';
@ -259,6 +305,9 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get settingsHost => 'Host';
@override
String get settingsHostMissing => 'No host set';
@override
String get settingsHostValid => 'Valid Host';
@ -277,16 +326,6 @@ class AppLocalizationsFa extends AppLocalizations {
return 'Issue: $_temp0';
}
@override
String get tooltipAddHostHeaders => 'Add host headers';
@override
String get settingsHostHeaderTitle => 'Set host header';
@override
String get settingsHostHeaderInvalid =>
'The entered text isn\'t a valid header JSON object';
@override
String settingsHostInvalidDetailed(String type) {
String _temp0 = intl.Intl.selectLogic(type, {
@ -298,6 +337,22 @@ class AppLocalizationsFa extends AppLocalizations {
return '$_temp0';
}
@override
String get settingsHostHeaderHeaderName => 'Header';
@override
String get settingsHostHeaderHeaderValue => 'Value';
@override
String settingsHostHeaderUnsupported(String header) {
return 'The header “$header” may not be supported.';
}
@override
String settingsHostHeaderDuplicate(String header) {
return 'The header “$header” is already set.';
}
@override
String get settingsSystemMessage => 'System message';

View File

@ -9,7 +9,12 @@ class AppLocalizationsIt extends AppLocalizations {
AppLocalizationsIt([String locale = 'it']) : super(locale);
@override
String get appTitle => 'Ollama';
String appTitle(String env, String title) {
return 'Ollama';
}
@override
String get learnMore => 'Learn more';
@override
String get optionNewChat => 'Nuova Chat';
@ -23,6 +28,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get optionNoChatFound => 'Nessuna Chat trovata';
@override
String optionNoChatFoundSearch(String query) {
return 'No chats found for $query';
}
@override
String get optionSearchChats => 'Search through chats';
@override
String get optionChatDetails => 'Chat details';
@override
String get optionChatDetailsNoChat => 'No chat selected';
@override
String get tipPrefix => 'Suggerimento: ';
@ -55,25 +74,54 @@ class AppLocalizationsIt extends AppLocalizations {
String get uploadImage => 'Carica immagine';
@override
String get notAValidImage => 'Immagine non valida';
String newChatGreeting1(String user) {
return 'Whats on your mind, $user?';
}
@override
String get imageOnlyConversation => 'Conversazione di sole immagini';
String newChatGreeting2(String user) {
return 'Good to see you, $user.';
}
@override
String get messageInputPlaceholder => 'Messaggio';
String newChatGreeting3(String user) {
return 'What can I do for you, $user?';
}
@override
String get tooltipAttachment => 'Aggiungi allegato';
String newChatGreeting4(String user) {
return 'How can I help you today, $user?';
}
@override
String newChatGreeting5(String user) {
return 'What would you like to talk about, $user?';
}
@override
String get newChatGreetingNameFallback => 'user';
@override
String messageInputPlaceholder(String model) {
return 'Messaggio';
}
@override
String get messageInputPlaceholderModelPlaceholder => 'model';
@override
String get tooltipMessageOptions => 'Message options';
@override
String get tooltipSend => 'Invia';
@override
String get tooltipSave => 'Salva';
String tooltipLetAIThink(String model) {
return 'Lasciamo che sia IA a pensare';
}
@override
String get tooltipLetAIThink => 'Lasciamo che sia IA a pensare';
String get tooltipAddHostHeaders => 'Aggiungi host headers';
@override
String get tooltipReset => 'Reimposta la chat corrente';
@ -95,7 +143,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get newChatTitle => 'Chat senza nome';
@override
String get modelDialogAddModel => 'Aggiungi';
String get modelDialogTitle => 'Select a model';
@override
String get modelDialogAdd => 'Add';
@override
String get modelDialogRefresh => 'Refresh';
@override
String get modelDialogAddPromptTitle => 'Aggiungi nuovo modello';
@ -104,6 +158,9 @@ class AppLocalizationsIt extends AppLocalizations {
String get modelDialogAddPromptDescription =>
'Questo può essere un nome normale (ad es. \'llama3\') o nome e tag (ad es. \'llama3:70b\').';
@override
String get modelDialogAddCurrently => 'Currently downloading:';
@override
String get modelDialogAddPromptAlreadyExists => 'Il modello esiste già';
@ -111,44 +168,29 @@ class AppLocalizationsIt extends AppLocalizations {
String get modelDialogAddPromptInvalid => 'Nome del modello non valido';
@override
String get modelDialogAddAllowanceTitle => 'Abilita Proxy';
String get modelDialogAddPromptNotFound =>
'Couldnt find your model on the server. Please check the spelling and try again.';
@override
String get modelDialogAddAllowanceDescription =>
'Ollama App deve controllare se il modello inserito è valido. Per questo, normalmente inviamo una richiesta web alla lista dei modelli Ollama e controlliamo il codice di stato, ma perché stai usando il web client, non possiamo farlo direttamente. Invece, l\'app invierà la richiesta a un altro api, ospitato da JHubi1, per eseguire il controllo.\nQuesta è una richiesta verrà inviata solo quando aggiungi un nuovo modello.\nIl tuo indirizzo IP verrà inviato con la richiesta e potrebbe essere memorizzato per un massimo di dieci minuti per evitare lo spamming con potenziali intenzioni nocive.\nSe accetti, la tua selezione sarà ricordata in futuro; in caso contrario, non verrà inviato nulla e il modello non verrà aggiunto.';
String get modelDialogAddPromptNetworkError =>
'Network error while checking model. Please check your connection and try again.';
@override
String get modelDialogAddAllowanceAllow => 'Consenti';
String get modelDialogAddPromptCorruptionError =>
'The API returned “EOF”; your server was likely restarted while downloading the model previously.';
@override
String get modelDialogAddAllowanceDeny => 'Nega';
String get modelDialogAddPromptCorruptionErrorSolution =>
'Alternatively, you can run the following command on your Linux host directly:\nIf you see a new error, try running the command with “sudo” placed in front of it.';
@override
String modelDialogAddAssuranceTitle(String model) {
return 'Aggiungi $model?';
}
String get modelDialogAddCancelTitle => 'Cancel download?';
@override
String modelDialogAddAssuranceDescription(String model) {
return 'Premendo \'Aggiungi\' scaricherà il modello \'$model\' direttamente dal server Ollama al tuo host.\nQuesto può richiedere un po\' di tempo a seconda della tua connessione internet. L\'azione non può essere annullata.\nSe l\'app è chiusa durante il download, riprenderà se inserisci di nuovo il nome nella finestra del modello.';
}
String get modelDialogAddCancelConfirm => 'Cancel';
@override
String get modelDialogAddAssuranceAdd => 'Aggiungi';
@override
String get modelDialogAddAssuranceCancel => 'Annulla';
@override
String get modelDialogAddDownloadPercentLoading => 'Caricamento in corso';
@override
String modelDialogAddDownloadPercent(String percent) {
return 'scarica al $percent%';
}
@override
String get modelDialogAddDownloadFailed => 'Disconnesso, riprova';
String get modelDialogAddCancelHide => 'Hide';
@override
String get modelDialogAddDownloadSuccess =>
@ -188,6 +230,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get dialogEditMessageTitle => 'Modifica Messaggio';
@override
String get settingsTitleOverview => 'Overview';
@override
String get settingsDescriptionOverview =>
'General settings regarding your host.';
@override
String get settingsTitleBehavior => 'Comportamento';
@ -223,10 +272,6 @@ class AppLocalizationsIt extends AppLocalizations {
String get settingsDescriptionAbout =>
'Controlla gli aggiornamenti e scopri di più su Ollama App.';
@override
String get settingsSavedAutomatically =>
'Le impostazioni vengono salvate automaticamente';
@override
String get settingsExperimentalAlpha => 'alpha';
@ -263,6 +308,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settingsHost => 'Host';
@override
String get settingsHostMissing => 'No host set';
@override
String get settingsHostValid => 'Host valido';
@ -281,16 +329,6 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Problema: $_temp0';
}
@override
String get tooltipAddHostHeaders => 'Aggiungi host headers';
@override
String get settingsHostHeaderTitle => 'Imposta header host';
@override
String get settingsHostHeaderInvalid =>
'Il testo immesso non è un valido oggetto JSON';
@override
String settingsHostInvalidDetailed(String type) {
String _temp0 = intl.Intl.selectLogic(type, {
@ -302,6 +340,22 @@ class AppLocalizationsIt extends AppLocalizations {
return '$_temp0';
}
@override
String get settingsHostHeaderHeaderName => 'Header';
@override
String get settingsHostHeaderHeaderValue => 'Value';
@override
String settingsHostHeaderUnsupported(String header) {
return 'The header “$header” may not be supported.';
}
@override
String settingsHostHeaderDuplicate(String header) {
return 'The header “$header” is already set.';
}
@override
String get settingsSystemMessage => 'Messaggio di sistema';

View File

@ -9,7 +9,12 @@ class AppLocalizationsTr extends AppLocalizations {
AppLocalizationsTr([String locale = 'tr']) : super(locale);
@override
String get appTitle => 'Ollama';
String appTitle(String env, String title) {
return 'Ollama';
}
@override
String get learnMore => 'Learn more';
@override
String get optionNewChat => 'Yeni Sohbet';
@ -23,6 +28,20 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get optionNoChatFound => 'Sohbet bulunamadı';
@override
String optionNoChatFoundSearch(String query) {
return 'No chats found for $query';
}
@override
String get optionSearchChats => 'Search through chats';
@override
String get optionChatDetails => 'Chat details';
@override
String get optionChatDetailsNoChat => 'No chat selected';
@override
String get tipPrefix => 'İpucu: ';
@ -54,25 +73,54 @@ class AppLocalizationsTr extends AppLocalizations {
String get uploadImage => 'Görsel Yükle';
@override
String get notAValidImage => 'Geçerli bir görsel değil';
String newChatGreeting1(String user) {
return 'Whats on your mind, $user?';
}
@override
String get imageOnlyConversation => 'Sadece Görsel İçeren Konuşma';
String newChatGreeting2(String user) {
return 'Good to see you, $user.';
}
@override
String get messageInputPlaceholder => 'Mesaj';
String newChatGreeting3(String user) {
return 'What can I do for you, $user?';
}
@override
String get tooltipAttachment => 'Ek ekle';
String newChatGreeting4(String user) {
return 'How can I help you today, $user?';
}
@override
String newChatGreeting5(String user) {
return 'What would you like to talk about, $user?';
}
@override
String get newChatGreetingNameFallback => 'user';
@override
String messageInputPlaceholder(String model) {
return 'Mesaj';
}
@override
String get messageInputPlaceholderModelPlaceholder => 'model';
@override
String get tooltipMessageOptions => 'Message options';
@override
String get tooltipSend => 'Gönder';
@override
String get tooltipSave => 'Kaydet';
String tooltipLetAIThink(String model) {
return 'AI\'\'nın düşünmesine izin ver';
}
@override
String get tooltipLetAIThink => 'AI\'\'nın düşünmesine izin ver';
String get tooltipAddHostHeaders => 'Ana bilgisayar başlıkları ekle';
@override
String get tooltipReset => 'Mevcut sohbeti sıfırla';
@ -94,7 +142,13 @@ class AppLocalizationsTr extends AppLocalizations {
String get newChatTitle => 'İsimsiz Sohbet';
@override
String get modelDialogAddModel => 'Ekle';
String get modelDialogTitle => 'Select a model';
@override
String get modelDialogAdd => 'Add';
@override
String get modelDialogRefresh => 'Refresh';
@override
String get modelDialogAddPromptTitle => 'Yeni model ekle';
@ -103,6 +157,9 @@ class AppLocalizationsTr extends AppLocalizations {
String get modelDialogAddPromptDescription =>
'Bu normal bir isim (örneğin \'llama3\') ya da isim ve etiket (örneğin \'llama3:70b\') olabilir.';
@override
String get modelDialogAddCurrently => 'Currently downloading:';
@override
String get modelDialogAddPromptAlreadyExists => 'Model zaten mevcut';
@ -110,45 +167,29 @@ class AppLocalizationsTr extends AppLocalizations {
String get modelDialogAddPromptInvalid => 'Geçersiz model adı';
@override
String get modelDialogAddAllowanceTitle => 'Proxy\'e İzin Ver';
String get modelDialogAddPromptNotFound =>
'Couldnt find your model on the server. Please check the spelling and try again.';
@override
String get modelDialogAddAllowanceDescription =>
'Ollama Uygulaması, girilen modelin geçerli olup olmadığını kontrol etmelidir. Bunun için normalde Ollama model listesine bir web isteği gönderir ve durum kodunu kontrol ederiz, ancak siz web istemcisini kullandığınız için bunu doğrudan yapamayız. Bunun yerine, uygulama bizim için kontrol etmek amacıyla JHubi1 tarafından barındırılan farklı bir API\'ye istek gönderecek. \nBu, yalnızca bir kez yapılan bir istektir ve yalnızca yeni bir model eklediğinizde gönderilecektir. \nIP adresiniz istekle birlikte gönderilecek ve olası zararlı niyetlerle spam yapılmasını önlemek amacıyla on dakikaya kadar saklanabilir. \nKabul ederseniz, seçiminiz gelecekte hatırlanacaktır; kabul etmezseniz, hiçbir şey gönderilmeyecek ve model eklenmeyecektir.';
String get modelDialogAddPromptNetworkError =>
'Network error while checking model. Please check your connection and try again.';
@override
String get modelDialogAddAllowanceAllow => 'İzin ver';
String get modelDialogAddPromptCorruptionError =>
'The API returned “EOF”; your server was likely restarted while downloading the model previously.';
@override
String get modelDialogAddAllowanceDeny => 'Reddet';
String get modelDialogAddPromptCorruptionErrorSolution =>
'Alternatively, you can run the following command on your Linux host directly:\nIf you see a new error, try running the command with “sudo” placed in front of it.';
@override
String modelDialogAddAssuranceTitle(String model) {
return '$model Ekle?';
}
String get modelDialogAddCancelTitle => 'Cancel download?';
@override
String modelDialogAddAssuranceDescription(String model) {
return '\'Ekle\' tuşuna basmak, \'$model\' modelini doğrudan Ollama sunucusundan bilgisayarınıza indirecektir. İnternet bağlantınıza bağlı olarak bu işlem biraz zaman alabilir. Bu işlem iptal edilemez. Uygulama indirme sırasında kapatılırsa, model adını tekrar model diyaloguna girerseniz indirme işlemi kaldığı yerden devam eder.';
}
String get modelDialogAddCancelConfirm => 'Cancel';
@override
String get modelDialogAddAssuranceAdd => 'Ekle';
@override
String get modelDialogAddAssuranceCancel => 'İptal';
@override
String get modelDialogAddDownloadPercentLoading => 'yükleme ilerleme durumu';
@override
String modelDialogAddDownloadPercent(String percent) {
return '%$percent oranında indir';
}
@override
String get modelDialogAddDownloadFailed =>
'Bağlantı kesildi, yeniden deneyin';
String get modelDialogAddCancelHide => 'Hide';
@override
String get modelDialogAddDownloadSuccess => 'İndirme tamamlandı';
@ -187,6 +228,13 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get dialogEditMessageTitle => 'Mesajı düzenle';
@override
String get settingsTitleOverview => 'Overview';
@override
String get settingsDescriptionOverview =>
'General settings regarding your host.';
@override
String get settingsTitleBehavior => 'Davranış';
@ -222,9 +270,6 @@ class AppLocalizationsTr extends AppLocalizations {
String get settingsDescriptionAbout =>
'Güncellemeleri kontrol edin ve Ollama Uygulaması hakkında daha fazla bilgi edinin.';
@override
String get settingsSavedAutomatically => 'Ayarlar otomatik olarak kaydedilir';
@override
String get settingsExperimentalAlpha => 'alfa';
@ -261,6 +306,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get settingsHost => 'Ana bilgisayar';
@override
String get settingsHostMissing => 'No host set';
@override
String get settingsHostValid => 'Geçerli Ana Bilgisayar';
@ -279,16 +327,6 @@ class AppLocalizationsTr extends AppLocalizations {
return 'Sorun: $_temp0';
}
@override
String get tooltipAddHostHeaders => 'Ana bilgisayar başlıkları ekle';
@override
String get settingsHostHeaderTitle => 'Ana bilgisayar başlığını ayarla';
@override
String get settingsHostHeaderInvalid =>
'Girilen metin geçerli bir başlık JSON nesnesi değil';
@override
String settingsHostInvalidDetailed(String type) {
String _temp0 = intl.Intl.selectLogic(type, {
@ -299,6 +337,22 @@ class AppLocalizationsTr extends AppLocalizations {
return '$_temp0';
}
@override
String get settingsHostHeaderHeaderName => 'Header';
@override
String get settingsHostHeaderHeaderValue => 'Value';
@override
String settingsHostHeaderUnsupported(String header) {
return 'The header “$header” may not be supported.';
}
@override
String settingsHostHeaderDuplicate(String header) {
return 'The header “$header” is already set.';
}
@override
String get settingsSystemMessage => 'Sistem mesajı';

View File

@ -9,7 +9,12 @@ class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
@override
String get appTitle => 'Ollama';
String appTitle(String env, String title) {
return 'Ollama';
}
@override
String get learnMore => 'Learn more';
@override
String get optionNewChat => '新建聊天';
@ -23,6 +28,20 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get optionNoChatFound => '暂无聊天消息';
@override
String optionNoChatFoundSearch(String query) {
return 'No chats found for $query';
}
@override
String get optionSearchChats => 'Search through chats';
@override
String get optionChatDetails => 'Chat details';
@override
String get optionChatDetailsNoChat => 'No chat selected';
@override
String get tipPrefix => '提示: ';
@ -54,25 +73,54 @@ class AppLocalizationsZh extends AppLocalizations {
String get uploadImage => '上传图像';
@override
String get notAValidImage => '不是一个有效的图片文件.';
String newChatGreeting1(String user) {
return 'Whats on your mind, $user?';
}
@override
String get imageOnlyConversation => '仅图片对话';
String newChatGreeting2(String user) {
return 'Good to see you, $user.';
}
@override
String get messageInputPlaceholder => '消息';
String newChatGreeting3(String user) {
return 'What can I do for you, $user?';
}
@override
String get tooltipAttachment => '添加附件';
String newChatGreeting4(String user) {
return 'How can I help you today, $user?';
}
@override
String newChatGreeting5(String user) {
return 'What would you like to talk about, $user?';
}
@override
String get newChatGreetingNameFallback => 'user';
@override
String messageInputPlaceholder(String model) {
return '消息';
}
@override
String get messageInputPlaceholderModelPlaceholder => 'model';
@override
String get tooltipMessageOptions => 'Message options';
@override
String get tooltipSend => '发送';
@override
String get tooltipSave => '保存';
String tooltipLetAIThink(String model) {
return '让AI思考';
}
@override
String get tooltipLetAIThink => '让AI思考';
String get tooltipAddHostHeaders => '设置主机请求头';
@override
String get tooltipReset => '重置当前聊天';
@ -93,7 +141,13 @@ class AppLocalizationsZh extends AppLocalizations {
String get newChatTitle => '未命名的聊天';
@override
String get modelDialogAddModel => '添加';
String get modelDialogTitle => 'Select a model';
@override
String get modelDialogAdd => 'Add';
@override
String get modelDialogRefresh => 'Refresh';
@override
String get modelDialogAddPromptTitle => '添加新模型';
@ -102,6 +156,9 @@ class AppLocalizationsZh extends AppLocalizations {
String get modelDialogAddPromptDescription =>
'可以是一个普通名称(如:\'llama3\'),也可以是名称加标签(如:\'llama3:70b\')。';
@override
String get modelDialogAddCurrently => 'Currently downloading:';
@override
String get modelDialogAddPromptAlreadyExists => '模型已存在';
@ -109,44 +166,29 @@ class AppLocalizationsZh extends AppLocalizations {
String get modelDialogAddPromptInvalid => '无效的模型名称';
@override
String get modelDialogAddAllowanceTitle => '允许代理服务器';
String get modelDialogAddPromptNotFound =>
'Couldnt find your model on the server. Please check the spelling and try again.';
@override
String get modelDialogAddAllowanceDescription =>
'Ollama 应用程序必须检查输入的模型是否有效。 为此我们通常向Ollama模型列表发送一个网络请求并检查状态。 由于您正在使用 Web 客户端,我们不能直接做到这一点。 因此,应用将把请求发送到另一个由 JHubi 1 部署的api 上进行检查。\n这是一个一次性请求,只有当您添加一个新模型时才会发送。\n您的IP地址将与请求一起发送可能会被存储长达10分钟以防止潜在的有害故障。\n如果您接受,您的选择将在将来被记住;如果不接受,将不会发送任何内容,也不会添加模型。';
String get modelDialogAddPromptNetworkError =>
'Network error while checking model. Please check your connection and try again.';
@override
String get modelDialogAddAllowanceAllow => '允许';
String get modelDialogAddPromptCorruptionError =>
'The API returned “EOF”; your server was likely restarted while downloading the model previously.';
@override
String get modelDialogAddAllowanceDeny => '拒绝';
String get modelDialogAddPromptCorruptionErrorSolution =>
'Alternatively, you can run the following command on your Linux host directly:\nIf you see a new error, try running the command with “sudo” placed in front of it.';
@override
String modelDialogAddAssuranceTitle(String model) {
return '添加$model?';
}
String get modelDialogAddCancelTitle => 'Cancel download?';
@override
String modelDialogAddAssuranceDescription(String model) {
return '按下“添加”将直接从 Ollama 服务器下载模型“$model”到您的主机。\n这可能需要一些时间,取决于您的互联网连接。该操作不能被取消。\n如果在下载过程中关闭应用,当您再次在模型对话框中输入名称,它将恢复之前的下载。';
}
String get modelDialogAddCancelConfirm => 'Cancel';
@override
String get modelDialogAddAssuranceAdd => '添加';
@override
String get modelDialogAddAssuranceCancel => '取消';
@override
String get modelDialogAddDownloadPercentLoading => '加载进度';
@override
String modelDialogAddDownloadPercent(String percent) {
return '已下载 $percent%';
}
@override
String get modelDialogAddDownloadFailed => '连接断开,请重试';
String get modelDialogAddCancelHide => 'Hide';
@override
String get modelDialogAddDownloadSuccess => '下载成功';
@ -185,6 +227,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get dialogEditMessageTitle => '编辑消息';
@override
String get settingsTitleOverview => 'Overview';
@override
String get settingsDescriptionOverview =>
'General settings regarding your host.';
@override
String get settingsTitleBehavior => '行为';
@ -215,9 +264,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settingsDescriptionAbout => '检查更新并了解更多关于Ollama App的信息。';
@override
String get settingsSavedAutomatically => '设置已自动保存';
@override
String get settingsExperimentalAlpha => 'alpha';
@ -251,6 +297,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settingsHost => '主机地址';
@override
String get settingsHostMissing => 'No host set';
@override
String get settingsHostValid => '有效主机地址';
@ -268,15 +317,6 @@ class AppLocalizationsZh extends AppLocalizations {
return '问题:$_temp0';
}
@override
String get tooltipAddHostHeaders => '设置主机请求头';
@override
String get settingsHostHeaderTitle => '设置主机请求头';
@override
String get settingsHostHeaderInvalid => '输入的文本不是有效的标题 JSON 对象';
@override
String settingsHostInvalidDetailed(String type) {
String _temp0 = intl.Intl.selectLogic(type, {
@ -286,6 +326,22 @@ class AppLocalizationsZh extends AppLocalizations {
return '$_temp0';
}
@override
String get settingsHostHeaderHeaderName => 'Header';
@override
String get settingsHostHeaderHeaderValue => 'Value';
@override
String settingsHostHeaderUnsupported(String header) {
return 'The header “$header” may not be supported.';
}
@override
String settingsHostHeaderDuplicate(String header) {
return 'The header “$header” is already set.';
}
@override
String get settingsSystemMessage => '系统信息';

View File

@ -1,81 +1,123 @@
import 'dart:async';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:dartx/dartx.dart';
import 'package:dynamic_system_colors/dynamic_system_colors.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter/material.dart' hide RouteSettings;
// import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:intl/date_symbol_data_local.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pwa_install/pwa_install.dart' as pwa;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences/util/legacy_to_async_migration_util.dart';
import 'package:speech_to_text/speech_to_text.dart';
import 'package:universal_html/html.dart' as html;
import 'package:uuid/uuid.dart';
import 'l10n/gen/app_localizations.dart';
import 'screens/main.dart';
import 'services/clients.dart';
import 'services/responsive.dart';
import 'services/theme.dart';
import 'main.gr.dart';
import 'services/services.dart';
// client configuration
// use host or not, if false dialog is shown
/// forces usage of [fixedHost] as the host
const bool useHost = false;
// host of ollama, must be accessible from the client, without trailing slash
// ! will always be accepted as valid, even if [useHost] is false
/// the host used when [useHost] is true; not validated!
const String fixedHost = "http://example.com:11434";
// use model or not, if false selector is shown
/// forces usage of [fixedModel] as the model
const bool useModel = false;
// model name as string, must be valid ollama model!
const String fixedModel = "gemma3";
// recommended models, shown with a star in model selector
const List<String> recommendedModels = ["gemma3", "llama3.3"];
// allow opening of settings
const bool allowSettings = true;
// allow multiple chats
const bool allowMultipleChats = true;
/// the model used when [useModel] is true; not validated!
const String fixedModel = "gemma3:latest";
// client configuration end
Completer<void> prefsReady = Completer<void>();
SharedPreferencesWithCache? prefs;
String? host;
bool chatAllowed = true;
String hoveredChat = "";
final user = types.User(id: const Uuid().v4());
final assistant = types.User(id: const Uuid().v4());
bool settingsOpen = false;
bool desktopTitleVisible = true;
bool logoVisible = true;
bool menuVisible = false;
bool sendable = false;
bool updateDetectedOnStart = false;
double sidebarIconSize = 1;
SpeechToText speech = SpeechToText();
FlutterTts voice = FlutterTts();
bool voiceSupported = false;
BuildContext? mainContext;
void Function(void Function())? setGlobalState;
void Function(void Function())? setMainAppState;
Color adaptedSurfaceFromColorScheme(ColorScheme cs) => cs.surface;
Color adaptedOnSurfaceFromColorScheme(ColorScheme cs) => cs.surfaceContainerLow;
void main() {
const kDisabledOpacity = 0.38;
const kImageLogo = AssetImage("assets/logo512.png");
const kImageLogoError = AssetImage("assets/logo512error.png");
final uuidRegex = RegExp(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
caseSensitive: false,
);
@AutoRouterConfig()
class AppRouter extends RootStackRouter {
@override
RouteType get defaultRouteType => const RouteType.material();
@override
List<AutoRoute> get routes => [
AutoRoute(
page: RouteMain.page,
path: "/",
children: [
AutoRoute(page: RouteNewChat.page, path: ""),
AutoRoute(page: RouteChat.page, path: "c/:id"),
AutoRoute(
page: RouteSettingsShell.page,
path: "settings",
children: [
AutoRoute(page: RouteSettings.page, path: "", initial: true),
AutoRoute(page: RouteSettingsOverview.page, path: "overview"),
AutoRoute(page: RouteSettingsBehavior.page, path: "behavior"),
AutoRoute(page: RouteSettingsInterface.page, path: "interface"),
AutoRoute(page: RouteSettingsVoice.page, path: "voice"),
AutoRoute(page: RouteSettingsExport.page, path: "export"),
AutoRoute(page: RouteSettingsAbout.page, path: "about"),
],
),
],
),
RedirectRoute(path: "*", redirectTo: "/"),
];
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
usePathUrlStrategy();
await initializeDateFormatting(null, null);
pwa.PWAInstall().setup();
try {
HttpOverrides.global = OllamaHttpOverrides();
} catch (_) {}
SharedPreferences.setPrefix("ollama.");
await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
legacySharedPreferencesInstance: await SharedPreferences.getInstance(),
sharedPreferencesAsyncOptions: const SharedPreferencesOptions(),
migrationCompletedKey: "migrationCompleted",
);
prefs = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
prefsReady.complete();
Preferences.instance;
chatDb = ChatDatabase();
await ChatManager.instance.loadChats();
runApp(const App());
if (LayoutFeature.desktop()) {
@ -96,6 +138,9 @@ class App extends StatefulWidget {
}
class _AppState extends State<App> {
final _appRouter = AppRouter();
Chat? _currentChat;
@override
void initState() {
super.initState();
@ -104,17 +149,6 @@ class _AppState extends State<App> {
// FlutterDisplayMode.setHighRefreshRate().catchError((_) {});
Future<void> load() async {
SharedPreferences.setPrefix("ollama.");
await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
legacySharedPreferencesInstance: await SharedPreferences.getInstance(),
sharedPreferencesAsyncOptions: const SharedPreferencesOptions(),
migrationCompletedKey: "migrationCompleted",
);
prefs = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
prefsReady.complete();
try {
if ((await Permission.bluetoothConnect.isGranted) &&
(await Permission.microphone.isGranted)) {
@ -130,6 +164,34 @@ class _AppState extends State<App> {
}
load();
ChatManager.instance.addListener(onChatUpdate);
WidgetsBinding.instance.addPostFrameCallback((_) {
precacheImage(kImageLogo, context);
precacheImage(kImageLogoError, context);
if (mounted) setState(() {});
});
}
@override
void dispose() {
ChatManager.instance.removeListener(onChatUpdate);
super.dispose();
}
void onUpdate() {
if (mounted) setState(() {});
}
void onChatUpdate() {
if (ChatManager.instance.currentChat != _currentChat) {
_currentChat?.removeListener(onUpdate);
_currentChat = ChatManager.instance.currentChat;
_currentChat?.addListener(onUpdate);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() {});
});
}
}
@override
@ -142,15 +204,38 @@ class _AppState extends State<App> {
dynamicDark: dynamicDark,
),
builder: (themeMode, themeLight, themeDark) {
return MaterialApp(
return MaterialApp.router(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
onGenerateTitle: (context) =>
AppLocalizations.of(context).appTitle,
onGenerateTitle: (context) {
final currentId = _appRouter.currentPath
.split("/")
.where(uuidRegex.hasMatch)
.lastOrNull;
final currentTitle = currentId != null
? ChatManager.instance.chats
.firstOrNullWhere((chat) => chat.id == currentId)
?.title ??
AppLocalizations.of(context).newChatTitle
: null;
return AppLocalizations.of(context).appTitle(switch (kIsWeb
? "web"
: Platform.operatingSystem) {
"android" || "ios" => "short",
"web" when (currentTitle != null) => "integrated",
_ => "other",
}, currentTitle ?? "");
},
theme: themeLight,
darkTheme: themeDark,
themeMode: themeMode,
home: const ScreenMain(),
routerConfig: _appRouter.config(
navigatorObservers: () => [
GlobalNavigationObserver(),
HeroController(),
],
),
);
},
);
@ -158,3 +243,30 @@ class _AppState extends State<App> {
);
}
}
class GlobalNavigationObserver extends NavigatorObserver {
static String? _path;
static String? get path => _path;
static final StreamController<void> _routerStream =
StreamController<void>.broadcast();
static Stream<void> get routerStream => _routerStream.stream;
@override
void didChangeTop(Route<dynamic> topRoute, Route<dynamic>? previousTopRoute) {
super.didChangeTop(topRoute, previousTopRoute);
var node = topRoute.data;
var path = node?.path ?? "";
while (node?.parent != null) {
node = node!.parent;
if (node?.path != null && node!.path.isNotEmpty) {
path = "${node.path}/$path";
}
}
path = "/${path.replaceAll(RegExp(r"(^/+)|(/+$)"), "")}";
_path = path;
_routerStream.add(null);
}
}

224
lib/main.gr.dart Normal file
View File

@ -0,0 +1,224 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// AutoRouterGenerator
// **************************************************************************
// ignore_for_file: type=lint
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i4;
import 'package:flutter/material.dart' as _i5;
import 'package:ollama_app/screens/chat.dart' as _i1;
import 'package:ollama_app/screens/main.dart' as _i2;
import 'package:ollama_app/screens/settings.dart' as _i3;
/// generated route for
/// [_i1.ScreenChat]
class RouteChat extends _i4.PageRouteInfo<RouteChatArgs> {
RouteChat({_i5.Key? key, String? chatId, List<_i4.PageRouteInfo>? children})
: super(
RouteChat.name,
args: RouteChatArgs(key: key, chatId: chatId),
rawPathParams: {'id': chatId},
initialChildren: children,
);
static const String name = 'RouteChat';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
final pathParams = data.inheritedPathParams;
final args = data.argsAs<RouteChatArgs>(
orElse: () => RouteChatArgs(chatId: pathParams.optString('id')),
);
return _i1.ScreenChat(key: args.key, chatId: args.chatId);
},
);
}
class RouteChatArgs {
const RouteChatArgs({this.key, this.chatId});
final _i5.Key? key;
final String? chatId;
@override
String toString() {
return 'RouteChatArgs{key: $key, chatId: $chatId}';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! RouteChatArgs) return false;
return key == other.key && chatId == other.chatId;
}
@override
int get hashCode => key.hashCode ^ chatId.hashCode;
}
/// generated route for
/// [_i2.ScreenMain]
class RouteMain extends _i4.PageRouteInfo<void> {
const RouteMain({List<_i4.PageRouteInfo>? children})
: super(RouteMain.name, initialChildren: children);
static const String name = 'RouteMain';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i2.ScreenMain();
},
);
}
/// generated route for
/// [_i1.ScreenNewChat]
class RouteNewChat extends _i4.PageRouteInfo<void> {
const RouteNewChat({List<_i4.PageRouteInfo>? children})
: super(RouteNewChat.name, initialChildren: children);
static const String name = 'RouteNewChat';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i1.ScreenNewChat();
},
);
}
/// generated route for
/// [_i3.ScreenSettings]
class RouteSettings extends _i4.PageRouteInfo<void> {
const RouteSettings({List<_i4.PageRouteInfo>? children})
: super(RouteSettings.name, initialChildren: children);
static const String name = 'RouteSettings';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.ScreenSettings();
},
);
}
/// generated route for
/// [_i3.ScreenSettingsAbout]
class RouteSettingsAbout extends _i4.PageRouteInfo<void> {
const RouteSettingsAbout({List<_i4.PageRouteInfo>? children})
: super(RouteSettingsAbout.name, initialChildren: children);
static const String name = 'RouteSettingsAbout';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.ScreenSettingsAbout();
},
);
}
/// generated route for
/// [_i3.ScreenSettingsBehavior]
class RouteSettingsBehavior extends _i4.PageRouteInfo<void> {
const RouteSettingsBehavior({List<_i4.PageRouteInfo>? children})
: super(RouteSettingsBehavior.name, initialChildren: children);
static const String name = 'RouteSettingsBehavior';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.ScreenSettingsBehavior();
},
);
}
/// generated route for
/// [_i3.ScreenSettingsExport]
class RouteSettingsExport extends _i4.PageRouteInfo<void> {
const RouteSettingsExport({List<_i4.PageRouteInfo>? children})
: super(RouteSettingsExport.name, initialChildren: children);
static const String name = 'RouteSettingsExport';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.ScreenSettingsExport();
},
);
}
/// generated route for
/// [_i3.ScreenSettingsInterface]
class RouteSettingsInterface extends _i4.PageRouteInfo<void> {
const RouteSettingsInterface({List<_i4.PageRouteInfo>? children})
: super(RouteSettingsInterface.name, initialChildren: children);
static const String name = 'RouteSettingsInterface';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.ScreenSettingsInterface();
},
);
}
/// generated route for
/// [_i3.ScreenSettingsOverview]
class RouteSettingsOverview extends _i4.PageRouteInfo<void> {
const RouteSettingsOverview({List<_i4.PageRouteInfo>? children})
: super(RouteSettingsOverview.name, initialChildren: children);
static const String name = 'RouteSettingsOverview';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.ScreenSettingsOverview();
},
);
}
/// generated route for
/// [_i3.ScreenSettingsShell]
class RouteSettingsShell extends _i4.PageRouteInfo<void> {
const RouteSettingsShell({List<_i4.PageRouteInfo>? children})
: super(RouteSettingsShell.name, initialChildren: children);
static const String name = 'RouteSettingsShell';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.ScreenSettingsShell();
},
);
}
/// generated route for
/// [_i3.ScreenSettingsVoice]
class RouteSettingsVoice extends _i4.PageRouteInfo<void> {
const RouteSettingsVoice({List<_i4.PageRouteInfo>? children})
: super(RouteSettingsVoice.name, initialChildren: children);
static const String name = 'RouteSettingsVoice';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.ScreenSettingsVoice();
},
);
}

520
lib/screens/chat.dart Normal file
View File

@ -0,0 +1,520 @@
import 'dart:convert';
import 'dart:math' as math;
import 'dart:ui';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ollama_dart/ollama_dart.dart' as llama;
import 'package:visibility_detector/visibility_detector.dart';
import '../l10n/gen/app_localizations.dart';
import '../main.dart';
import '../main.gr.dart';
import '../services/services.dart';
import '../widgets/two_state_widget.dart';
@RoutePage()
class ScreenNewChat extends StatefulWidget {
const ScreenNewChat({super.key});
@override
State<ScreenNewChat> createState() => _ScreenNewChatState();
}
class _ScreenNewChatState extends State<ScreenNewChat> {
late int greetingId;
@override
void initState() {
super.initState();
ChatManager.instance.addListener(onUpdate);
shuffleGreeting();
}
@override
void dispose() {
ChatManager.instance.removeListener(onUpdate);
super.dispose();
}
void onUpdate() {
if (ChatManager.instance.currentChatId != null) {
context.router.navigate(
RouteChat(chatId: ChatManager.instance.currentChatId!),
);
}
}
void shuffleGreeting() => greetingId = math.Random().nextInt(5) + 1;
@override
Widget build(BuildContext context) {
final textTheme = TextTheme.of(context);
final breakpoint = Breakpoint.of(context);
final embeddedNavigation = breakpoint.panesRecommended >= 2;
final appLocalizations = AppLocalizations.of(context);
final user =
Preferences.instance.user ??
appLocalizations.newChatGreetingNameFallback;
final greeting = switch (greetingId) {
1 => appLocalizations.newChatGreeting1(user),
2 => appLocalizations.newChatGreeting2(user),
3 => appLocalizations.newChatGreeting3(user),
4 => appLocalizations.newChatGreeting4(user),
_ => appLocalizations.newChatGreeting5(user),
};
return _ChatFrame(
placeSearchBar: !embeddedNavigation,
builder: (context, safeAreaPadding, input) => Padding(
padding: safeAreaPadding,
child: VisibilityDetector(
key: const Key("newChatGreeting"),
onVisibilityChanged: (i) {
if (i.visibleFraction <= 0) {
shuffleGreeting();
if (mounted) setState(() {});
}
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
greeting,
style: textTheme.titleLarge,
textAlign: TextAlign.center,
),
),
if (input != null) ...[const SizedBox(height: 12), input],
],
),
),
),
),
);
}
}
@RoutePage()
class ScreenChat extends StatefulWidget {
final String? chatId;
const ScreenChat({super.key, @PathParam("id") this.chatId});
@override
State<ScreenChat> createState() => _ScreenChatState();
}
class _ScreenChatState extends State<ScreenChat> {
Chat? _chat;
@override
void initState() {
super.initState();
ChatManager.instance.addListener(onUpdate);
checkChatId();
}
@override
void dispose() {
ChatManager.instance.removeListener(onUpdate);
_chat?.removeListener(onUpdate);
super.dispose();
}
void onUpdate() {
if (ChatManager.instance.currentChatId != widget.chatId) {
context.router.navigate(
ChatManager.instance.currentChatId != null
? RouteChat(chatId: ChatManager.instance.currentChatId!)
: const RouteNewChat(),
);
}
if (mounted) setState(() {});
}
@override
void didUpdateWidget(covariant ScreenChat oldWidget) {
super.didUpdateWidget(oldWidget);
checkChatId();
}
void checkChatId() {
if ((widget.chatId?.trim() ?? "").isEmpty ||
!uuidRegex.hasMatch(widget.chatId!) ||
!ChatManager.instance.chats.any((chat) => chat.id == widget.chatId)) {
context.router.navigate(const RouteNewChat());
} else if (widget.chatId != ChatManager.instance.currentChatId) {
ChatManager.instance.currentChatId = widget.chatId!;
}
_chat?.removeListener(onUpdate);
_chat = ChatManager.instance.chats
.where((chat) => chat.id == widget.chatId)
.first;
_chat?.addListener(onUpdate);
}
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
final adaptedOnSurface = adaptedOnSurfaceFromColorScheme(colorScheme);
return _ChatFrame(
builder: (context, safeAreaPadding, input) {
final msg = _chat?.messages
.where((m) => m.sender == .assistant)
.whereType<TextMessage>()
.lastOrNull;
return Text(
"${msg?.thinking ?? "<dumdum>"}\n\n${msg?.content ?? "<none>"}\n\n${msg?.stats?.toJson() ?? "<no stats>"}",
);
},
);
}
}
class _ChatFrame extends StatelessWidget {
final Widget Function(
BuildContext context,
EdgeInsetsGeometry safeAreaPadding,
Widget? input,
)
builder;
final bool placeSearchBar;
const _ChatFrame({required this.builder, this.placeSearchBar = true});
@override
Widget build(BuildContext context) {
final breakpoint = Breakpoint.of(context);
final halfSpacing = breakpoint.spacing / 2;
final input = Hero(
tag: "chatScreenInput",
child: Align(
alignment: AlignmentGeometry.bottomCenter,
child: AnimatedPadding(
duration: ExpressiveCurves.standardSpatial.fastDuration,
curve: ExpressiveCurves.standardEffects.fast,
padding: breakpoint.panesRecommended >= 2
? EdgeInsetsGeometry.only(
left: halfSpacing,
right: halfSpacing,
bottom: halfSpacing,
)
: EdgeInsetsGeometry.only(bottom: breakpoint.spacing),
child: const _ChatInput(),
),
),
);
final child = builder.call(
context,
EdgeInsetsGeometry.zero,
placeSearchBar ? null : input,
);
final stack = Stack(children: [child, if (placeSearchBar) input]);
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 720 - halfSpacing),
child: SizedBox.expand(child: stack),
),
);
}
}
class _ChatInput extends StatefulWidget {
const _ChatInput();
@override
State<_ChatInput> createState() => _ChatInputState();
}
class _ChatInputState extends State<_ChatInput> {
late final TextEditingController _controller = TextEditingController();
late final FocusNode _focusNode = FocusNode(
onKeyEvent: (_, event) {
if (!HardwareKeyboard.instance.isShiftPressed &&
event.logicalKey == LogicalKeyboardKey.enter) {
if (event is KeyDownEvent) _submit();
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
},
);
Chat? _chat;
@override
void initState() {
super.initState();
ModelManager.instance.addListener(onUpdate);
ChatManager.instance.addListener(onChatUpdate);
onChatUpdate();
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
ModelManager.instance.removeListener(onUpdate);
ChatManager.instance.removeListener(onChatUpdate);
super.dispose();
}
void onUpdate() {
if (mounted) setState(() {});
}
void onChatUpdate() {
final chat = ChatManager.instance.currentChat;
if (chat != _chat) {
_chat?.removeListener(onUpdate);
_chat = chat;
_chat?.addListener(onUpdate);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() {});
});
}
Future<void> _submit() async {
final text = _controller.text.trim();
if (text.isEmpty) return;
var chat = ChatManager.instance.currentChat;
final isNewChat = chat == null;
if (isNewChat) {
chat = ChatManager.instance.createChat(context: context);
context.router.navigate(RouteChat(chatId: chat.id));
}
_controller.clear();
await errorGuard(
context,
"N89P97ND",
() async => chat!.send(TextMessage(text, sender: .user)),
errorMessage: errorGuardErrorMessageWithFallbackSingle(
llama.OllamaException,
"Unable to send message",
),
enableReporting: false,
);
if (!mounted || !chat.alive || !isNewChat) return;
await errorGuard(
context,
"A02A05PB",
() async => chat!.generateTitle(context: context, think: true),
errorMessage: errorGuardErrorMessageWithFallbackSingle(
llama.OllamaException,
"Unable to generate chat title",
),
);
}
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
final textTheme = TextTheme.of(context);
final appLocalizations = AppLocalizations.of(context);
final adaptiveOnSurface = adaptedOnSurfaceFromColorScheme(colorScheme);
final enabled = ModelManager.instance.currentModel != null;
final opacity = enabled ? 1.0 : kDisabledOpacity;
Widget enabledWidget({required Widget child}) => IgnorePointer(
ignoring: !enabled,
child: Opacity(opacity: opacity, child: child),
);
Padding leadingTrailingPaddingMap(e) => Padding(
padding: const EdgeInsetsGeometry.symmetric(vertical: 8),
child: e,
);
final leading = [
enabledWidget(
child: IconButton(
onPressed: () {},
icon: const Icon(Icons.add_rounded),
),
),
];
final trailing = [
TwoStateWidgetManaged(
state: _chat?.completer.isCompleted ?? true,
from: IconButton.outlined(
onPressed: () => _chat?.completer.complete(),
icon: const Icon(Icons.stop_rounded),
),
to: enabledWidget(
child: IconButton.filled(
onPressed: () {},
icon: const Icon(Icons.arrow_upward_rounded),
),
),
),
];
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 56.0),
child: Material(
shadowColor: colorScheme.shadow,
elevation: 1,
shape: RoundedRectangleBorder(
side: BorderSide(color: adaptiveOnSurface, width: 1),
borderRadius: const BorderRadius.all(Radius.circular(56 / 2)),
),
child: InkWell(
onTap: enabled
? () {
if (!_focusNode.hasFocus) _focusNode.requestFocus();
}
: null,
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
customBorder: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(56 / 2)),
),
mouseCursor: enabled
? SystemMouseCursors.text
: SystemMouseCursors.basic,
child: Padding(
padding: const EdgeInsetsGeometry.symmetric(horizontal: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
...leading.map(leadingTrailingPaddingMap),
Expanded(
child: Padding(
padding: const EdgeInsetsGeometry.symmetric(
horizontal: 8.0,
vertical: (56 - 24) / 2,
),
child: Semantics(
inputType: SemanticsInputType.text,
child: Opacity(
opacity: opacity,
child: TextField(
autofocus: true,
onTapAlwaysCalled: true,
focusNode: _focusNode,
onSubmitted: (value) => _submit(),
controller: _controller,
style: textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface,
),
enabled: enabled,
minLines: 1,
maxLines: 9,
decoration:
InputDecoration(
hintText: appLocalizations.messageInputPlaceholder(
Model.nameColoredStatic(
name: ModelManager
.instance
.currentModelName,
context: context,
)?.first.text ??
appLocalizations
.messageInputPlaceholderModelPlaceholder,
),
).applyDefaults(
InputDecorationThemeData(
hintStyle: textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
),
enabledBorder: InputBorder.none,
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
),
),
),
),
),
...trailing.map(leadingTrailingPaddingMap),
],
),
),
),
),
);
}
}
class ChatDetails extends StatelessWidget {
final VoidCallback? onClose;
final EdgeInsetsGeometry padding;
const ChatDetails({
super.key,
this.onClose,
this.padding = const EdgeInsetsGeometry.directional(
start: 24,
end: 16,
top: 16,
bottom: 24,
),
});
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
final appLocalizations = AppLocalizations.of(context);
final currentChat = ChatManager.instance.currentChat;
return Padding(
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
appLocalizations.optionChatDetails,
style: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 22,
),
),
),
const SizedBox(width: 12),
IconButton(onPressed: onClose, icon: const Icon(Icons.close)),
],
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsetsGeometry.directional(end: 8),
child: SizedBox(
width: double.infinity,
// TODO: implement proper details view
child: SingleChildScrollView(
child: Text(
currentChat != null
? "${currentChat.title}\n\nstats:\n${JsonEncoder.withIndent(" " * 2).convert(currentChat.messages.whereType<TextMessage>().lastOrNull?.stats?.toJson() ?? {})}"
: appLocalizations.optionChatDetailsNoChat,
),
),
),
),
],
),
);
}
}

View File

@ -1,6 +1,18 @@
import 'package:flutter/material.dart';
import 'package:ollama_dart/ollama_dart.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'dart:async';
import 'dart:ui';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart' hide RouteSettings;
import 'package:ollama_dart/ollama_dart.dart' as llama;
import '../l10n/gen/app_localizations.dart';
import '../main.dart';
import '../main.gr.dart';
import '../services/services.dart';
import '../widgets/model_selector.dart';
import '../widgets/two_state_widget.dart';
import 'chat.dart';
import 'settings.dart';
// import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
// import 'package:flutter_chat_ui/flutter_chat_ui.dart' as chat_ui;
@ -15,12 +27,6 @@ import 'package:scroll_to_index/scroll_to_index.dart';
// import '../l10n/gen/app_localizations.dart';
// import '../main.dart';
import '../l10n/gen/app_localizations.dart';
import '../main.dart';
import '../services/chat.dart';
import '../services/error.dart';
import '../services/markdown.dart';
import '../services/model.dart';
// import '../services/model.dart';
// import '../services/preferences.dart';
// import '../worker/desktop.dart';
@ -1770,6 +1776,7 @@ import '../services/model.dart';
// }
// }
@RoutePage()
class ScreenMain extends StatefulWidget {
const ScreenMain({super.key});
@ -1777,42 +1784,64 @@ class ScreenMain extends StatefulWidget {
State<ScreenMain> createState() => _ScreenMainState();
}
class _ScreenMainState extends State<ScreenMain> {
class _ScreenMainState extends State<ScreenMain>
with SingleTickerProviderStateMixin {
bool _thirdPaneOpen = false;
bool isSettingsRoute = false;
late final bool startedAtSettingsRoute;
PageController? _sidebarController;
final FocusNode _searchBarFocusNode = FocusNode();
final TextEditingController _searchBarController = TextEditingController();
late final AnimationController _searchBarFocusController;
late final CurvedAnimation _searchBarFocusCurved;
final _modelSelectorKey = GlobalKey();
@override
void initState() {
super.initState();
setIsSettingsRoute();
startedAtSettingsRoute = isSettingsRoute;
host = "https://raspimainollama.tunler.net";
ChatManager.instance.addListener(onUpdate);
ModelManager.instance.addListener(onUpdate);
HostManager.instance.addListener(onUpdate);
prefsReady.future.then((_) async {
if (!mounted) return;
if (HostManager.instance.host != null) {
errorGuard(
context,
"Q3L4Z1X6",
() async {
await ModelManager.instance.loadModels();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Models: ${ModelManager.instance.models.length}"),
),
);
},
() async => ModelManager.instance.loadModels(),
errorMessage: errorGuardErrorMessageWithFallbackSingle(
OllamaClientException,
llama.OllamaException,
"Unable to load models",
),
enableReporting: false,
);
}
await ChatManager.instance.loadChats();
_searchBarFocusController = AnimationController(
vsync: this,
duration: ExpressiveCurves.expressiveSpatial.fastDuration,
reverseDuration: ExpressiveCurves.expressiveSpatial.fastDuration,
);
_searchBarFocusCurved = CurvedAnimation(
parent: _searchBarFocusController,
curve: ExpressiveCurves.expressiveSpatial.fast,
reverseCurve: ExpressiveCurves.expressiveSpatial.fast.flipped,
);
_searchBarFocusNode.addListener(() {
if (!mounted) return;
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text("Chats: ${ChatManager.instance.chats.length}")),
// );
if (_searchBarFocusNode.hasFocus) {
_searchBarFocusController.forward();
} else {
_searchBarFocusController.reverse();
}
setState(() {});
});
}
@ -1820,121 +1849,779 @@ class _ScreenMainState extends State<ScreenMain> {
void dispose() {
ChatManager.instance.removeListener(onUpdate);
ModelManager.instance.removeListener(onUpdate);
HostManager.instance.removeListener(onUpdate);
_searchBarFocusNode.dispose();
_searchBarController.dispose();
_searchBarFocusCurved.dispose();
_searchBarFocusController.dispose();
_sidebarController?.dispose();
super.dispose();
}
void onUpdate() {
if (mounted) setState(() {});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() {});
});
}
bool test = false;
double testSlider = 0.5;
void setIsSettingsRoute() {
isSettingsRoute = context.router.currentPath.contains("/settings");
var page = isSettingsRoute ? 1 : 0;
if (_sidebarController != null && _sidebarController!.hasClients) {
if (startedAtSettingsRoute) page = page == 0 ? 1 : 0;
_sidebarController!.animateToPage(
page,
duration: ExpressiveCurves.expressiveEffects.slowDuration,
curve: ExpressiveCurves.expressiveEffects.slow,
);
} else {
_sidebarController = PageController(initialPage: 0);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
...ChatManager.instance.chats.map(
(chat) => ScreenMainChatTile(chat: chat),
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final textTheme = theme.textTheme;
final appLocalizations = AppLocalizations.of(context);
final adaptedSurface = adaptedSurfaceFromColorScheme(colorScheme);
final adaptedOnSurface = adaptedOnSurfaceFromColorScheme(colorScheme);
final breakpoint = Breakpoint.of(context);
final chatsSearched = ChatManager.instance.chats.where((chat) {
final searchTerm = _searchBarController.text.toLowerCase();
return chat.title.toLowerCase().contains(searchTerm) ||
chat.messages.any(
(message) =>
message is TextMessage &&
message.content.toLowerCase().contains(searchTerm),
);
}).toList();
if (breakpoint != Breakpoint.extraLarge) _thirdPaneOpen = false;
final embeddedNavigation = breakpoint.panesRecommended >= 2;
final navigationRail = Transform.translate(
offset: const Offset(0, -8),
child: _ScreenMainNavigationBarWide(
adaptedSurface: adaptedSurface,
isSettingsRoute: isSettingsRoute,
onDestinationSelected: () {
setIsSettingsRoute();
if (mounted) setState(() {});
},
),
);
final children = [
AnimatedSwitcher(
duration: ExpressiveCurves.expressiveSpatial.normalDuration,
switchInCurve: ExpressiveCurves.expressiveSpatial.normal,
switchOutCurve: ExpressiveCurves.expressiveSpatial.normal.flipped,
transitionBuilder: (child, animation) => SizeTransition(
sizeFactor: animation,
axis: Axis.horizontal,
alignment: const AlignmentDirectional(0.75, 0.5),
child: child,
),
child: embeddedNavigation
? navigationRail
: SizedBox(width: breakpoint.spacing),
),
AnimatedContainer(
duration: ExpressiveCurves.standardSpatial.fastDuration,
curve: ExpressiveCurves.standardEffects.fast,
padding: breakpoint.panesRecommended >= 2
? EdgeInsetsDirectional.only(end: breakpoint.spacing)
: EdgeInsetsDirectional.zero,
child: AnimatedSwitcher(
duration: ExpressiveCurves.expressiveSpatial.normalDuration,
switchInCurve: ExpressiveCurves.expressiveSpatial.normal,
switchOutCurve: ExpressiveCurves.expressiveSpatial.normal.flipped,
transitionBuilder: (child, animation) => SizeTransition(
sizeFactor: animation,
axis: Axis.horizontal,
alignment: const AlignmentDirectional(0.25, 0.5),
child: child,
),
ListTile(
title: const Text("Add chat"),
onTap: () async {
if (ModelManager.instance.models.isEmpty) return;
var chat = ChatManager.instance.createChat(
context: context,
model: ModelManager.instance.models.first,
);
child: breakpoint.panesRecommended >= 2
? Builder(
builder: (context) {
final children = [
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(56 * (2 / 3)),
),
child: chatsSearched.isEmpty
? Padding(
padding: EdgeInsetsGeometry.only(
top: 64,
bottom: breakpoint.spacing,
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
_searchBarController.text.isEmpty
? appLocalizations.optionNoChatFound
: appLocalizations
.optionNoChatFoundSearch(
_searchBarController.text,
),
style: textTheme.labelLarge!.copyWith(
color: colorScheme.outline,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
)
: ListView.builder(
padding: EdgeInsetsGeometry.only(
top: 64,
bottom: breakpoint.spacing,
),
itemBuilder: (context, index) {
final chat = chatsSearched.elementAtOrNull(
index,
);
if (chat == null) return null;
return ListTile(
title: Text(chat.title),
trailing: IconButton(
onPressed: () =>
ChatManager.instance.deleteChat(chat),
icon: const Icon(Icons.delete_outline),
),
);
},
),
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
adaptedSurface,
adaptedSurface.withAlpha(0),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.25, 1.0],
),
),
child: const SizedBox(
width: double.infinity,
height: 56,
),
),
AnimatedBuilder(
animation: _searchBarFocusCurved,
builder: (context, child) => LayoutBuilder(
builder: (context, constraints) {
final padding = lerpDouble(
12.0,
0.0,
_searchBarFocusCurved.value,
)!;
return OverflowBox(
maxWidth: double.infinity,
alignment: AlignmentGeometry.topCenter,
child: SizedBox(
width: constraints.maxWidth - padding * 2,
child: child,
),
);
},
),
child: SearchBar(
hintText: appLocalizations.optionSearchChats,
leading: TwoStateWidget(
animation: _searchBarFocusCurved,
from: IconButton(
onPressed: null,
disabledColor: IconTheme.of(context).color,
icon: const Icon(Icons.search),
),
to: IconButton(
onPressed: () {
_searchBarFocusNode.unfocus();
onUpdate();
},
icon: const Icon(Icons.chevron_left),
),
),
elevation: WidgetStateProperty.all(0),
focusNode: _searchBarFocusNode,
controller: _searchBarController,
onChanged: (value) => onUpdate(),
),
),
];
await errorGuard(
context,
"M49WC9CW",
() async {
var msg = TextMessage(
"Write a long demo message for all GitHub Flavored Markdown features.",
sender: MessageSender.user,
);
return chat.send(msg);
},
errorMessage: errorGuardErrorMessageWithFallbackSingle(
OllamaClientException,
"Unable to send message",
),
enableReporting: false,
);
var pageViewChildren = [
Stack(children: children),
const SettingsOptions(embeddedNavigation: true),
];
if (startedAtSettingsRoute) {
pageViewChildren = pageViewChildren.reversed.toList();
}
if (!context.mounted || !chat.alive) return;
await errorGuard(
context,
"W97BM0DJ",
() async => chat.generateTitle(context: context),
errorMessage: errorGuardErrorMessageWithFallbackSingle(
OllamaClientException,
"Unable to generate chat title",
),
);
return SizedBox(
width: breakpoint.panesFixedWidth,
child: PageView(
controller: _sidebarController,
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
clipBehavior: Clip.antiAliasWithSaveLayer,
children: pageViewChildren,
),
);
},
)
: null,
),
),
Expanded(
child: Builder(
builder: (context) {
const radius = BorderRadius.vertical(top: Radius.circular(12));
final border = BorderSide(
width: 2,
color: colorScheme.surfaceContainerHigh,
);
return AnimatedContainer(
duration: ExpressiveCurves.standardSpatial.fastDuration,
curve: ExpressiveCurves.standardEffects.fast,
decoration: breakpoint.panesRecommended >= 2
? BoxDecoration(
borderRadius: radius,
border: Border(top: border, left: border, right: border),
)
: const BoxDecoration(),
child: const ClipRRect(borderRadius: radius, child: AutoRouter()),
);
},
),
),
AnimatedContainer(
duration: ExpressiveCurves.standardSpatial.fastDuration,
curve: ExpressiveCurves.standardEffects.fast,
padding: breakpoint.panes.contains(3) && _thirdPaneOpen
? EdgeInsetsDirectional.only(start: breakpoint.spacing)
: EdgeInsetsDirectional.zero,
child: Card.filled(
margin: EdgeInsets.zero,
color: adaptedOnSurface,
child: AnimatedSwitcher(
duration: ExpressiveCurves.expressiveSpatial.normalDuration,
switchInCurve: ExpressiveCurves.expressiveSpatial.normal,
switchOutCurve: ExpressiveCurves.expressiveSpatial.normal.flipped,
transitionBuilder: (child, animation) => SizeTransition(
sizeFactor: animation,
axis: Axis.horizontal,
alignment: const AlignmentDirectional(0.25, 0.5),
child: child,
),
child: breakpoint.panes.contains(3) && _thirdPaneOpen
? SizedBox(
width: breakpoint.panesThirdFixedWidth,
child: ChatDetails(
onClose: () {
_thirdPaneOpen = false;
if (mounted) setState(() {});
},
),
)
: null,
),
),
),
SizedBox(width: breakpoint.spacing),
];
final modelSelectorAnimationDuration =
ExpressiveCurves.standardEffects.fastDuration;
final modelSelectorAnimationCurve = ExpressiveCurves.standardEffects.fast;
final modelHostSelected = HostManager.instance.host != null;
final modelSelector = GestureDetector(
key: _modelSelectorKey,
onTap: !modelHostSelected
? null
: () {
showModelSelector(context: context, anchorKey: _modelSelectorKey);
},
child: Opacity(
opacity: !modelHostSelected ? kDisabledOpacity : 1.0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text.rich(
TextSpan(
children:
Model.nameColoredStatic(
name: ModelManager.instance.currentModelName,
context: context,
) ??
[TextSpan(text: appLocalizations.noSelectedModel)],
),
style: textTheme.titleMedium!.copyWith(
fontFamily: "GoogleSansCode",
),
),
SizedBox(width: breakpoint.spacing / 8),
const Icon(Icons.keyboard_arrow_down),
],
),
),
);
final modelSelectorShowSubtitle = breakpoint.panesRecommended >= 2;
final modelSelectorOffset = modelSelectorShowSubtitle
? 80.0 +
(breakpoint.panesFixedWidth ?? 0) +
breakpoint.spacing -
NavigationToolbar.kMiddleSpacing -
kToolbarHeight
: 0.0;
return Scaffold(
backgroundColor: adaptedSurface,
endDrawer: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: DecoratedBox(
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLow,
borderRadius: const BorderRadiusDirectional.only(
topStart: Radius.circular(16),
bottomStart: Radius.circular(16),
),
),
child: ChatDetails(
onClose: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
),
],
),
),
appBar: AppBar(
clipBehavior: Clip.none,
leading: TwoStateWidgetManaged(
state: breakpoint.panesRecommended >= 2,
dimension: kToolbarHeight,
from: const DrawerBackButton(),
to: Transform.translate(
offset: const Offset(12, 2),
child: const Padding(
padding: EdgeInsetsGeometry.only(top: 8),
child: ImageIcon(kImageLogo),
),
),
),
actions: [
TwoStateWidgetManaged(
state: isSettingsRoute,
from: SizedBox.square(
dimension: kToolbarHeight,
child: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.info_outline),
tooltip: appLocalizations.optionChatDetails,
onPressed: () {
if (breakpoint.panes.contains(3)) {
_thirdPaneOpen = !_thirdPaneOpen;
if (mounted) setState(() {});
} else {
Scaffold.of(context).openEndDrawer();
}
},
),
),
),
),
],
title: SizedBox(
width: double.infinity,
height: theme.appBarTheme.toolbarHeight ?? kToolbarHeight,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
AnimatedPositionedDirectional(
key: const ValueKey("modelSelectorSubtitle"),
duration: modelSelectorAnimationDuration,
curve: modelSelectorAnimationCurve,
start: modelSelectorShowSubtitle ? modelSelectorOffset : 64,
child: AnimatedSlide(
offset: Offset(
0,
modelHostSelected || !modelSelectorShowSubtitle ? 0 : 0.7,
),
duration: modelSelectorAnimationDuration,
curve: modelSelectorAnimationCurve,
child: IgnorePointer(
child: AnimatedOpacity(
opacity: modelHostSelected || !modelSelectorShowSubtitle
? 0
: 1,
duration: modelSelectorAnimationDuration,
curve: modelSelectorAnimationCurve,
child: Text(
appLocalizations.noHostSelected,
style: textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
),
),
),
),
AnimatedPositionedDirectional(
key: const ValueKey("modelSelectorButton"),
duration: modelSelectorAnimationDuration,
curve: modelSelectorAnimationCurve,
start: modelSelectorShowSubtitle ? modelSelectorOffset : null,
child: AnimatedSlide(
offset: Offset(
0,
modelHostSelected || !modelSelectorShowSubtitle ? 0 : -0.2,
),
duration: modelSelectorAnimationDuration,
curve: modelSelectorAnimationCurve,
child: modelSelector,
),
),
],
),
),
centerTitle: breakpoint.panesRecommended < 2,
backgroundColor: adaptedSurface,
scrolledUnderElevation: 0,
),
body: Builder(
builder: (context) {
if (breakpoint == Breakpoint.extraLarge &&
Scaffold.of(context).isEndDrawerOpen) {
_thirdPaneOpen = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {});
Navigator.of(context).pop();
});
}
return Row(mainAxisSize: MainAxisSize.max, children: children);
},
),
);
// return Scaffold(
// body: ListView(
// children: [
// Padding(
// padding: const EdgeInsetsGeometry.only(
// top: 16,
// left: 16,
// right: 16,
// bottom: 8,
// ),
// child: Card.filled(
// margin: EdgeInsets.zero,
// clipBehavior: Clip.antiAlias,
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// ListTile(
// title: Text(
// HostManager.instance.host?.toString() ?? "<no host?>",
// ),
// onTap: loadingModels
// ? null
// : () async {
// loadingModels = true;
// HostManager.instance.host = Uri.parse(
// HostManager.instance.host.toString() ==
// "https://ollama.tunler.net"
// ? "https://raspimainollama.tunler.net"
// : "https://ollama.tunler.net",
// );
// setState(() {});
// await loadModels();
// },
// dense: true,
// ),
// const Divider(height: 1),
// AnimatedSize(
// duration: Durations.medium1,
// curve: Curves.easeInOutCubic,
// child: ListTile(
// title: ModelManager.instance.models.isEmpty
// ? const Text("No models available")
// : Text.rich(
// TextSpan(
// children: [
// const TextSpan(
// text: "Models: ",
// style: TextStyle(
// fontStyle: FontStyle.italic,
// ),
// ),
// for (
// var i = 0;
// i <
// ModelManager.instance.models.length *
// 2 -
// 1;
// i++
// )
// if (i.isEven)
// TextSpan(
// text: ModelManager.instance.models
// .elementAt(i ~/ 2)
// .name,
// style: i ~/ 2 == modelIndex
// ? const TextStyle(
// fontWeight: FontWeight.bold,
// )
// : null,
// )
// else
// const TextSpan(text: ", "),
// ],
// ),
// ),
// onTap:
// loadingModels || ModelManager.instance.models.isEmpty
// ? null
// : () async {
// final currentIndex = modelIndex;
// if (currentIndex == null) return;
// final nextIndex =
// (currentIndex + 1) %
// ModelManager.instance.models.length;
// modelIndex = nextIndex;
// ModelManager.instance.currentModel = ModelManager
// .instance
// .models
// .elementAt(nextIndex);
// setState(() {});
// },
// dense: true,
// ),
// ),
// ],
// ),
// ),
// ),
// ...ChatManager.instance.chats.map(
// (chat) => ScreenMainChatTile(chat: chat, chatsLoaded: chatsLoaded),
// ),
// MenuAnchor(
// animated: true,
// menuChildren:
// <(String, String)>[
// (
// "GFM demo",
// "Write a long demo message for all GitHub Flavored Markdown features.",
// ),
// (
// "Lorem Ipsum",
// "Write a middle long lorem ipsum stand in body text.",
// ),
// (
// "Best emoji",
// "Think very hard about the objectively best emoji.",
// ),
// (
// "Math test",
// "Calculate the normalized Shannon entropy for the following values: [0.1, 0.1, 0.2, 0.3, 0.3]",
// ),
// ].map((prompt) {
// return MenuItemButton(
// child: Text(
// prompt.$1,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// onPressed: () async {
// final chat = ChatManager.instance.createChat(
// context: context,
// system:
// "Write using GitHub Flavored Markdown messages. Your messages support all GitHub Flavored Markdown features, including tables, task lists, strikethrough, alert boxes, emojis and autolinks. You must format LaTeX math using the dollar sign syntax (`\$...\$`); bracket style does not work! Speak german to the user, even if he doesn't write you in that language!",
// );
// await errorGuard(
// context,
// "M49WC9CW",
// () async {
// final msg = TextMessage(
// prompt.$2,
// sender: MessageSender.user,
// );
// return chat.send(msg);
// },
// errorMessage: errorGuardErrorMessageWithFallbackSingle(
// llama.OllamaException,
// "Unable to send message",
// ),
// enableReporting: false,
// );
// if (!context.mounted || !chat.alive) return;
// await errorGuard(
// context,
// "W97BM0DJ",
// () async => chat.generateTitle(context: context),
// errorMessage: errorGuardErrorMessageWithFallbackSingle(
// llama.OllamaException,
// "Unable to generate chat title",
// ),
// );
// },
// );
// }).toList(),
// builder: (_, controller, _) => ListTile(
// leading: const Icon(Icons.add_rounded),
// title: const Text("Add chat"),
// onTap: loadingModels || ModelManager.instance.currentModel == null
// ? null
// : controller.open,
// ),
// ),
// ],
// ),
// );
}
}
class _ScreenMainNavigationBarWide extends StatelessWidget {
final Color adaptedSurface;
final bool isSettingsRoute;
final void Function()? onDestinationSelected;
const _ScreenMainNavigationBarWide({
required this.adaptedSurface,
required this.isSettingsRoute,
this.onDestinationSelected,
});
@override
Widget build(BuildContext context) {
final breakpoint = Breakpoint.of(context);
final isEmbeddedNavigation = breakpoint.panesRecommended >= 2;
return NavigationRail(
leadingAtTop: true,
trailingAtBottom: true,
backgroundColor: adaptedSurface,
leading: FloatingActionButton(
onPressed: () async {
ChatManager.instance.currentChat = null;
if (isSettingsRoute) {
await context.router.navigate(const RouteNewChat());
onDestinationSelected?.call();
}
},
elevation: 0,
heroTag: null,
child: const Icon(Icons.add),
),
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.near_me),
label: Text("Chats"),
),
NavigationRailDestination(
icon: Icon(Icons.settings),
label: Text("Settings"),
),
],
selectedIndex: isSettingsRoute ? 1 : 0,
onDestinationSelected: (value) async {
switch (value) {
case 0 when isSettingsRoute:
await context.router.navigate(
ChatManager.instance.currentChatId != null
? RouteChat(chatId: ChatManager.instance.currentChatId)
: const RouteNewChat(),
);
onDestinationSelected?.call();
case 1 when !isSettingsRoute:
await context.router.navigate(
isEmbeddedNavigation
? const RouteSettingsOverview()
: const RouteSettings(),
);
onDestinationSelected?.call();
default:
}
},
);
}
}
class ScreenMainChatTile extends StatefulWidget {
final Chat chat;
const ScreenMainChatTile({super.key, required this.chat});
class DrawerBackButton extends StatefulWidget {
const DrawerBackButton({super.key});
@override
State<ScreenMainChatTile> createState() => _ScreenMainChatTileState();
State<DrawerBackButton> createState() => _DrawerBackButtonState();
}
class _ScreenMainChatTileState extends State<ScreenMainChatTile> {
class _DrawerBackButtonState extends State<DrawerBackButton> {
StreamSubscription<void>? _routerStream;
String? path;
@override
void initState() {
super.initState();
widget.chat.addListener(onChange);
path = GlobalNavigationObserver.path;
_routerStream = GlobalNavigationObserver.routerStream.listen(onUpdate);
}
@override
void dispose() {
widget.chat.removeListener(onChange);
_routerStream?.cancel();
super.dispose();
}
void onChange() {
if (mounted) setState(() {});
void onUpdate(_) {
if (GlobalNavigationObserver.path != null) {
path = GlobalNavigationObserver.path;
if (mounted) setState(() {});
}
}
@override
Widget build(BuildContext context) {
return ListTile(
title: ChatText(
widget.chat.title.emptyOn(AppLocalizations.of(context).newChatTitle),
placeholder: Text(AppLocalizations.of(context).newChatTitle),
final isSettingsRoute = path?.contains("/settings") ?? false;
final state = isSettingsRoute && path != "/settings";
return TwoStateWidgetManaged(
state: state,
from: const SizedBox.expand(child: DrawerButton()),
to: SizedBox.expand(
child: BackButton(
onPressed: () async {
if (!state) return;
if (context.router.canPop()) {
context.router.popTop();
} else {
await context.router.navigate(const RouteSettings());
}
if (mounted) setState(() {});
},
),
),
subtitle:
(widget.chat.messages.isEmpty ||
widget.chat.messages.last.runtimeType != TextMessage)
? null
// : ChatText(
// (widget.chat.messages.last as TextMessage).content,
// placeholder: const Text("<incoming>"),
// ),
: (widget.chat.messages.last as TextMessage).content.isEmpty
? const LinearProgressIndicator()
: Text.rich(
Markdown(
(widget.chat.messages.last as TextMessage).content,
).toTextSpan(context),
),
onTap: () => ChatManager.instance.deleteChat(widget.chat),
);
}
}
extension on String {
String emptyOn(String other) => (this == other) ? "" : this;
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ class _ScreenSettingsVoiceState extends State<ScreenSettingsVoice> {
bool dialogMustLoad = true;
Future<void> load() async {
var tmp = await speech.locales();
final tmp = await speech.locales();
languageOptionIds = tmp.map((e) => e.localeId);
languageOptions = tmp.map((e) => e.name);
@ -43,8 +43,9 @@ class _ScreenSettingsVoiceState extends State<ScreenSettingsVoice> {
voiceLanguageOptions = await voice.getLanguages as List;
for (var i = 0; i < languageOptionIds.length; i++) {
if (voiceLanguageOptions
.contains(languageOptionIds.elementAt(i).replaceAll("_", "-"))) {
if (voiceLanguageOptions.contains(
languageOptionIds.elementAt(i).replaceAll("_", "-"),
)) {
voiceLanguageOptionsAvailable.add(languageOptionIds.elementAt(i));
}
}
@ -63,298 +64,381 @@ class _ScreenSettingsVoiceState extends State<ScreenSettingsVoice> {
return WindowBorder(
color: Theme.of(context).colorScheme.surface,
child: Scaffold(
appBar: AppBar(
title: Badge(
label: Text(
AppLocalizations.of(context).settingsExperimentalBeta),
offset: const Offset(20, -4),
backgroundColor: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
child:
Text(AppLocalizations.of(context).settingsTitleVoice))),
body: Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Column(children: [
Expanded(
child: ListView(children: [
appBar: AppBar(
title: Badge(
label: Text(AppLocalizations.of(context).settingsExperimentalBeta),
offset: const Offset(20, -4),
backgroundColor: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
child: Text(AppLocalizations.of(context).settingsTitleVoice),
),
),
body: Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Column(
children: [
Expanded(
child: ListView(
children: [
(permissionLoading ||
(permissionBluetooth &&
permissionRecord &&
voiceSupported &&
voiceLanguageOptionsAvailable.contains(
prefs!.getString("voiceLanguage") ??
"en_US")))
prefs!.getString("voiceLanguage") ?? "en_US",
)))
? const SizedBox.shrink()
: button(
permissionLoading
? AppLocalizations.of(context)
.settingsVoicePermissionLoading
? AppLocalizations.of(
context,
).settingsVoicePermissionLoading
: (!voiceLanguageOptionsAvailable.contains(
prefs!.getString("voiceLanguage") ??
"en_US") &&
(prefs!.getBool("voiceModeEnabled") ??
false))
? AppLocalizations.of(context)
.settingsVoiceTtsNotSupported
: !(permissionBluetooth && permissionRecord)
? AppLocalizations.of(context)
.settingsVoicePermissionNot
: AppLocalizations.of(context)
.settingsVoiceNotSupported,
Icons.info_rounded, () {
selectionHaptic();
if (permissionLoading) return;
if (!(permissionBluetooth && permissionRecord)) {
Future<void> load() async {
try {
if (await Permission
.bluetooth.isPermanentlyDenied ||
await Permission
.microphone.isPermanentlyDenied) {
await openAppSettings();
}
permissionRecord = await Permission.microphone
.request()
.isGranted;
permissionBluetooth = await Permission
.bluetoothConnect
.request()
.isGranted;
permissionLoading = false;
if (permissionBluetooth && permissionRecord) {
voiceSupported = await speech.initialize();
}
setState(() {});
} catch (_) {
permissionLoading = false;
prefs!.getString("voiceLanguage") ??
"en_US",
) &&
(prefs!.getBool("voiceModeEnabled") ??
false))
? AppLocalizations.of(
context,
).settingsVoiceTtsNotSupported
: !(permissionBluetooth && permissionRecord)
? AppLocalizations.of(
context,
).settingsVoicePermissionNot
: AppLocalizations.of(
context,
).settingsVoiceNotSupported,
Icons.info_rounded,
() {
selectionHaptic();
if (permissionLoading) return;
if (!(permissionBluetooth && permissionRecord)) {
Future<void> load() async {
try {
if (await Permission
.bluetooth
.isPermanentlyDenied ||
await Permission
.microphone
.isPermanentlyDenied) {
await openAppSettings();
}
permissionRecord = await Permission
.microphone
.request()
.isGranted;
permissionBluetooth = await Permission
.bluetoothConnect
.request()
.isGranted;
permissionLoading = false;
if (permissionBluetooth &&
permissionRecord) {
voiceSupported = await speech
.initialize();
}
setState(() {});
} catch (_) {}
} catch (_) {
permissionLoading = false;
try {
setState(() {});
} catch (_) {}
}
}
load();
} else if (!voiceLanguageOptions.contains(
prefs!.getString("voiceLanguage") ?? "en_US",
)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
).settingsVoiceTtsNotSupportedDescription,
),
showCloseIcon: true,
),
);
}
},
),
toggle(
context,
AppLocalizations.of(context).settingsVoiceEnable,
prefs!.getBool("voiceModeEnabled") ?? false,
(value) {
selectionHaptic();
prefs!.setBool("voiceModeEnabled", value);
setState(() {});
},
disabled: !voiceSupported,
),
button(
((prefs!.getString("voiceLanguage") ?? "") == "" ||
languageOptions.isEmpty)
? AppLocalizations.of(context).settingsVoiceNoLanguage
: () {
for (
var i = 0;
i < languageOptionIds.length;
i++
) {
if (languageOptionIds.elementAt(i) ==
prefs!.getString("voiceLanguage")) {
return languageOptions.elementAt(i);
}
}
return "";
}(),
Icons.language_rounded,
() {
var usedIndex = -1;
Function? setModalState;
selectionHaptic();
showModalBottomSheet(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setLocalState) {
setModalState = setLocalState;
Future<void> loadSelected() async {
await load();
if ((prefs!.getString("voiceLanguage") ?? "") !=
"") {
for (
var i = 0;
i < languageOptionIds.length;
i++
) {
if (languageOptionIds.elementAt(i) ==
(prefs!.getString("voiceLanguage") ??
"")) {
setModalState!(() {
usedIndex = i;
});
break;
}
}
}
}
load();
} else if (!voiceLanguageOptions.contains(
prefs!.getString("voiceLanguage") ?? "en_US")) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context)
.settingsVoiceTtsNotSupportedDescription),
showCloseIcon: true));
}
}),
toggle(
context,
AppLocalizations.of(context).settingsVoiceEnable,
prefs!.getBool("voiceModeEnabled") ?? false, (value) {
selectionHaptic();
prefs!.setBool("voiceModeEnabled", value);
setState(() {});
}, disabled: !voiceSupported),
button(
((prefs!.getString("voiceLanguage") ?? "") == "" ||
languageOptions.isEmpty)
? AppLocalizations.of(context)
.settingsVoiceNoLanguage
: () {
for (var i = 0;
i < languageOptionIds.length;
i++) {
if (languageOptionIds.elementAt(i) ==
prefs!.getString("voiceLanguage")) {
return languageOptions.elementAt(i);
}
}
return "";
}(),
Icons.language_rounded, () {
var usedIndex = -1;
Function? setModalState;
if (dialogMustLoad) {
loadSelected();
dialogMustLoad = false;
}
selectionHaptic();
showModalBottomSheet(
context: context,
builder:
(context) => StatefulBuilder(
builder: (context, setLocalState) {
setModalState = setLocalState;
Future<void> loadSelected() async {
await load();
if ((prefs!.getString("voiceLanguage") ??
"") !=
"") {
for (var i = 0;
i < languageOptionIds.length;
i++) {
if (languageOptionIds.elementAt(i) ==
(prefs!.getString(
"voiceLanguage") ??
"")) {
setModalState!(() {
usedIndex = i;
});
break;
}
}
}
}
if (dialogMustLoad) {
loadSelected();
dialogMustLoad = false;
}
return PopScope(
onPopInvokedWithResult:
(didPop, result) {
if (usedIndex == -1) return;
prefs!.setString(
"voiceLanguage",
languageOptionIds
.elementAt(usedIndex));
setState(() {
dialogMustLoad = true;
});
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: ((Platform
.isWindows ||
Platform
.isLinux ||
Platform
.isMacOS) &&
MediaQuery.of(
context)
.size
.width >=
1000)
? 300
: double.infinity,
constraints: BoxConstraints(
maxHeight:
MediaQuery.of(
context)
.size
.height *
0.4),
child:
SingleChildScrollView(
scrollDirection:
Axis.vertical,
child: Wrap(
spacing: 5.0,
alignment:
WrapAlignment
.center,
children: List<
Widget>.generate(
languageOptionIds
.length,
(int index) {
return ChoiceChip(
label: Text(
languageOptions
.elementAt(index)),
selected:
usedIndex ==
index,
avatar: (usedIndex ==
index)
? null
: (voiceLanguageOptionsAvailable.contains(languageOptionIds.elementAt(index)))
? const Icon(Icons.spatial_tracking_rounded)
: null,
checkmarkColor: (usedIndex == index &&
!(prefs?.getBool("useDeviceTheme") ??
false))
? ((MediaQuery.of(context).platformBrightness == Brightness.light)
? themeLight().colorScheme.secondary
: themeDark().colorScheme.secondary)
: null,
labelStyle: (usedIndex == index &&
!(prefs?.getBool("useDeviceTheme") ??
false))
? TextStyle(
color: (MediaQuery.of(context).platformBrightness == Brightness.light) ? themeLight().colorScheme.secondary : themeDark().colorScheme.secondary)
: null,
selectedColor: (prefs?.getBool("useDeviceTheme") ??
false)
? null
: (MediaQuery.of(context).platformBrightness == Brightness.light)
? themeLight().colorScheme.primary
: themeDark().colorScheme.primary,
onSelected:
(bool
selected) {
selectionHaptic();
setLocalState(
() {
usedIndex = selected
? index
: -1;
});
},
);
},
).toList(),
)))
])));
}));
},
disabled: !voiceSupported ||
!(prefs!.getBool("voiceModeEnabled") ?? false)),
return PopScope(
onPopInvokedWithResult: (didPop, result) {
if (usedIndex == -1) return;
prefs!.setString(
"voiceLanguage",
languageOptionIds.elementAt(usedIndex),
);
setState(() {
dialogMustLoad = true;
});
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width:
((Platform.isWindows ||
Platform.isLinux ||
Platform.isMacOS) &&
MediaQuery.of(
context,
).size.width >=
1000)
? 300
: double.infinity,
constraints: BoxConstraints(
maxHeight:
MediaQuery.of(
context,
).size.height *
0.4,
),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Wrap(
spacing: 5.0,
alignment: WrapAlignment.center,
children: List<Widget>.generate(
languageOptionIds.length,
(int index) {
return ChoiceChip(
label: Text(
languageOptions.elementAt(
index,
),
),
selected: usedIndex == index,
avatar: (usedIndex == index)
? null
: (voiceLanguageOptionsAvailable
.contains(
languageOptionIds
.elementAt(
index,
),
))
? const Icon(
Icons
.spatial_tracking_rounded,
)
: null,
checkmarkColor:
(usedIndex == index &&
!(prefs?.getBool(
"useDeviceTheme",
) ??
false))
? ((MediaQuery.of(
context,
).platformBrightness ==
Brightness
.light)
? themeLight()
.colorScheme
.secondary
: themeDark()
.colorScheme
.secondary)
: null,
labelStyle:
(usedIndex == index &&
!(prefs?.getBool(
"useDeviceTheme",
) ??
false))
? TextStyle(
color:
(MediaQuery.of(
context,
).platformBrightness ==
Brightness
.light)
? themeLight()
.colorScheme
.secondary
: themeDark()
.colorScheme
.secondary,
)
: null,
selectedColor:
(prefs?.getBool(
"useDeviceTheme",
) ??
false)
? null
: (MediaQuery.of(
context,
).platformBrightness ==
Brightness.light)
? themeLight()
.colorScheme
.primary
: themeDark()
.colorScheme
.primary,
onSelected: (bool selected) {
selectionHaptic();
setLocalState(() {
usedIndex = selected
? index
: -1;
});
},
);
},
).toList(),
),
),
),
],
),
),
);
},
),
);
},
disabled:
!voiceSupported ||
!(prefs!.getBool("voiceModeEnabled") ?? false),
),
titleDivider(),
toggle(
context,
AppLocalizations.of(context).settingsVoiceLimitLanguage,
prefs!.getBool("voiceLimitLanguage") ?? true, (value) {
selectionHaptic();
prefs!.setBool("voiceLimitLanguage", value);
setState(() {});
},
disabled: !voiceSupported ||
!(prefs!.getBool("voiceModeEnabled") ?? false)),
context,
AppLocalizations.of(context).settingsVoiceLimitLanguage,
prefs!.getBool("voiceLimitLanguage") ?? true,
(value) {
selectionHaptic();
prefs!.setBool("voiceLimitLanguage", value);
setState(() {});
},
disabled:
!voiceSupported ||
!(prefs!.getBool("voiceModeEnabled") ?? false),
),
toggle(
context,
AppLocalizations.of(context).settingsVoicePunctuation,
prefs!.getBool("aiPunctuation") ?? true, (value) {
selectionHaptic();
prefs!.setBool("aiPunctuation", value);
setState(() {});
},
disabled: !voiceSupported ||
!(prefs!.getBool("voiceModeEnabled") ?? false))
]),
context,
AppLocalizations.of(context).settingsVoicePunctuation,
prefs!.getBool("aiPunctuation") ?? true,
(value) {
selectionHaptic();
prefs!.setBool("aiPunctuation", value);
setState(() {});
},
disabled:
!voiceSupported ||
!(prefs!.getBool("voiceModeEnabled") ?? false),
),
],
),
const SizedBox(height: 8),
button(
AppLocalizations.of(context)
.settingsExperimentalBetaFeature,
Icons.warning_rounded,
null,
color: Colors.orange
.harmonizeWith(Theme.of(context).colorScheme.primary),
onLongTap: () {
),
const SizedBox(height: 8),
button(
AppLocalizations.of(context).settingsExperimentalBetaFeature,
Icons.warning_rounded,
null,
color: Colors.orange.harmonizeWith(
Theme.of(context).colorScheme.primary,
),
onLongTap: () {
selectionHaptic();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context)
.settingsExperimentalBetaDescription),
showCloseIcon: true));
})
]))),
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
).settingsExperimentalBetaDescription,
),
showCloseIcon: true,
),
);
},
),
],
),
),
),
);
}
}

View File

@ -37,36 +37,39 @@ class _ScreenVoiceState extends State<ScreenVoice> {
speaking = true;
sttDone = false;
});
var textOldOld = text;
final textOldOld = text;
var textOld = "";
text = "";
speech.listen(
localeId: prefs!.getString("voiceLanguage") ?? "en-US",
listenOptions:
stt.SpeechListenOptions(listenMode: stt.ListenMode.dictation),
onResult: (result) {
lightHaptic();
if (!speaking) return;
setState(() {
sttDone = result.finalResult;
text = result.recognizedWords;
});
},
pauseFor: const Duration(seconds: 3));
localeId: prefs!.getString("voiceLanguage") ?? "en-US",
listenOptions: stt.SpeechListenOptions(
listenMode: stt.ListenMode.dictation,
),
onResult: (result) {
lightHaptic();
if (!speaking) return;
setState(() {
sttDone = result.finalResult;
text = result.recognizedWords;
});
},
pauseFor: const Duration(seconds: 3),
);
var start = DateTime.now();
var timeout = false;
await Future.doWhile(() =>
Future.delayed(const Duration(milliseconds: 1)).then((_) {
if (textOld != text) {
start = DateTime.now();
}
timeout =
DateTime.now().difference(start) >= const Duration(seconds: 3);
textOld = text;
return !sttDone && speaking && !timeout;
}));
await Future.doWhile(
() => Future.delayed(const Duration(milliseconds: 1)).then((_) {
if (textOld != text) {
start = DateTime.now();
}
timeout =
DateTime.now().difference(start) >= const Duration(seconds: 3);
textOld = text;
return !sttDone && speaking && !timeout;
}),
);
if (!sttDone || timeout) {
sttDone = true;
speech.stop();
@ -103,17 +106,21 @@ class _ScreenVoiceState extends State<ScreenVoice> {
aiThinking = true;
try {
if (prefs!.getBool("aiPunctuation") ?? true) {
var generated = await ollamaClient
final generated = await ollamaClient
.generateCompletion(
request: llama.GenerateCompletionRequest(
model: model!,
prompt:
"Add punctuation and syntax to the following sentence. You must not change order of words or a word in itself! You must not add any word or phrase or remove one! Do not change between formal and personal form, keep the original one!\n\n$text",
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
model: model!,
prompt:
"Add punctuation and syntax to the following sentence. You must not change order of words or a word in itself! You must not add any word or phrase or remove one! Do not change between formal and personal form, keep the original one!\n\n$text",
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300"),
),
)
.timeout(Duration(
.timeout(
Duration(
seconds: (10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
.round()));
.round(),
),
);
setState(() {
text = generated.response!;
});
@ -121,50 +128,59 @@ class _ScreenVoiceState extends State<ScreenVoice> {
} catch (_) {}
// ignore: use_build_context_synchronously
send(text, context, setState, onStream: (currentText, done) async {
setState(() {
aiText = currentText;
lightHaptic();
updateScrollState();
});
if (done) {
aiThinking = false;
heavyHaptic();
send(
text,
context,
setState,
onStream: (currentText, done) async {
setState(() {
aiText = currentText;
lightHaptic();
updateScrollState();
});
if (done) {
aiThinking = false;
heavyHaptic();
if (currentText.isEmpty) {
text = "";
speaking = false;
try {
setState(() {});
} catch (_) {}
return;
if (currentText.isEmpty) {
text = "";
speaking = false;
try {
setState(() {});
} catch (_) {}
return;
}
if ((await voice.getLanguages as List).contains(
(prefs!.getString("voiceLanguage") ?? "en_US").replaceAll("_", "-"),
)) {
voice
..setLanguage(
(prefs!.getString("voiceLanguage") ?? "en_US").replaceAll(
"_",
"-",
),
)
..setSpeechRate(0.6)
..setCompletionHandler(() async {
speaking = false;
try {
setState(() {});
} catch (_) {}
process();
});
final tmp = aiText
..replaceAll("-", ".")
..replaceAll("*", ".");
voice.speak(tmp);
}
}
if ((await voice.getLanguages as List).contains(
(prefs!.getString("voiceLanguage") ?? "en_US")
.replaceAll("_", "-"))) {
voice
..setLanguage((prefs!.getString("voiceLanguage") ?? "en_US")
.replaceAll("_", "-"))
..setSpeechRate(0.6)
..setCompletionHandler(() async {
speaking = false;
try {
setState(() {});
} catch (_) {}
process();
});
var tmp = aiText
..replaceAll("-", ".")
..replaceAll("*", ".");
voice.speak(tmp);
}
}
},
addToSystem: (prefs!.getBool("voiceLimitLanguage") ?? true)
? "You must write in the following language: ${prefs!.getString("voiceLanguage") ?? "en_US"}!"
: null);
},
addToSystem: (prefs!.getBool("voiceLimitLanguage") ?? true)
? "You must write in the following language: ${prefs!.getString("voiceLanguage") ?? "en_US"}!"
: null,
);
}
final ScrollController scrollController = ScrollController();
@ -183,7 +199,7 @@ class _ScreenVoiceState extends State<ScreenVoice> {
scrollController.addListener(updateScrollState);
Future<void> load() async {
var tmp = await speech.locales();
final tmp = await speech.locales();
languageOptionIds = tmp.map((e) => e.localeId);
languageOptions = tmp.map((e) => e.name);
setState(() {});
@ -202,198 +218,235 @@ class _ScreenVoiceState extends State<ScreenVoice> {
@override
Widget build(BuildContext context) {
return Theme(
data: themeDark(),
child: PopScope(
canPop: !aiThinking,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) return;
speaking = false;
voice.stop();
if (chatUuid != null) {
loadChat(chatUuid!, setGlobalState!);
}
settingsOpen = false;
logoVisible = true;
},
child: Scaffold(
appBar: AppBar(
scrolledUnderElevation: 0.0,
leading: IconButton(
enableFeedback: false,
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.close_rounded,
color: Colors.grey)),
title: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
(model ??
AppLocalizations.of(context)
.noSelectedModel)
.split(":")[0],
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
style: const TextStyle(
fontFamily: "monospace", fontSize: 16))),
],
data: themeDark(),
child: PopScope(
canPop: !aiThinking,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) return;
speaking = false;
voice.stop();
if (chatUuid != null) {
loadChat(chatUuid!, setGlobalState!);
}
settingsOpen = false;
logoVisible = true;
},
child: Scaffold(
appBar: AppBar(
scrolledUnderElevation: 0.0,
leading: IconButton(
enableFeedback: false,
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.close_rounded, color: Colors.grey),
),
title: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
(model ?? AppLocalizations.of(context).noSelectedModel)
.split(":")[0],
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
style: const TextStyle(
fontFamily: "monospace",
fontSize: 16,
),
actions: [
IconButton(
enableFeedback: false,
onPressed: () {
speaking = false;
settingsOpen = false;
logoVisible = true;
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) =>
const ScreenSettingsVoice()));
},
icon: const Icon(
Icons.settings_rounded,
color: Colors.grey,
))
]),
body: SafeArea(
child: Column(mainAxisSize: MainAxisSize.max, children: [
Expanded(
child: Column(mainAxisSize: MainAxisSize.max, children: [
Expanded(
child: Padding(
),
),
],
),
actions: [
IconButton(
enableFeedback: false,
onPressed: () {
speaking = false;
settingsOpen = false;
logoVisible = true;
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const ScreenSettingsVoice(),
),
);
},
icon: const Icon(Icons.settings_rounded, color: Colors.grey),
),
],
),
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Center(
child: Text(text,
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
style: const TextStyle(
color: Colors.grey,
fontFamily: "monospace"))),
))
]),
),
Expanded(
child: Center(
child: DateTimeLoopBuilder(
timeUnit: TimeUnit.seconds,
builder: (context, dateTime, child) {
return SizedBox(
height: 96,
width: 96,
child: AnimatedScale(
scale: speaking
? aiThinking
? dateTime.second.isEven
? 2.4
: 2
: 2
: dateTime.second
.toString()
.endsWith("1")
? 1.6
: 1.4,
duration: aiThinking
? const Duration(seconds: 1)
: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: InkWell(
borderRadius:
BorderRadius.circular(48),
onTap: () {
if (speaking && !aiThinking) {
intendedStop = true;
speaking = false;
voice.stop();
return;
}
process();
},
child: CircleAvatar(
backgroundColor: themeDark()
child: Text(
text,
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
style: const TextStyle(
color: Colors.grey,
fontFamily: "monospace",
),
),
),
),
),
],
),
),
Expanded(
child: Center(
child: DateTimeLoopBuilder(
timeUnit: TimeUnit.seconds,
builder: (context, dateTime, child) {
return SizedBox(
height: 96,
width: 96,
child: AnimatedScale(
scale: speaking
? aiThinking
? dateTime.second.isEven
? 2.4
: 2
: 2
: dateTime.second.toString().endsWith("1")
? 1.6
: 1.4,
duration: aiThinking
? const Duration(seconds: 1)
: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: InkWell(
borderRadius: BorderRadius.circular(48),
onTap: () {
if (speaking && !aiThinking) {
intendedStop = true;
speaking = false;
voice.stop();
return;
}
process();
},
child: CircleAvatar(
backgroundColor: themeDark().colorScheme.primary
.withAlpha(!speaking ? 200 : 255),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: speaking
? aiThinking
? Icon(
Icons.auto_awesome_rounded,
color: themeDark()
.colorScheme
.primary
.withAlpha(
!speaking ? 200 : 255),
child: AnimatedSwitcher(
duration: const Duration(
milliseconds: 200),
child: speaking
? aiThinking
? Icon(Icons.auto_awesome_rounded,
color: themeDark()
.colorScheme
.secondary,
key: const ValueKey(
"aiThinking"))
: sttDone
? Icon(Icons.volume_up_rounded,
color: themeDark()
.colorScheme
.secondary,
key: const ValueKey(
"tts"))
: Icon(Icons.mic_rounded,
color: themeDark()
.colorScheme
.secondary,
key: const ValueKey("stt"))
: null)))),
);
}))),
Expanded(
child: Column(mainAxisSize: MainAxisSize.max, children: [
Expanded(
child: Stack(
.secondary,
key: const ValueKey(
"aiThinking",
),
)
: sttDone
? Icon(
Icons.volume_up_rounded,
color: themeDark()
.colorScheme
.secondary,
key: const ValueKey("tts"),
)
: Icon(
Icons.mic_rounded,
color: themeDark()
.colorScheme
.secondary,
key: const ValueKey("stt"),
)
: null,
),
),
),
),
);
},
),
),
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Stack(
children: [
ShaderMask(
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: const [
Colors.transparent,
Colors.black
],
stops: [0.0, atScrollEnd ? 0.0 : 0.1],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: SingleChildScrollView(
controller: scrollController,
child: Padding(
padding: const EdgeInsets.only(
left: 16, right: 16),
child: Center(
child: Text(aiText,
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
style: const TextStyle(
fontFamily:
"monospace")))))),
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: const [
Colors.transparent,
Colors.black,
],
stops: [0.0, atScrollEnd ? 0.0 : 0.1],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: SingleChildScrollView(
controller: scrollController,
child: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
),
child: Center(
child: Text(
aiText,
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
style: const TextStyle(
fontFamily: "monospace",
),
),
),
),
),
),
if (!atScrollEnd)
Positioned(
right: 0,
bottom: 0,
child: IconButton(
icon: const Icon(
Icons.arrow_downward_rounded,
color: Colors.grey),
onPressed: () {
scrollController.animateTo(
scrollController
.position.maxScrollExtent,
duration:
const Duration(milliseconds: 500),
curve: Curves.easeInOut);
}),
)
icon: const Icon(
Icons.arrow_downward_rounded,
color: Colors.grey,
),
onPressed: () {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(
milliseconds: 500,
),
curve: Curves.easeInOut,
);
},
),
),
],
))
]),
)
]),
))));
),
),
],
),
),
],
),
),
),
),
);
}
}

View File

@ -27,84 +27,105 @@ class _ScreenWelcomeState extends State<ScreenWelcome> {
precacheImage(const AssetImage("assets/welcome/3dark.png"), context);
return Scaffold(
bottomNavigationBar: BottomSheet(
enableDrag: false,
backgroundColor: (Theme.of(context).brightness == Brightness.light)
? Colors.grey[100]
: Colors.grey[900],
onClosing: () {},
builder: (context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
child: Column(mainAxisSize: MainAxisSize.min, children: [
SmoothPageIndicator(
controller: _pageController,
count: 3,
effect: ExpandingDotsEffect(
activeDotColor: (Theme.of(context).brightness ==
Brightness.light)
? themeLight().colorScheme.primary
: themeDark().colorScheme.primary)),
]));
}),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: FloatingActionButton(
onPressed: () {
if (page < 2) {
_pageController.nextPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut);
} else {
prefs!.setBool("welcomeFinished", true);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const ScreenMain()));
}
},
child: (page < 2)
? const Icon(Icons.arrow_forward)
: const Icon(Icons.check_rounded)),
body: SafeArea(
child: Column(children: [
Expanded(
child: PageView(
bottomNavigationBar: BottomSheet(
enableDrag: false,
backgroundColor: (Theme.of(context).brightness == Brightness.light)
? Colors.grey[100]
: Colors.grey[900],
onClosing: () {},
builder: (context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SmoothPageIndicator(
controller: _pageController,
onPageChanged: (value) {
setState(() {
page = value;
});
},
children: [
Center(
count: 3,
effect: ExpandingDotsEffect(
activeDotColor:
(Theme.of(context).brightness == Brightness.light)
? themeLight().colorScheme.primary
: themeDark().colorScheme.primary,
),
),
],
),
);
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: FloatingActionButton(
onPressed: () {
if (page < 2) {
_pageController.nextPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
} else {
prefs!.setBool("welcomeFinished", true);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const ScreenMain()),
);
}
},
child: (page < 2)
? const Icon(Icons.arrow_forward)
: const Icon(Icons.check_rounded),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (value) {
setState(() {
page = value;
});
},
children: [
Center(
child: (Theme.of(context).brightness == Brightness.light)
? FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image: const AssetImage("assets/welcome/1.png"))
image: const AssetImage("assets/welcome/1.png"),
)
: FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image:
const AssetImage("assets/welcome/1dark.png"))),
Center(
image: const AssetImage("assets/welcome/1dark.png"),
),
),
Center(
child: (Theme.of(context).brightness == Brightness.light)
? FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image: const AssetImage("assets/welcome/2.png"))
image: const AssetImage("assets/welcome/2.png"),
)
: FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image:
const AssetImage("assets/welcome/2dark.png"))),
Center(
image: const AssetImage("assets/welcome/2dark.png"),
),
),
Center(
child: (Theme.of(context).brightness == Brightness.light)
? FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image: const AssetImage("assets/welcome/3.png"))
image: const AssetImage("assets/welcome/3.png"),
)
: FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image:
const AssetImage("assets/welcome/3dark.png")))
])),
])));
image: const AssetImage("assets/welcome/3dark.png"),
),
),
],
),
),
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
export 'package:drift/drift.dart';
part 'chat_database.g.dart';
late final ChatDatabase chatDb;
class Chats extends Table {
TextColumn get id => text().unique()();
TextColumn get json => text()();
@override
Set<Column<Object>>? get primaryKey => {id};
}
class Assets extends Table {
TextColumn get id => text().unique()();
TextColumn get chatId =>
text().references(Chats, #id, onDelete: KeyAction.cascade)();
BlobColumn get data => blob()();
@override
Set<Column<Object>>? get primaryKey => {id};
}
@DriftDatabase(tables: [Chats, Assets])
class ChatDatabase extends _$ChatDatabase {
ChatDatabase([QueryExecutor? executor])
: super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(
name: "chatDatabase",
native: const DriftNativeOptions(),
web: DriftWebOptions(
sqlite3Wasm: Uri.parse("sqlite3.wasm"),
driftWorker: Uri.parse("drift_worker.js"),
),
);
}
@override
MigrationStrategy get migration {
return MigrationStrategy(
beforeOpen: (details) async {
await customStatement("PRAGMA foreign_keys = ON");
},
onUpgrade: (Migrator m, int from, int to) async {
// if (from < 2) {}
},
);
}
}

View File

@ -0,0 +1,998 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_database.dart';
// ignore_for_file: type=lint
class $ChatsTable extends Chats with TableInfo<$ChatsTable, Chat> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$ChatsTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'),
);
static const VerificationMeta _jsonMeta = const VerificationMeta('json');
@override
late final GeneratedColumn<String> json = GeneratedColumn<String>(
'json',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
@override
List<GeneratedColumn> get $columns => [id, json];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'chats';
@override
VerificationContext validateIntegrity(
Insertable<Chat> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('json')) {
context.handle(
_jsonMeta,
json.isAcceptableOrUnknown(data['json']!, _jsonMeta),
);
} else if (isInserting) {
context.missing(_jsonMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
Chat map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return Chat(
id: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
json: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}json'],
)!,
);
}
@override
$ChatsTable createAlias(String alias) {
return $ChatsTable(attachedDatabase, alias);
}
}
class Chat extends DataClass implements Insertable<Chat> {
final String id;
final String json;
const Chat({required this.id, required this.json});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
map['json'] = Variable<String>(json);
return map;
}
ChatsCompanion toCompanion(bool nullToAbsent) {
return ChatsCompanion(id: Value(id), json: Value(json));
}
factory Chat.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return Chat(
id: serializer.fromJson<String>(json['id']),
json: serializer.fromJson<String>(json['json']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'json': serializer.toJson<String>(json),
};
}
Chat copyWith({String? id, String? json}) =>
Chat(id: id ?? this.id, json: json ?? this.json);
Chat copyWithCompanion(ChatsCompanion data) {
return Chat(
id: data.id.present ? data.id.value : this.id,
json: data.json.present ? data.json.value : this.json,
);
}
@override
String toString() {
return (StringBuffer('Chat(')
..write('id: $id, ')
..write('json: $json')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, json);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Chat && other.id == this.id && other.json == this.json);
}
class ChatsCompanion extends UpdateCompanion<Chat> {
final Value<String> id;
final Value<String> json;
final Value<int> rowid;
const ChatsCompanion({
this.id = const Value.absent(),
this.json = const Value.absent(),
this.rowid = const Value.absent(),
});
ChatsCompanion.insert({
required String id,
required String json,
this.rowid = const Value.absent(),
}) : id = Value(id),
json = Value(json);
static Insertable<Chat> custom({
Expression<String>? id,
Expression<String>? json,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (json != null) 'json': json,
if (rowid != null) 'rowid': rowid,
});
}
ChatsCompanion copyWith({
Value<String>? id,
Value<String>? json,
Value<int>? rowid,
}) {
return ChatsCompanion(
id: id ?? this.id,
json: json ?? this.json,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (json.present) {
map['json'] = Variable<String>(json.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('ChatsCompanion(')
..write('id: $id, ')
..write('json: $json, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class $AssetsTable extends Assets with TableInfo<$AssetsTable, Asset> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$AssetsTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'),
);
static const VerificationMeta _chatIdMeta = const VerificationMeta('chatId');
@override
late final GeneratedColumn<String> chatId = GeneratedColumn<String>(
'chat_id',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES chats (id) ON DELETE CASCADE',
),
);
static const VerificationMeta _dataMeta = const VerificationMeta('data');
@override
late final GeneratedColumn<Uint8List> data = GeneratedColumn<Uint8List>(
'data',
aliasedName,
false,
type: DriftSqlType.blob,
requiredDuringInsert: true,
);
@override
List<GeneratedColumn> get $columns => [id, chatId, data];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'assets';
@override
VerificationContext validateIntegrity(
Insertable<Asset> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('chat_id')) {
context.handle(
_chatIdMeta,
chatId.isAcceptableOrUnknown(data['chat_id']!, _chatIdMeta),
);
} else if (isInserting) {
context.missing(_chatIdMeta);
}
if (data.containsKey('data')) {
context.handle(
_dataMeta,
this.data.isAcceptableOrUnknown(data['data']!, _dataMeta),
);
} else if (isInserting) {
context.missing(_dataMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
Asset map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return Asset(
id: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
chatId: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}chat_id'],
)!,
data: attachedDatabase.typeMapping.read(
DriftSqlType.blob,
data['${effectivePrefix}data'],
)!,
);
}
@override
$AssetsTable createAlias(String alias) {
return $AssetsTable(attachedDatabase, alias);
}
}
class Asset extends DataClass implements Insertable<Asset> {
final String id;
final String chatId;
final Uint8List data;
const Asset({required this.id, required this.chatId, required this.data});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
map['chat_id'] = Variable<String>(chatId);
map['data'] = Variable<Uint8List>(data);
return map;
}
AssetsCompanion toCompanion(bool nullToAbsent) {
return AssetsCompanion(
id: Value(id),
chatId: Value(chatId),
data: Value(data),
);
}
factory Asset.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return Asset(
id: serializer.fromJson<String>(json['id']),
chatId: serializer.fromJson<String>(json['chatId']),
data: serializer.fromJson<Uint8List>(json['data']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'chatId': serializer.toJson<String>(chatId),
'data': serializer.toJson<Uint8List>(data),
};
}
Asset copyWith({String? id, String? chatId, Uint8List? data}) => Asset(
id: id ?? this.id,
chatId: chatId ?? this.chatId,
data: data ?? this.data,
);
Asset copyWithCompanion(AssetsCompanion data) {
return Asset(
id: data.id.present ? data.id.value : this.id,
chatId: data.chatId.present ? data.chatId.value : this.chatId,
data: data.data.present ? data.data.value : this.data,
);
}
@override
String toString() {
return (StringBuffer('Asset(')
..write('id: $id, ')
..write('chatId: $chatId, ')
..write('data: $data')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, chatId, $driftBlobEquality.hash(data));
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Asset &&
other.id == this.id &&
other.chatId == this.chatId &&
$driftBlobEquality.equals(other.data, this.data));
}
class AssetsCompanion extends UpdateCompanion<Asset> {
final Value<String> id;
final Value<String> chatId;
final Value<Uint8List> data;
final Value<int> rowid;
const AssetsCompanion({
this.id = const Value.absent(),
this.chatId = const Value.absent(),
this.data = const Value.absent(),
this.rowid = const Value.absent(),
});
AssetsCompanion.insert({
required String id,
required String chatId,
required Uint8List data,
this.rowid = const Value.absent(),
}) : id = Value(id),
chatId = Value(chatId),
data = Value(data);
static Insertable<Asset> custom({
Expression<String>? id,
Expression<String>? chatId,
Expression<Uint8List>? data,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (chatId != null) 'chat_id': chatId,
if (data != null) 'data': data,
if (rowid != null) 'rowid': rowid,
});
}
AssetsCompanion copyWith({
Value<String>? id,
Value<String>? chatId,
Value<Uint8List>? data,
Value<int>? rowid,
}) {
return AssetsCompanion(
id: id ?? this.id,
chatId: chatId ?? this.chatId,
data: data ?? this.data,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (chatId.present) {
map['chat_id'] = Variable<String>(chatId.value);
}
if (data.present) {
map['data'] = Variable<Uint8List>(data.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('AssetsCompanion(')
..write('id: $id, ')
..write('chatId: $chatId, ')
..write('data: $data, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
abstract class _$ChatDatabase extends GeneratedDatabase {
_$ChatDatabase(QueryExecutor e) : super(e);
$ChatDatabaseManager get managers => $ChatDatabaseManager(this);
late final $ChatsTable chats = $ChatsTable(this);
late final $AssetsTable assets = $AssetsTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [chats, assets];
@override
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([
WritePropagation(
on: TableUpdateQuery.onTableName(
'chats',
limitUpdateKind: UpdateKind.delete,
),
result: [TableUpdate('assets', kind: UpdateKind.delete)],
),
]);
}
typedef $$ChatsTableCreateCompanionBuilder =
ChatsCompanion Function({
required String id,
required String json,
Value<int> rowid,
});
typedef $$ChatsTableUpdateCompanionBuilder =
ChatsCompanion Function({
Value<String> id,
Value<String> json,
Value<int> rowid,
});
final class $$ChatsTableReferences
extends BaseReferences<_$ChatDatabase, $ChatsTable, Chat> {
$$ChatsTableReferences(super.$_db, super.$_table, super.$_typedResult);
static MultiTypedResultKey<$AssetsTable, List<Asset>> _assetsRefsTable(
_$ChatDatabase db,
) => MultiTypedResultKey.fromTable(
db.assets,
aliasName: 'chats__id__assets__chat_id',
);
$$AssetsTableProcessedTableManager get assetsRefs {
final manager = $$AssetsTableTableManager(
$_db,
$_db.assets,
).filter((f) => f.chatId.id.sqlEquals($_itemColumn<String>('id')!));
final cache = $_typedResult.readTableOrNull(_assetsRefsTable($_db));
return ProcessedTableManager(
manager.$state.copyWith(prefetchedData: cache),
);
}
}
class $$ChatsTableFilterComposer extends Composer<_$ChatDatabase, $ChatsTable> {
$$ChatsTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get json => $composableBuilder(
column: $table.json,
builder: (column) => ColumnFilters(column),
);
Expression<bool> assetsRefs(
Expression<bool> Function($$AssetsTableFilterComposer f) f,
) {
final $$AssetsTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.id,
referencedTable: $db.assets,
getReferencedColumn: (t) => t.chatId,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => $$AssetsTableFilterComposer(
$db: $db,
$table: $db.assets,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return f(composer);
}
}
class $$ChatsTableOrderingComposer
extends Composer<_$ChatDatabase, $ChatsTable> {
$$ChatsTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get json => $composableBuilder(
column: $table.json,
builder: (column) => ColumnOrderings(column),
);
}
class $$ChatsTableAnnotationComposer
extends Composer<_$ChatDatabase, $ChatsTable> {
$$ChatsTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get json =>
$composableBuilder(column: $table.json, builder: (column) => column);
Expression<T> assetsRefs<T extends Object>(
Expression<T> Function($$AssetsTableAnnotationComposer a) f,
) {
final $$AssetsTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.id,
referencedTable: $db.assets,
getReferencedColumn: (t) => t.chatId,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => $$AssetsTableAnnotationComposer(
$db: $db,
$table: $db.assets,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return f(composer);
}
}
class $$ChatsTableTableManager
extends
RootTableManager<
_$ChatDatabase,
$ChatsTable,
Chat,
$$ChatsTableFilterComposer,
$$ChatsTableOrderingComposer,
$$ChatsTableAnnotationComposer,
$$ChatsTableCreateCompanionBuilder,
$$ChatsTableUpdateCompanionBuilder,
(Chat, $$ChatsTableReferences),
Chat,
PrefetchHooks Function({bool assetsRefs})
> {
$$ChatsTableTableManager(_$ChatDatabase db, $ChatsTable table)
: super(
TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$ChatsTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$ChatsTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$ChatsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
Value<String> id = const Value.absent(),
Value<String> json = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => ChatsCompanion(id: id, json: json, rowid: rowid),
createCompanionCallback:
({
required String id,
required String json,
Value<int> rowid = const Value.absent(),
}) => ChatsCompanion.insert(id: id, json: json, rowid: rowid),
withReferenceMapper: (p0) => p0
.map(
(e) =>
(e.readTable(table), $$ChatsTableReferences(db, table, e)),
)
.toList(),
prefetchHooksCallback: ({assetsRefs = false}) {
return PrefetchHooks(
db: db,
explicitlyWatchedTables: [if (assetsRefs) db.assets],
addJoins: null,
getPrefetchedDataCallback: (items) async {
return [
if (assetsRefs)
await $_getPrefetchedData<Chat, $ChatsTable, Asset>(
currentTable: table,
referencedTable: $$ChatsTableReferences._assetsRefsTable(
db,
),
managerFromTypedResult: (p0) =>
$$ChatsTableReferences(db, table, p0).assetsRefs,
referencedItemsForCurrentItem: (item, referencedItems) =>
referencedItems.where((e) => e.chatId == item.id),
typedResults: items,
),
];
},
);
},
),
);
}
typedef $$ChatsTableProcessedTableManager =
ProcessedTableManager<
_$ChatDatabase,
$ChatsTable,
Chat,
$$ChatsTableFilterComposer,
$$ChatsTableOrderingComposer,
$$ChatsTableAnnotationComposer,
$$ChatsTableCreateCompanionBuilder,
$$ChatsTableUpdateCompanionBuilder,
(Chat, $$ChatsTableReferences),
Chat,
PrefetchHooks Function({bool assetsRefs})
>;
typedef $$AssetsTableCreateCompanionBuilder =
AssetsCompanion Function({
required String id,
required String chatId,
required Uint8List data,
Value<int> rowid,
});
typedef $$AssetsTableUpdateCompanionBuilder =
AssetsCompanion Function({
Value<String> id,
Value<String> chatId,
Value<Uint8List> data,
Value<int> rowid,
});
final class $$AssetsTableReferences
extends BaseReferences<_$ChatDatabase, $AssetsTable, Asset> {
$$AssetsTableReferences(super.$_db, super.$_table, super.$_typedResult);
static $ChatsTable _chatIdTable(_$ChatDatabase db) =>
db.chats.createAlias('assets__chat_id__chats__id');
$$ChatsTableProcessedTableManager get chatId {
final $_column = $_itemColumn<String>('chat_id')!;
final manager = $$ChatsTableTableManager(
$_db,
$_db.chats,
).filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_chatIdTable($_db));
if (item == null) return manager;
return ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]),
);
}
}
class $$AssetsTableFilterComposer
extends Composer<_$ChatDatabase, $AssetsTable> {
$$AssetsTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<Uint8List> get data => $composableBuilder(
column: $table.data,
builder: (column) => ColumnFilters(column),
);
$$ChatsTableFilterComposer get chatId {
final $$ChatsTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.chatId,
referencedTable: $db.chats,
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => $$ChatsTableFilterComposer(
$db: $db,
$table: $db.chats,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$AssetsTableOrderingComposer
extends Composer<_$ChatDatabase, $AssetsTable> {
$$AssetsTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<Uint8List> get data => $composableBuilder(
column: $table.data,
builder: (column) => ColumnOrderings(column),
);
$$ChatsTableOrderingComposer get chatId {
final $$ChatsTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.chatId,
referencedTable: $db.chats,
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => $$ChatsTableOrderingComposer(
$db: $db,
$table: $db.chats,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$AssetsTableAnnotationComposer
extends Composer<_$ChatDatabase, $AssetsTable> {
$$AssetsTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<Uint8List> get data =>
$composableBuilder(column: $table.data, builder: (column) => column);
$$ChatsTableAnnotationComposer get chatId {
final $$ChatsTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.chatId,
referencedTable: $db.chats,
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => $$ChatsTableAnnotationComposer(
$db: $db,
$table: $db.chats,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$AssetsTableTableManager
extends
RootTableManager<
_$ChatDatabase,
$AssetsTable,
Asset,
$$AssetsTableFilterComposer,
$$AssetsTableOrderingComposer,
$$AssetsTableAnnotationComposer,
$$AssetsTableCreateCompanionBuilder,
$$AssetsTableUpdateCompanionBuilder,
(Asset, $$AssetsTableReferences),
Asset,
PrefetchHooks Function({bool chatId})
> {
$$AssetsTableTableManager(_$ChatDatabase db, $AssetsTable table)
: super(
TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$AssetsTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$AssetsTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$AssetsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
Value<String> id = const Value.absent(),
Value<String> chatId = const Value.absent(),
Value<Uint8List> data = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => AssetsCompanion(
id: id,
chatId: chatId,
data: data,
rowid: rowid,
),
createCompanionCallback:
({
required String id,
required String chatId,
required Uint8List data,
Value<int> rowid = const Value.absent(),
}) => AssetsCompanion.insert(
id: id,
chatId: chatId,
data: data,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map(
(e) =>
(e.readTable(table), $$AssetsTableReferences(db, table, e)),
)
.toList(),
prefetchHooksCallback: ({chatId = false}) {
return PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins:
<
T extends TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic
>
>(state) {
if (chatId) {
state =
state.withJoin(
currentTable: table,
currentColumn: table.chatId,
referencedTable: $$AssetsTableReferences
._chatIdTable(db),
referencedColumn: $$AssetsTableReferences
._chatIdTable(db)
.id,
)
as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
),
);
}
typedef $$AssetsTableProcessedTableManager =
ProcessedTableManager<
_$ChatDatabase,
$AssetsTable,
Asset,
$$AssetsTableFilterComposer,
$$AssetsTableOrderingComposer,
$$AssetsTableAnnotationComposer,
$$AssetsTableCreateCompanionBuilder,
$$AssetsTableUpdateCompanionBuilder,
(Asset, $$AssetsTableReferences),
Asset,
PrefetchHooks Function({bool chatId})
>;
class $ChatDatabaseManager {
final _$ChatDatabase _db;
$ChatDatabaseManager(this._db);
$$ChatsTableTableManager get chats =>
$$ChatsTableTableManager(_db, _db.chats);
$$AssetsTableTableManager get assets =>
$$AssetsTableTableManager(_db, _db.assets);
}

View File

@ -5,6 +5,7 @@ import 'package:http/http.dart' as http;
import 'package:ollama_dart/ollama_dart.dart' as llama;
import '../main.dart';
import 'host.dart';
const String userAgent = "Ollama App";
@ -13,14 +14,16 @@ class OllamaHttpOverrides extends HttpOverrides {
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..userAgent = userAgent
..badCertificateCallback = (_, __, ___) => true;
..badCertificateCallback = (_, _, _) => true;
}
}
final httpClient = http.Client();
llama.OllamaClient get ollamaClient => llama.OllamaClient(
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
.cast<String, String>(),
baseUrl: "$host/api",
client: httpClient,
config: llama.OllamaConfig(
baseUrl: HostManager.instance.host!.toString(),
defaultHeaders: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
.cast<String, String>(),
),
httpClient: httpClient,
);

50
lib/services/curves.dart Normal file
View File

@ -0,0 +1,50 @@
import 'package:flutter/animation.dart';
typedef CurveGroup = ({
Curve fast,
Duration fastDuration,
Curve normal,
Duration normalDuration,
Curve slow,
Duration slowDuration,
});
final class ExpressiveCurves {
ExpressiveCurves._();
static const CurveGroup expressiveSpatial = (
fast: Cubic(0.42, 1.67, 0.21, 0.90),
fastDuration: Duration(milliseconds: 350),
normal: Cubic(0.38, 1.21, 0.22, 1.00),
normalDuration: Duration(milliseconds: 500),
slow: Cubic(0.39, 1.29, 0.35, 0.98),
slowDuration: Duration(milliseconds: 650),
);
static const CurveGroup expressiveEffects = (
fast: Cubic(0.31, 0.94, 0.34, 1.00),
fastDuration: Duration(milliseconds: 150),
normal: Cubic(0.34, 0.80, 0.34, 1.00),
normalDuration: Duration(milliseconds: 200),
slow: Cubic(0.34, 0.88, 0.34, 1.00),
slowDuration: Duration(milliseconds: 300),
);
static const CurveGroup standardSpatial = (
fast: Cubic(0.27, 1.06, 0.18, 1.00),
fastDuration: Duration(milliseconds: 350),
normal: Cubic(0.27, 1.06, 0.18, 1.00),
normalDuration: Duration(milliseconds: 500),
slow: Cubic(0.27, 1.06, 0.18, 1.00),
slowDuration: Duration(milliseconds: 750),
);
static const CurveGroup standardEffects = (
fast: Cubic(0.31, 0.94, 0.34, 1.00),
fastDuration: Duration(milliseconds: 150),
normal: Cubic(0.34, 0.80, 0.34, 1.00),
normalDuration: Duration(milliseconds: 200),
slow: Cubic(0.34, 0.88, 0.34, 1.00),
slowDuration: Duration(milliseconds: 300),
);
}

View File

@ -6,11 +6,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:ollama_dart/ollama_dart.dart';
import 'package:ollama_dart/ollama_dart.dart' as llama;
import 'package:url_launcher/url_launcher.dart';
import '../l10n/gen/app_localizations.dart';
import 'markdown.dart';
import 'services.dart';
final _logIdRegex = RegExp(r"^[A-Z0-9]{2,16}$");
@ -23,7 +24,7 @@ typedef ErrorGuardDetailsMessageGenerator =
typedef ErrorGuardIgnoreIfGenerator = bool Function(Object exception);
String _defaultErrorMessage(Object exception) => switch (exception) {
OllamaClientException _ => "Unknown client error",
llama.OllamaException _ => "Unknown client error",
AssertionError _ =>
exception.toString().split(": ").elementAtOrNull(4) ??
"An assertion failed",
@ -39,15 +40,15 @@ String? _defaultDetailsMessage(
Object exception,
StackTrace stackTrace,
) => switch (exception) {
OllamaClientException e =>
final llama.OllamaException e =>
[
"SocketException",
"HttpException",
"ClientException",
].contains(e.body.toString().split(":").first)
SocketException,
HttpException,
ClientException,
].contains(e.cause.runtimeType)
? "A network error occurred while trying to connect to the server."
"\n\nYou may check your network connection and server reachability and try again."
: "The Ollama API client received a faulty response with code `${e.code}`."
: "The Ollama API client received a faulty response with code `${e.message.split(":").last.trim()}`."
"\n\nPlease check your Ollama server or proxy configuration and try again.",
AssertionError _ =>
"An assertion failed, meaning that the app is misconfigured or a bug occurred."
@ -123,7 +124,7 @@ ErrorGuardDetailsMessageGenerator errorGuardDetailsMessageWithFallbackSingle(
/// alphanumeric characters, starting with a letter. Other values will be
/// ignored. The recommended length is 8 characters.
///
/// To generate a random log ID, run: `dart run tools/logid.dart`
/// To generate a random log ID, run: `dart run tool/logid.dart`
///
/// The [errorMessage] function is used to generate the error message. This
/// should be a short message that describes the error in a user-friendly way.
@ -156,6 +157,11 @@ ErrorGuardDetailsMessageGenerator errorGuardDetailsMessageWithFallbackSingle(
/// useful if it's certain that the error is not caused by a bug in the app,
/// but rather by a user error or a misconfiguration. [forceReporting] can be
/// used to force the reporting of the error.
///
/// The [onError] callback can be used to perform additional actions when an
/// error occurs, such as logging the error to a remote server or performing
/// additional cleanup. This callback is called before the error dialog or
/// snackbar is shown.
Future<T?> errorGuard<T>(
BuildContext context,
String? logId,
@ -167,6 +173,7 @@ Future<T?> errorGuard<T>(
bool enableDetails = true,
bool enableReporting = true,
bool forceReporting = false,
void Function(Object exception, StackTrace stackTrace)? onError,
}) async {
assert(
logId == null || _logIdRegex.hasMatch(logId),
@ -193,9 +200,10 @@ Future<T?> errorGuard<T>(
action.call(),
).catchError(Error.throwWithStackTrace);
} catch (exception, stackTrace) {
onError?.call(exception, stackTrace);
if (context.mounted && !ignoreIf.call(exception)) {
var dateTime = DateTime.now();
var colorScheme = Theme.of(context).colorScheme;
final dateTime = DateTime.now();
final colorScheme = Theme.of(context).colorScheme;
logId = logId?.toUpperCase();
if (logId != null && !_logIdRegex.hasMatch(logId)) {
@ -261,24 +269,26 @@ Future<T?> errorGuard<T>(
if (instantDialog) {
showErrorDialog();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
_removeNewlines(
Markdown(errorMessageText).toTextSpan(context).toPlainText(),
ScaffoldMessenger.of(context)
..clearSnackBars()
..showSnackBar(
SnackBar(
content: Text(
_removeNewlines(
Markdown(errorMessageText).toTextSpan(context).toPlainText(),
),
style: TextStyle(color: colorScheme.onErrorContainer),
),
style: TextStyle(color: colorScheme.onErrorContainer),
backgroundColor: colorScheme.errorContainer,
action: !enableDetails
? null
: SnackBarAction(
label: AppLocalizations.of(context).errorGuardDetails,
textColor: colorScheme.onErrorContainer,
onPressed: showErrorDialog,
),
),
backgroundColor: colorScheme.errorContainer,
action: !enableDetails
? null
: SnackBarAction(
label: AppLocalizations.of(context).errorGuardDetails,
textColor: colorScheme.onErrorContainer,
onPressed: showErrorDialog,
),
),
);
);
}
}
return null;
@ -317,6 +327,7 @@ class _ErrorGuardDetailsDialogState extends State<_ErrorGuardDetailsDialog> {
return PopScope(
canPop: !widget.forceReporting,
child: AlertDialog(
constraints: const BoxConstraints(minWidth: 280, maxWidth: 560),
title: Stack(
children: [
Align(
@ -334,7 +345,14 @@ class _ErrorGuardDetailsDialogState extends State<_ErrorGuardDetailsDialog> {
),
),
),
Text(AppLocalizations.of(context).errorGuardTitle),
Builder(
builder: (context) => Text(
AppLocalizations.of(context).errorGuardTitle,
style: DefaultTextStyle.of(
context,
).style.copyWith(fontFamily: "GoogleSansCode"),
),
),
],
),
content: Column(
@ -344,7 +362,14 @@ class _ErrorGuardDetailsDialogState extends State<_ErrorGuardDetailsDialog> {
if (widget.logId != null)
Transform.translate(
offset: const Offset(0, -20),
child: Text("@${widget.logId}"),
child: Builder(
builder: (context) => Text(
"@${widget.logId}",
style: DefaultTextStyle.of(
context,
).style.copyWith(fontFamily: "GoogleSansCode"),
),
),
),
Text.rich(Markdown(widget.errorMessage).toTextSpan(context)),
@ -414,7 +439,7 @@ ${(widget.exception != null) ? "```\n${widget.exception}\n```" : "> Not availabl
<summary>Stack Trace</summary>
```
${widget.stackTrace.toString().trim()}
${widget.stackTrace.toString().trim().split("\n").take(20).join("\n")}
```
</details>
@ -424,7 +449,7 @@ ${widget.stackTrace.toString().trim()}
The app suggested the following cause of the issue:
- ***Error Message:*** ${_removeNewlines(widget.errorMessage)}
- ***Details Message:*** ${_removeNewlines((widget.detailsMessage ?? "None provided").trim())}"""
- ***Details Message:*** ${_removeNewlines((widget.detailsMessage ?? "*None provided*").trim())}"""
.trim();
void _report() {
@ -434,7 +459,7 @@ The app suggested the following cause of the issue:
url +=
"&description=${Uri.encodeComponent('Received error: "${widget.errorMessage.replaceFirst(RegExp(r".$"), "")}"${(widget.logId != null) ? " (@${widget.logId})" : ""}')}";
var contextText = _reportText();
final contextText = _reportText();
url += "&context=${Uri.encodeComponent(contextText)}";
Clipboard.setData(ClipboardData(text: url));
@ -475,17 +500,24 @@ class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
super.initState();
_animationController = AnimationController(
value: widget.isExpanded ? 1.0 : 0.0,
duration: kThemeAnimationDuration,
duration: ExpressiveCurves.expressiveEffects.fastDuration,
vsync: this,
);
_expanded = widget.isExpanded;
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.fastEaseInToSlowEaseOut,
curve: ExpressiveCurves.expressiveEffects.fast,
reverseCurve: ExpressiveCurves.expressiveEffects.fast.flipped,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleAnimation() {
setState(() {
_expanded = !_expanded;
@ -514,7 +546,7 @@ class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
final theme = Theme.of(context);
return DecoratedBox(
decoration: BoxDecoration(
@ -541,7 +573,7 @@ class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
),
SizeTransition(
sizeFactor: _animation,
axisAlignment: -1,
alignment: Alignment.topCenter,
child: _monospacedContent(
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12),
@ -549,7 +581,7 @@ class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
? Text(
widget.content.trim(),
style: const TextStyle(
fontFamily: "monospace",
fontFamily: "GoogleSansCode",
height: kTextHeightNone,
),
)

View File

@ -30,9 +30,22 @@ DateTime _lastHapticChat = DateTime.fromMillisecondsSinceEpoch(0);
void chatHaptic() {
if (!Preferences.instance.enableHaptic) return;
var now = DateTime.now();
final now = DateTime.now();
if (now.difference(_lastHapticChat) < _hapticChatDelay) return;
_lastHapticChat = now;
HapticFeedback.selectionClick();
}
const Duration _hapticThinkingDelay = Duration(milliseconds: 150);
DateTime _lastHapticThinking = DateTime.fromMillisecondsSinceEpoch(0);
void chatHapticThinking() {
if (!Preferences.instance.enableHaptic) return;
final now = DateTime.now();
if (now.difference(_lastHapticThinking) < _hapticThinkingDelay) return;
_lastHapticThinking = now;
HapticFeedback.successNotification();
}

142
lib/services/host.dart Normal file
View File

@ -0,0 +1,142 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:ollama_dart/ollama_dart.dart' as ollama;
import '../l10n/gen/app_localizations.dart';
import '../main.dart';
import 'clients.dart' as clients;
import 'services.dart';
enum HostSettingError {
invalidUrl,
unreachable,
undetectable,
timeout,
outdated,
other;
String title(BuildContext context) {
final appLocalizations = AppLocalizations.of(context);
return appLocalizations.settingsHostInvalid(name);
}
String description(BuildContext context) {
final appLocalizations = AppLocalizations.of(context);
return appLocalizations.settingsHostInvalidDetailed(name);
}
}
typedef HostSettingErrorCallbackStore = ({
Object exception,
StackTrace stackTrace,
});
class HostManager extends ChangeNotifier {
static final HostManager _instance = HostManager._();
static HostManager get instance => _instance;
bool _silentFlag = false;
Uri? _host;
Uri? get host => _host;
set host(Uri? value) {
assert(!useHost, "Host is fixed to $fixedHost and cannot be changed.");
if (useHost) return;
var uri = value;
if (uri != null && uri.path.endsWith("/")) {
uri = uri.replace(path: uri.path.substring(0, uri.path.length - 1));
}
if (uri == _host) return;
_host = uri;
Preferences.instance.host = uri;
if (!_silentFlag) notifyListeners();
}
HostManager._()
: _host = useHost ? Uri.parse(fixedHost) : Preferences.instance.host;
Future<HostSettingError?> smartSetHost(
BuildContext context,
String value,
) async {
assert(!useHost, "Host is fixed to $fixedHost and cannot be changed.");
final uri = Uri.tryParse(value)?.normalizePath();
if (uri == null || (uri.scheme != "http" && uri.scheme != "https")) {
return HostSettingError.invalidUrl;
}
final oldHost = this.host;
final host = uri.resolve("api/version");
try {
HostSettingErrorCallbackStore? error;
final response = await errorGuard(
context,
"S4XS48Z9",
() async => clients.httpClient
.get(host, headers: Preferences.instance.hostHeaders)
.timeout(
TimeoutMultiplier.medium,
onTimeout: () => throw HostSettingError.timeout,
),
errorMessage: errorGuardErrorMessageWithFallback(
(_) => "Failed to connect to host.",
),
enableReporting: false,
onError: (e, s) => error = (exception: e, stackTrace: s),
);
if (response == null) {
Error.throwWithStackTrace(error!.exception, error!.stackTrace);
}
if (response.statusCode == 200) {
final version = jsonDecode(response.body)["version"] as String?;
if (!RegExp(r"(?:.*?\.){2}.*").hasMatch(version ?? "")) {
return HostSettingError.undetectable;
}
_silentFlag = true;
this.host = host.resolve("..");
ollama.ListResponse? data;
try {
if (context.mounted) {
data = await errorGuard(
context,
"U7K91OW8",
() async => clients.ollamaClient.models.list().timeout(
TimeoutMultiplier.medium,
onTimeout: () => throw HostSettingError.timeout,
),
enableReporting: false,
onError: (e, s) => error = (exception: e, stackTrace: s),
);
}
if (data == null) {
Error.throwWithStackTrace(error!.exception, error!.stackTrace);
}
} on HostSettingError catch (_) {
rethrow;
} catch (_) {
this.host = oldHost;
return HostSettingError.undetectable;
}
// TODO: maybe implement version checking
ModelManager.instance.loadModelsRaw(data: data);
_silentFlag = false;
notifyListeners();
return null;
}
} on HostSettingError catch (e) {
return e;
} catch (_) {}
return HostSettingError.unreachable;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,54 +2,136 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:ollama_dart/ollama_dart.dart' as ollama;
import '../main.dart';
import 'clients.dart' as clients;
import 'preferences.dart';
typedef ModelCapability = ollama.Capability;
/// Container class for model capabilities.
///
/// See more:
/// - https://github.com/ollama/ollama/blob/main/types/model/capability.go
class ModelCapability {
final String name;
ModelCapability(this.name);
static ModelCapability completion = ModelCapability("completion");
static ModelCapability tools = ModelCapability("tools");
static ModelCapability insertion = ModelCapability("insert");
static ModelCapability vision = ModelCapability("vision");
static ModelCapability embedding = ModelCapability("embedding");
static ModelCapability thinking = ModelCapability("thinking");
static ModelCapability image = ModelCapability("image");
static ModelCapability audio = ModelCapability("audio");
@override
operator ==(Object other) => other is ModelCapability && other.name == name;
@override
int get hashCode => name.hashCode;
IconData? get icon => switch (name) {
"completion" => Icons.text_fields,
"tools" => Icons.build,
"insert" => Icons.read_more,
"vision" => Icons.visibility_outlined,
"embedding" => Icons.link,
"thinking" => Icons.emoji_objects_outlined,
"image" => Icons.image_outlined,
"audio" => Icons.audiotrack_outlined,
_ => Icons.inventory_2_outlined,
};
}
class Model extends ChangeNotifier {
final String name;
List<TextSpan> nameColored(BuildContext context) =>
nameColoredStatic(name: name, context: context)!;
static List<TextSpan>? nameColoredStatic({
required String? name,
required BuildContext context,
}) {
if (name == null) return null;
final colorScheme = ColorScheme.of(context);
final parts = name.split(":");
assert(
parts.isNotEmpty && parts.length <= 2,
"Model name should be in format family:version, but got $name",
);
return [
TextSpan(text: parts.first),
if (parts.length == 2)
TextSpan(
text: ":${parts.last}",
style: TextStyle(color: colorScheme.outline),
),
];
}
DateTime _modifiedAt;
DateTime get modifiedAt => _modifiedAt;
int _size;
int get size => _size;
String _family;
String get family => _family;
Set<String> _families;
Set<String> get families => Set.unmodifiable(_families);
String _parameterSize;
String get parameterSize => _parameterSize;
Set<ModelCapability> _capabilities;
Set<ModelCapability> get capabilities => Set.unmodifiable(_capabilities);
Model._(
this.name, {
required String family,
Set<String> families = const {},
Set<ModelCapability> capabilities = const {},
}) : _family = family,
_families = families,
_capabilities = capabilities;
required this._modifiedAt,
required this._size,
required this._family,
this._families = const {},
required this._parameterSize,
}) : _capabilities = const {};
factory Model.fromApi({required ollama.Model model, ollama.ModelInfo? info}) {
factory Model.fromApi({required ollama.ModelSummary model}) {
return Model._(
model.model!,
modifiedAt:
DateTime.tryParse(model.modifiedAt!)?.toLocal() ??
DateTime.fromMillisecondsSinceEpoch(0),
size: model.size ?? 0,
family: model.details!.family!,
families: model.details!.families?.toSet() ?? {},
capabilities: info?.capabilities?.toSet() ?? {},
parameterSize: model.details!.parameterSize!.replaceAll(".0", ""),
);
}
bool _didCallUpdateData = false;
bool get didCallUpdateData => _didCallUpdateData;
Future<void> updateData() async {
var data = await clients.ollamaClient.showModelInfo(
request: ollama.ModelInfoRequest(model: name),
final data = await clients.ollamaClient.models.show(
request: ollama.ShowRequest(model: name),
);
// just in case, but should always be present
if (data.details != null) {
_family = data.details!.family!;
_families = data.details!.families?.toSet() ?? {};
_family = data.details?["family"] ?? _family;
_families = data.details?["families"] != null
? (data.details!["families"] as List).map((e) => e.toString()).toSet()
: _families;
_parameterSize = data.details?["parameterSize"] ?? _parameterSize;
}
_capabilities = data.capabilities!.toSet();
_capabilities = data.capabilities!.map(ModelCapability.new).toSet();
_didCallUpdateData = true;
notifyListeners();
}
@ -58,15 +140,15 @@ class Model extends ChangeNotifier {
// sending a [GenerateChatCompletionRequest] without [messages], which is
// required for loading, otherwise a message will be generated
var headers = <String, String>{
final headers = <String, String>{
"Content-Type": "application/json",
...Preferences.instance.hostHeaders,
};
var body = {"model": name, "keep_alive": Preferences.instance.keepAlive};
final body = {"model": name, "keep_alive": Preferences.instance.keepAlive};
await clients.httpClient
.post(
Uri.parse("${clients.ollamaClient.baseUrl}/api/generate"),
Uri.parse("${clients.ollamaClient.config.baseUrl}/api/generate"),
headers: headers,
body: jsonEncode(body),
)
@ -89,12 +171,16 @@ class ModelManager extends ChangeNotifier {
String? _currentModelName;
String? get currentModelName => _currentModelName;
set currentModelName(String? name) {
assert(!useModel, "Model if fixed to $fixedModel and cannot be changed.");
if (useModel) return;
if (name == _currentModelName) return;
_currentModelName = name;
Preferences.instance.model = name;
notifyListeners();
}
Model? get currentModel => _currentModelName == null
Model? get currentModel => _currentModelName == null || _models.isEmpty
? null
: _models.singleWhere((e) => e.name == _currentModelName);
set currentModel(Model? model) => currentModelName = model?.name;
@ -102,11 +188,23 @@ class ModelManager extends ChangeNotifier {
final Set<Model> _models = {};
Set<Model> get models => Set.unmodifiable(_models);
ModelManager._() : _currentModelName = Preferences.instance.model;
ModelManager._()
: _currentModelName = useModel ? fixedModel : Preferences.instance.model;
Future<void> loadModels({bool fetchCapabilitiesInBackground = true}) async {
var data = await clients.ollamaClient.listModels();
final data = await clients.ollamaClient.models.list().timeout(
TimeoutMultiplier.medium,
);
loadModelsRaw(
data: data,
fetchCapabilitiesInBackground: fetchCapabilitiesInBackground,
);
}
void loadModelsRaw({
required ollama.ListResponse data,
bool fetchCapabilitiesInBackground = true,
}) {
for (var model in _models) {
model.dispose();
}
@ -115,6 +213,10 @@ class ModelManager extends ChangeNotifier {
for (var model in data.models!) {
_models.add(Model.fromApi(model: model)..addListener(notifyListeners));
}
if (_currentModelName != null &&
!_models.any((e) => e.name == _currentModelName)) {
_currentModelName = null;
}
_initialized = true;
notifyListeners();
@ -123,60 +225,49 @@ class ModelManager extends ChangeNotifier {
compute((_) async {
for (var model in _instance.models) {
await model.updateData().catchError((_) {});
notifyListeners();
}
}, null);
}
}
}
// MARK: Model Set Dialog
String bytesToHumanReadable(int bytes, {String? locale}) {
const units = ["B", "KB", "MB", "GB", "TB"];
var value = bytes.toDouble();
var unitIndex = 0;
// class ModelSetDialog extends StatefulWidget {
// final bool contentOnly;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
final nf = NumberFormat.decimalPattern(locale)
..maximumFractionDigits = 2
..minimumFractionDigits = 0;
// const ModelSetDialog({super.key, this.contentOnly = false});
final separator =
RegExp(r"(\s)")
.firstMatch(
NumberFormat.simpleCurrency(locale: locale, name: "USD").format(0),
)
?.group(1) ??
"";
// @override
// State<ModelSetDialog> createState() => _ModelSetDialogState();
// }
return "${nf.format(value)}$separator${units[unitIndex]}";
}
// class _ModelSetDialogState extends State<ModelSetDialog> {
// @override
// void initState() {
// super.initState();
// ModelManager.instance.addListener(onChange);
// ModelManager.instance.loadModels();
// }
// @override
// void dispose() {
// ModelManager.instance.removeListener(onChange);
// super.dispose();
// }
// void onChange() {
// setState(() {});
// }
// @override
// Widget build(BuildContext context) {
// Widget main = Placeholder();
// if (widget.contentOnly) {
// return main;
// } else {
// return AlertDialog();
// }
// }
// }
// void showModelSetDialog(BuildContext context) {
// if (Display.of(context).isDesktop) {
// showModalBottomSheet(
// context: context,
// builder: (_) => const ModelSetDialog(contentOnly: true),
// );
// } else {
// showDialog(context: context, builder: (_) => const ModelSetDialog());
// }
// }
extension FilterSupportedCapabilities on Iterable<ModelCapability> {
Iterable<ModelCapability> filterSupportedCapabilities() => where(
(capability) => switch (capability.name) {
"completion" => false,
"tools" => false,
"insert" => false,
"vision" => true,
"embedding" => false,
"thinking" => true,
"image" => false,
"audio" => false,
_ => false,
},
);
}

View File

@ -14,12 +14,14 @@ class Preferences extends ChangeNotifier {
}
}
String? get host => prefs?.getString("host") ?? (useHost ? fixedHost : null);
set host(String? value) {
if (value == null || value.isEmpty) {
Uri? get host => Uri.tryParse(
prefs?.getString("host") ?? (useHost ? fixedHost : null) ?? "::",
);
set host(Uri? value) {
if (value == null) {
prefs?.remove("host");
} else {
prefs?.setString("host", value);
prefs?.setString("host", value.toString());
}
notifyListeners();
}
@ -41,6 +43,16 @@ class Preferences extends ChangeNotifier {
notifyListeners();
}
String? get user => prefs?.getString("user");
set user(String? value) {
if (value == null || value.isEmpty) {
prefs?.remove("user");
} else {
prefs?.setString("user", value);
}
notifyListeners();
}
bool get useSystem => prefs?.getBool("useSystem") ?? true;
set useSystem(bool value) {
prefs?.setBool("useSystem", value);
@ -48,7 +60,9 @@ class Preferences extends ChangeNotifier {
}
String? get system => useSystem
? prefs?.getString("system") ?? "You are a helpful assistant."
? prefs?.getString("system") ??
"You are a helpful assistant. Answer the user's questions to the best of your ability. If you do not know the answer, say that you do not know and do not fabricate information.\n\n"
"You may use Markdown to format your response. For math, you may use LaTeX with standard Markdown LaTeX syntax."
: null;
set system(String? value) {
if (value == null || value.isEmpty) {

View File

@ -3,20 +3,164 @@ import 'dart:io' as io;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Display {
late final Size size;
enum BreakpointNavigationType {
navigationBar,
navigationRail,
navigationRailExtended,
navigationRailModal,
}
Display.from(BuildContext context) {
size = MediaQuery.sizeOf(context);
enum Breakpoint {
compact(
null,
599,
panes: [1],
panesRecommended: 1,
navigation: [.navigationBar, .navigationRailModal],
fullScreenDialogRecommended: true,
spacing: 16,
),
medium(
600,
839,
panes: [1, 2],
panesRecommended: 1,
navigation: [.navigationBar, .navigationRail],
spacing: 24,
),
expanded(
840,
1199,
panes: [1, 2],
panesRecommended: 2,
panesFixedWidth: 360,
navigation: [.navigationRail, .navigationRailExtended],
spacing: 24,
),
large(
1200,
1599,
panes: [1, 2],
panesRecommended: 2,
panesFixedWidth: 412,
navigation: [.navigationRail, .navigationRailExtended],
spacing: 24,
),
extraLarge(
1600,
null,
panes: [1, 2, 3],
panesRecommended: 2,
panesFixedWidth: 412,
panesThirdFixedWidth: 400,
navigation: [.navigationRail, .navigationRailExtended],
spacing: 24,
);
final int? from;
final int? to;
/// List of possible pane numbers according to specification.
///
/// [panesRecommended] is always included in this list. It is the recommended
/// number of panes for this breakpoint. If not using the recommended number
/// of panes, good reasoning should be documented.
///
/// The first two panes can use either of the following layout types:
/// - Split-pane layout:
/// - The two panes are displayed side by side, with the same width. If the
/// navigation rail is used, it is counted into the 50 percent split of the
/// pane next to it, typically the first one.
/// - Fixed-and-flexible layout:
/// - One pane, typically the first one, has the fixed width of
/// [panesFixedWidth]. The other pane takes up the remaining space.
/// - This layout option is only supported if [panesFixedWidth] is not null.
///
/// All panes are separated by [spacing] and have that much space to the
/// screen edge as well.
///
/// The third pane of the [extraLarge] breakpoint is largely independent of
/// the rules for the first two panes. It must always have the width of
/// [panesThirdFixedWidth]. It is separated from the other panes by [spacing].
/// Stylistically, according to the specification, it should be a *standard
/// side sheet*.
final List<int> panes;
/// The number of panes that are recommended for this breakpoint.
final int panesRecommended;
/// The fixed width for the fixed pane in a fixed-and-flexible layout.
///
/// See the documentation for [panes] for more details on usage of this
/// property.
///
/// If this is null, a fixed-and-flexible layout is not supported for this
/// breakpoint.
final double? panesFixedWidth;
/// The fixed width for the third pane in the [extraLarge] breakpoint.
///
/// This is only used for the third pane in the [extraLarge] breakpoint. If a
/// third pane is used, the width of ot must always be set to this value.
///
/// The third pane is separated from the other panes by [spacing].
/// Stylistically, according to the specification, it should be a *standard
/// side sheet*.
final double? panesThirdFixedWidth;
/// The navigation types that are recommended for this breakpoint.
final List<BreakpointNavigationType> navigation;
/// Whether a full screen dialog is recommended for this breakpoint.
///
/// If this is false, using a normal dialog is recommended instead. Normal
/// dialogs can still be used for this breakpoint if this is true.
final bool fullScreenDialogRecommended;
/// The spacing between panes, and between panes and the screen edge.
///
/// This is the spacing that is recommended for this breakpoint.
///
/// Please note that the Material specification also includes a complicated
/// set of guidelines for spacing using rulers. Usually these can be sidelined
/// in favor of a single spacing value that is used everywhere. Read more
/// here: https://m3.material.io/foundations/layout/grids-spacing/grids
final double spacing;
const Breakpoint(
this.from,
this.to, {
required this.panes,
required this.panesRecommended,
this.panesFixedWidth,
this.panesThirdFixedWidth,
required this.navigation,
this.fullScreenDialogRecommended = false,
required this.spacing,
});
/// Returns the [Breakpoint] for the given [BuildContext].
static Breakpoint of(BuildContext context) {
final width = MediaQuery.sizeOf(context).width.ceil();
return Breakpoint.values.firstWhere(
(breakpoint) =>
width >= (breakpoint.from ?? double.negativeInfinity) &&
width <= (breakpoint.to ?? double.infinity),
);
}
bool get isMobile => size.width < 600;
bool operator >(Breakpoint b) =>
(to ?? double.infinity) > (b.from ?? double.negativeInfinity);
bool operator <(Breakpoint b) =>
(from ?? double.negativeInfinity) < (b.to ?? double.infinity);
bool operator >=(Breakpoint b) => this > b || this == b;
bool operator <=(Breakpoint b) => this < b || this == b;
bool get isTabletOrLess => size.width < 1200;
bool get isTablet => size.width >= 600 && size.width < 1200;
bool get isTabletOrMore => size.width >= 600;
bool get isDesktop => size.width >= 1200;
@override
String toString() =>
"Breakpoint.$name(from: $from, to: $to, panes: $panes, panesRecommended: $panesRecommended, navigation: $navigation, fullScreenDialogRecommended: $fullScreenDialogRecommended, spacing: $spacing)";
String toPrettyString() =>
"Breakpoint.$name(\n\tfrom: $from,\n\tto: $to,\n\tpanes: $panes,\n\tpanesRecommended: $panesRecommended,\n\tnavigation: [${navigation.map((n) => n.name).join(", ")}],\n\tfullScreenDialogRecommended: $fullScreenDialogRecommended,\n\tspacing: $spacing\n)";
}
class LayoutFeature {

View File

@ -0,0 +1,12 @@
export 'chat.dart';
export 'chat_database/chat_database.dart' show ChatDatabase, chatDb;
export 'clients.dart';
export 'curves.dart';
export 'error.dart';
export 'haptic.dart';
export 'host.dart';
export 'markdown.dart';
export 'model.dart';
export 'preferences.dart';
export 'responsive.dart';
export 'theme.dart';

View File

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../l10n/gen/app_localizations.dart';
@ -23,24 +24,32 @@ class ThemeBuilderData {
}
ThemeData themeModifier(BuildContext context, ThemeData theme) {
var isMobile = Display.from(context).isMobile;
final floatingSnackbar = Breakpoint.of(context) <= Breakpoint.medium;
return theme.copyWith(
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
builders: {
TargetPlatform.android: FadeForwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: FadeForwardsPageTransitionsBuilder(),
TargetPlatform.linux: FadeForwardsPageTransitionsBuilder(),
},
),
sliderTheme: theme.sliderTheme.copyWith(year2023: false),
progressIndicatorTheme: theme.progressIndicatorTheme.copyWith(
year2023: false,
),
dividerTheme: theme.dividerTheme.copyWith(
color: theme.colorScheme.outline,
),
snackBarTheme: theme.snackBarTheme.copyWith(
behavior: isMobile ? null : SnackBarBehavior.floating,
width: isMobile ? null : 288,
behavior: SnackBarBehavior.floating,
width: floatingSnackbar ? null : 288,
),
listTileTheme: theme.listTileTheme.copyWith(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
@ -48,10 +57,16 @@ class ThemeBuilderData {
if (Preferences.instance.themeSystem && dynamicLight != null) {
return ThemeData.from(colorScheme: dynamicLight!);
} else {
final colorScheme = ColorScheme.fromSeed(
seedColor: Colors.black,
dynamicSchemeVariant: DynamicSchemeVariant.content,
);
return ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.black,
dynamicSchemeVariant: DynamicSchemeVariant.content,
colorScheme: colorScheme.copyWith(
surface: Colors.white,
onPrimaryContainer: colorScheme.onPrimary,
onSecondaryContainer: colorScheme.onSurface,
onTertiaryContainer: colorScheme.onTertiary,
),
);
}
@ -61,12 +76,18 @@ class ThemeBuilderData {
if (Preferences.instance.themeSystem && dynamicDark != null) {
return ThemeData.from(colorScheme: dynamicDark!);
} else {
final colorScheme = ColorScheme.fromSeed(
seedColor: Colors.white,
dynamicSchemeVariant: DynamicSchemeVariant.content,
brightness: Brightness.dark,
);
return ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.white,
dynamicSchemeVariant: DynamicSchemeVariant.content,
brightness: Brightness.dark,
).copyWith(surface: Colors.black),
colorScheme: colorScheme.copyWith(
surface: Colors.black,
onPrimaryContainer: colorScheme.onPrimary,
onSecondaryContainer: colorScheme.onSurface,
onTertiaryContainer: colorScheme.onTertiary,
),
).copyWith(dividerColor: Colors.white30);
}
}
@ -86,7 +107,9 @@ class _ThemeBuilderState extends State<ThemeBuilder> {
@override
void initState() {
super.initState();
Preferences.instance.addListener(onChange);
WidgetsBinding.instance.addPostFrameCallback(
(_) => Preferences.instance.addListener(onChange),
);
}
@override

26
lib/util/extensions.dart Normal file
View File

@ -0,0 +1,26 @@
extension StringExtension on String {
String toTitleCase() {
if (isEmpty) return this;
return split(" ")
.map(
(word) => word.isEmpty
? word
: "${word[0].toUpperCase()}${word.substring(1)}",
)
.join(" ");
}
String emptyOn(String other) => (this == other) ? "" : this;
}
extension IterableExtension<T> on Iterable<T> {
Iterable<T> joinElements(T separator) {
if (length <= 1) return this;
final result = <T>[];
for (var i = 0; i < length; i++) {
result.add(elementAt(i));
if (i < length - 1) result.add(separator);
}
return result;
}
}

View File

@ -0,0 +1,344 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// A widget that implements the Material Design alert dialog layout structure.
///
/// The handling is identical to [AlertDialog], but instead of returning a
/// [Dialog], it returns a [Widget] that can be used as a child of a [Dialog]:
///
/// ```dart
/// Dialog(
/// semanticsRole: SemanticsRole.alertDialog,
/// child: AlertDialogChild(...),
/// );
/// ```
///
/// The code above would be equivalent to using [AlertDialog] directly.
/// Dialog-level properties ([backgroundColor], [elevation], [shadowColor],
/// [surfaceTintColor], [insetPadding], [clipBehavior], [shape], [alignment],
/// [constraints]) are not accepted by this widget and must be passed directly
/// to the [Dialog] wrapper instead.
class AlertDialogChild extends StatelessWidget {
const AlertDialogChild({
super.key,
this.icon,
this.iconPadding,
this.iconColor,
this.title,
this.titlePadding,
this.titleTextStyle,
this.content,
this.contentPadding,
this.contentTextStyle,
this.actions,
this.actionsPadding,
this.actionsAlignment,
this.actionsOverflowAlignment,
this.actionsOverflowDirection,
this.actionsOverflowButtonSpacing,
this.buttonPadding,
this.semanticLabel,
this.scrollable = false,
});
final Widget? icon;
final Color? iconColor;
final EdgeInsetsGeometry? iconPadding;
final Widget? title;
final EdgeInsetsGeometry? titlePadding;
final TextStyle? titleTextStyle;
final Widget? content;
final EdgeInsetsGeometry? contentPadding;
final TextStyle? contentTextStyle;
final List<Widget>? actions;
final EdgeInsetsGeometry? actionsPadding;
final MainAxisAlignment? actionsAlignment;
final OverflowBarAlignment? actionsOverflowAlignment;
final VerticalDirection? actionsOverflowDirection;
final double? actionsOverflowButtonSpacing;
final EdgeInsetsGeometry? buttonPadding;
final String? semanticLabel;
final bool scrollable;
@override
Widget build(BuildContext context) {
assert(
debugCheckHasMaterialLocalizations(context),
"No MaterialLocalizations found.",
);
final theme = Theme.of(context);
final dialogTheme = DialogTheme.of(context);
final defaults = theme.useMaterial3
? _DialogDefaultsM3(context)
: _DialogDefaultsM2(context);
final label = switch (defaultTargetPlatform) {
TargetPlatform.iOS || TargetPlatform.macOS => semanticLabel,
TargetPlatform.android ||
TargetPlatform.fuchsia ||
TargetPlatform.linux ||
TargetPlatform.windows =>
semanticLabel ?? MaterialLocalizations.of(context).alertDialogLabel,
};
// The paddingScaleFactor is used to adjust the padding of Dialog's
// children.
const fontSizeToScale = 14.0;
final effectiveTextScale =
MediaQuery.textScalerOf(context).scale(fontSizeToScale) /
fontSizeToScale;
final paddingScaleFactor = _scalePadding(effectiveTextScale);
final textDirection = Directionality.maybeOf(context);
Widget? iconWidget;
Widget? titleWidget;
Widget? contentWidget;
Widget? actionsWidget;
if (icon != null) {
final belowIsTitle = title != null;
final belowIsContent = !belowIsTitle && content != null;
final defaultIconPadding = EdgeInsets.only(
left: 24.0,
top: 24.0,
right: 24.0,
bottom: belowIsTitle
? 16.0
: belowIsContent
? 0.0
: 24.0,
);
final effectiveIconPadding =
iconPadding?.resolve(textDirection) ?? defaultIconPadding;
iconWidget = Padding(
padding: EdgeInsets.only(
left: effectiveIconPadding.left * paddingScaleFactor,
right: effectiveIconPadding.right * paddingScaleFactor,
top: effectiveIconPadding.top * paddingScaleFactor,
bottom: effectiveIconPadding.bottom,
),
child: IconTheme(
data: IconThemeData(
color: iconColor ?? dialogTheme.iconColor ?? defaults.iconColor,
),
child: icon!,
),
);
}
if (title != null) {
final defaultTitlePadding = EdgeInsets.only(
left: 24.0,
top: icon == null ? 24.0 : 0.0,
right: 24.0,
bottom: content == null ? 20.0 : 0.0,
);
final effectiveTitlePadding =
titlePadding?.resolve(textDirection) ?? defaultTitlePadding;
titleWidget = Padding(
padding: EdgeInsets.only(
left: effectiveTitlePadding.left * paddingScaleFactor,
right: effectiveTitlePadding.right * paddingScaleFactor,
top: icon == null
? effectiveTitlePadding.top * paddingScaleFactor
: effectiveTitlePadding.top,
bottom: effectiveTitlePadding.bottom,
),
child: DefaultTextStyle(
style:
titleTextStyle ??
dialogTheme.titleTextStyle ??
defaults.titleTextStyle!,
textAlign: icon == null ? TextAlign.start : TextAlign.center,
child: Semantics(
// For iOS platform, the focus always lands on the title.
// Set nameRoute to false to avoid title being announce twice.
namesRoute:
label == null && defaultTargetPlatform != TargetPlatform.iOS,
container: true,
child: title,
),
),
);
}
if (content != null) {
final defaultContentPadding = EdgeInsets.only(
left: 24.0,
top: theme.useMaterial3 ? 16.0 : 20.0,
right: 24.0,
bottom: 24.0,
);
final effectiveContentPadding =
contentPadding?.resolve(textDirection) ?? defaultContentPadding;
contentWidget = Padding(
padding: EdgeInsets.only(
left: effectiveContentPadding.left * paddingScaleFactor,
right: effectiveContentPadding.right * paddingScaleFactor,
top: title == null && icon == null
? effectiveContentPadding.top * paddingScaleFactor
: effectiveContentPadding.top,
bottom: effectiveContentPadding.bottom,
),
child: DefaultTextStyle(
style:
contentTextStyle ??
dialogTheme.contentTextStyle ??
defaults.contentTextStyle!,
child: Semantics(
container: true,
explicitChildNodes: true,
child: content,
),
),
);
}
if (actions != null) {
final spacing = (buttonPadding?.horizontal ?? 16) / 2;
actionsWidget = Padding(
padding:
actionsPadding ??
dialogTheme.actionsPadding ??
(theme.useMaterial3
? defaults.actionsPadding!
: defaults.actionsPadding!.add(EdgeInsets.all(spacing))),
child: OverflowBar(
alignment: actionsAlignment ?? MainAxisAlignment.end,
spacing: spacing,
overflowAlignment:
actionsOverflowAlignment ?? OverflowBarAlignment.end,
overflowDirection: actionsOverflowDirection ?? VerticalDirection.down,
overflowSpacing: actionsOverflowButtonSpacing ?? 0,
children: actions!,
),
);
}
List<Widget> columnChildren;
if (scrollable) {
columnChildren = <Widget>[
if (title != null || content != null)
Flexible(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[?iconWidget, ?titleWidget, ?contentWidget],
),
),
),
?actionsWidget,
];
} else {
columnChildren = <Widget>[
?iconWidget,
?titleWidget,
if (contentWidget != null) Flexible(child: contentWidget),
?actionsWidget,
];
}
Widget dialogChild = IntrinsicWidth(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: columnChildren,
),
);
if (label != null) {
dialogChild = Semantics(
scopesRoute: true,
explicitChildNodes: true,
namesRoute: true,
label: label,
child: dialogChild,
);
}
return dialogChild;
}
}
class _DialogDefaultsM2 extends DialogThemeData {
_DialogDefaultsM2(this.context)
: super(
alignment: Alignment.center,
elevation: 24.0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
clipBehavior: Clip.none,
);
final BuildContext context;
late final ThemeData theme = Theme.of(context);
late final TextTheme textTheme = theme.textTheme;
late final IconThemeData iconTheme = theme.iconTheme;
@override
Color? get iconColor => iconTheme.color;
@override
Color? get backgroundColor =>
theme.brightness == Brightness.dark ? Colors.grey[800]! : Colors.white;
@override
Color? get shadowColor => theme.shadowColor;
@override
TextStyle? get titleTextStyle => textTheme.titleLarge;
@override
TextStyle? get contentTextStyle => textTheme.titleMedium;
@override
EdgeInsetsGeometry? get actionsPadding => EdgeInsets.zero;
}
class _DialogDefaultsM3 extends DialogThemeData {
_DialogDefaultsM3(this.context)
: super(
alignment: Alignment.center,
elevation: 6.0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(28.0)),
),
clipBehavior: Clip.none,
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
@override
Color? get iconColor => _colors.secondary;
@override
Color? get backgroundColor => _colors.surfaceContainerHigh;
@override
Color? get shadowColor => Colors.transparent;
@override
Color? get surfaceTintColor => Colors.transparent;
@override
TextStyle? get titleTextStyle => _textTheme.headlineSmall;
@override
TextStyle? get contentTextStyle => _textTheme.bodyMedium;
@override
EdgeInsetsGeometry? get actionsPadding =>
const EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0);
}
double _scalePadding(double textScaleFactor) {
final clampedTextScaleFactor = clampDouble(textScaleFactor, 1.0, 2.0);
return lerpDouble(1.0, 1.0 / 3.0, clampedTextScaleFactor - 1.0)!;
}

107
lib/widgets/alpha_beta.dart Normal file
View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import '../l10n/gen/app_localizations.dart';
class AlphaBadge extends StatelessWidget {
const AlphaBadge({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
final textTheme = TextTheme.of(context);
final appLocalizations = AppLocalizations.of(context);
return Container(
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: colorScheme.errorContainer,
shape: const StadiumBorder(),
),
padding: const EdgeInsets.symmetric(horizontal: 4),
alignment: Alignment.center,
child: Text(
appLocalizations.settingsExperimentalAlpha,
style: textTheme.labelSmall!.copyWith(
color: colorScheme.onErrorContainer,
),
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
}
}
class Alpha extends StatelessWidget {
final Widget child;
final bool showBadge;
const Alpha({super.key, required this.child, this.showBadge = true});
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: child),
if (showBadge) ...[
const SizedBox(width: 2),
Transform.translate(
offset: const Offset(0, -4),
child: const AlphaBadge(),
),
],
],
);
}
class BetaBadge extends StatelessWidget {
const BetaBadge({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
final textTheme = TextTheme.of(context);
final appLocalizations = AppLocalizations.of(context);
return Container(
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: colorScheme.primaryContainer,
shape: const StadiumBorder(),
),
padding: const EdgeInsets.symmetric(horizontal: 4),
alignment: Alignment.center,
child: Text(
appLocalizations.settingsExperimentalBeta,
style: textTheme.labelSmall!.copyWith(
color: colorScheme.onPrimaryContainer,
),
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
}
}
class Beta extends StatelessWidget {
final Widget child;
final bool showBadge;
const Beta({super.key, required this.child, this.showBadge = true});
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: child),
if (showBadge) ...[
const SizedBox(width: 2),
Transform.translate(
offset: const Offset(0, -4),
child: const BetaBadge(),
),
],
],
);
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class MeasureSize extends SingleChildRenderObjectWidget {
final void Function(Size size) onChange;
const MeasureSize({super.key, required this.onChange, required super.child});
@override
RenderObject createRenderObject(BuildContext context) =>
MeasureSizeRenderObject(onChange);
@override
void updateRenderObject(
BuildContext context,
MeasureSizeRenderObject renderObject,
) => renderObject.onChange = onChange;
}
class MeasureSizeRenderObject extends RenderProxyBox {
ValueChanged<Size> onChange;
Size? _oldSize;
MeasureSizeRenderObject(this.onChange);
@override
void performLayout() {
super.performLayout();
final newSize = child?.size ?? Size.zero;
if (_oldSize != newSize) {
_oldSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) => onChange(newSize));
}
}
}

View File

@ -22,13 +22,13 @@ Widget heroTextFlightShuttleBuilder(
(begin, end) = (end, begin);
}
var styleTween = TextStyleTween(begin: begin, end: end);
var textString = fromText.data ?? toText.data ?? "";
final styleTween = TextStyleTween(begin: begin, end: end);
final textString = fromText.data ?? toText.data ?? "";
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
var currentStyle = styleTween.lerp(animation.value);
final currentStyle = styleTween.lerp(animation.value);
return Material(
type: MaterialType.transparency,
child: Center(

View File

@ -62,7 +62,7 @@ class ListTileSlide extends StatelessWidget {
@override
Widget build(BuildContext context) {
var content = Column(
final content = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -87,9 +87,9 @@ class ListTileSlide extends StatelessWidget {
],
);
var theme = Theme.of(context);
var switchTheme = SliderTheme.of(context);
var effectiveActiveColor =
final theme = Theme.of(context);
final switchTheme = SliderTheme.of(context);
final effectiveActiveColor =
thumbColor ?? switchTheme.thumbColor ?? theme.colorScheme.secondary;
return MergeSemantics(

View File

@ -85,13 +85,13 @@ class ListTileSwitchInteractive extends StatelessWidget {
@override
Widget build(BuildContext context) {
var listTileTheme = ListTileTheme.of(context);
var effectiveControlAffinity =
final listTileTheme = ListTileTheme.of(context);
final effectiveControlAffinity =
controlAffinity ??
listTileTheme.controlAffinity ??
ListTileControlAffinity.platform;
var controlChildren = [
final controlChildren = [
const SizedBox(height: 32, child: VerticalDivider()),
const SizedBox(width: 8),
Switch(
@ -126,7 +126,7 @@ class ListTileSwitchInteractive extends StatelessWidget {
overlayColor: overlayColor,
),
];
var control = ExcludeFocus(
final control = ExcludeFocus(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
@ -144,15 +144,15 @@ class ListTileSwitchInteractive extends StatelessWidget {
ListTileControlAffinity.platform => (secondary, control),
};
var theme = Theme.of(context);
var switchTheme = SwitchTheme.of(context);
var states = <WidgetState>{if (selected) WidgetState.selected};
var effectiveActiveColor =
final theme = Theme.of(context);
final switchTheme = SwitchTheme.of(context);
final states = <WidgetState>{if (selected) WidgetState.selected};
final effectiveActiveColor =
activeThumbColor ??
switchTheme.thumbColor?.resolve(states) ??
theme.colorScheme.secondary;
var effectiveContentPadding =
final effectiveContentPadding =
contentPadding ??
EdgeInsets.only(
left: effectiveControlAffinity == ListTileControlAffinity.leading
@ -273,13 +273,13 @@ class ListTileSwitch extends StatelessWidget {
@override
Widget build(BuildContext context) {
var listTileTheme = ListTileTheme.of(context);
var effectiveControlAffinity =
final listTileTheme = ListTileTheme.of(context);
final effectiveControlAffinity =
controlAffinity ??
listTileTheme.controlAffinity ??
ListTileControlAffinity.platform;
var control = ExcludeFocus(
final control = ExcludeFocus(
child: IgnorePointer(
child: Switch(
value: value,
@ -316,15 +316,15 @@ class ListTileSwitch extends StatelessWidget {
ListTileControlAffinity.platform => (secondary, control),
};
var theme = Theme.of(context);
var switchTheme = SwitchTheme.of(context);
var states = <WidgetState>{if (selected) WidgetState.selected};
var effectiveActiveColor =
final theme = Theme.of(context);
final switchTheme = SwitchTheme.of(context);
final states = <WidgetState>{if (selected) WidgetState.selected};
final effectiveActiveColor =
activeThumbColor ??
switchTheme.thumbColor?.resolve(states) ??
theme.colorScheme.secondary;
var effectiveContentPadding =
final effectiveContentPadding =
contentPadding ??
EdgeInsets.only(
left: effectiveControlAffinity == ListTileControlAffinity.leading

View File

@ -0,0 +1,551 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:ollama_dart/ollama_dart.dart' as ollama;
import 'package:url_launcher/url_launcher.dart';
import '../l10n/gen/app_localizations.dart';
import '../services/services.dart';
import 'alert_dialog_child.dart';
final modelTagRegex = RegExp(
r"^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\[(?:[a-fA-F0-9:]+)\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\w][\w.-]{0,127}))?$",
);
Map<String, StreamSubscription<ollama.StatusEvent>> _downloadingModels = {};
Map<String, _DownloadingModelsState> _downloadingModelsState = {};
class _DownloadingModelsState with ChangeNotifier {
double? progress;
int? total;
bool completedType;
bool? pageState;
bool cancelFlag;
bool cancelledType;
bool errorNotFoundType;
bool errorNetworkType;
bool errorCorruptionType;
void _notify() => notifyListeners();
_DownloadingModelsState()
: progress = null,
total = null,
completedType = false,
pageState = null,
cancelFlag = false,
cancelledType = false,
errorNotFoundType = false,
errorNetworkType = false,
errorCorruptionType = false;
}
class ModelAdder extends StatefulWidget {
const ModelAdder({super.key});
@override
State<ModelAdder> createState() => _ModelAdderState();
}
class _ModelAdderState extends State<ModelAdder> {
bool? get _pageState =>
modelName != null ? _downloadingModelsState[modelName]?.pageState : false;
set _pageState(bool? value) => modelName != null
? _downloadingModelsState[modelName]!.pageState = value
: null;
bool _wantPop = false;
final _controller = TextEditingController();
final _focusNode = FocusNode();
String? Function() _errorText = () => null;
bool _errorIsCorruption = false;
String? modelName;
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
_downloadingModelsState.forEach(
(_, state) => state.removeListener(onUpdate),
);
super.dispose();
}
Future<void> _downloadModel([String? model]) async {
var modelName = model ?? _controller.text.trim();
if (modelName.isEmpty) {
_focusNode.requestFocus();
return;
}
if (!modelName.contains(":")) {
modelName = "$modelName:latest";
}
_errorText = () => null;
_errorIsCorruption = false;
if (mounted) setState(() {});
if (ModelManager.instance.models.any((model) => model.name == modelName)) {
_errorText = () =>
AppLocalizations.of(context).modelDialogAddPromptAlreadyExists;
if (mounted) setState(() {});
_focusNode.requestFocus();
return;
}
if (!modelTagRegex.hasMatch(modelName)) {
_errorText = () =>
AppLocalizations.of(context).modelDialogAddPromptInvalid;
if (mounted) setState(() {});
_focusNode.requestFocus();
return;
}
_pageState = null;
if (mounted) setState(() {});
try {
_downloadingModelsState
.putIfAbsent(modelName, _DownloadingModelsState.new)
.addListener(onUpdate);
this.modelName = modelName;
var sub = _downloadingModels[modelName];
sub ??= ollamaClient.models
.pullStream(
request: ollama.PullRequest(model: modelName, stream: true),
)
.listen(
(event) {
final state = _downloadingModelsState[modelName]!;
if (state.cancelFlag) {
state
..cancelledType = true
.._notify();
this.modelName = null;
_downloadingModels.remove(modelName);
_downloadingModelsState.remove(modelName);
sub?.cancel();
return;
} else if (event.status == "success") {
state
..completedType = true
.._notify();
this.modelName = null;
_downloadingModels.remove(modelName);
_downloadingModelsState.remove(modelName);
return;
}
if ((event.total ?? 0) == 0 && (event.progress ?? 0) == 0) return;
_pageState = true;
if ((event.progress ?? -1) > (state.progress ?? -1)) {
state.progress = event.progress;
}
if ((event.total ?? -1) > (state.total ?? -1)) {
state.total = event.total;
}
if (!_downloadingModels.containsKey(modelName)) {
_downloadingModels[modelName] = sub!;
}
state._notify();
},
onError: (Object error) {
final state = _downloadingModelsState[modelName];
if (error is ollama.StreamException) {
if (error.message.contains("EOF")) {
state?.errorCorruptionType = true;
} else {
state?.errorNotFoundType = true;
}
} else {
state?.errorNetworkType = true;
}
state?._notify();
this.modelName = null;
_downloadingModels.remove(modelName);
_downloadingModelsState.remove(modelName);
sub?.cancel();
},
);
} catch (_) {
if (!mounted) return;
_downloadingModelsState[modelName]
?..errorNetworkType = true
.._notify();
_downloadingModels.remove(modelName);
_downloadingModelsState.remove(modelName);
if (mounted) {
setState(() {});
_focusNode.requestFocus();
}
}
}
void onUpdate() {
final state = modelName != null ? _downloadingModelsState[modelName] : null;
if (state == null || !mounted) return;
if (state.completedType || state.cancelledType) {
final modelName = state.completedType ? this.modelName : null;
ModelManager.instance.loadModels().then((_) {
if (mounted) Navigator.of(context).pop(modelName);
});
return;
} else if (state.errorNotFoundType ||
state.errorNetworkType ||
state.errorCorruptionType) {
_errorText = () {
if (state.errorNotFoundType) {
return AppLocalizations.of(context).modelDialogAddPromptNotFound;
} else if (state.errorNetworkType) {
return AppLocalizations.of(context).modelDialogAddPromptNetworkError;
} else if (state.errorCorruptionType) {
_errorIsCorruption = true;
return AppLocalizations.of(
context,
).modelDialogAddPromptCorruptionError;
}
return null;
};
if (mounted) setState(() {});
_focusNode.requestFocus();
}
setState(() {});
}
@override
Widget build(BuildContext context) {
final state = modelName != null ? _downloadingModelsState[modelName] : null;
final colorScheme = ColorScheme.of(context);
final textTheme = TextTheme.of(context);
final appLocalizations = AppLocalizations.of(context);
final breakpoint = Breakpoint.of(context);
final embeddedNavigation = breakpoint.panesRecommended >= 2;
final alignment = !embeddedNavigation
? AlignmentDirectional.bottomCenter
: AlignmentDirectional.topStart;
const constraints = BoxConstraints(minWidth: 280, maxWidth: 360);
final childInput = AlertDialogChild(
key: const ValueKey("input"),
title: Text(appLocalizations.modelDialogAddPromptTitle),
content: SizedBox(
width: 560,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(appLocalizations.modelDialogAddPromptDescription),
const SizedBox(height: 12),
TextField(
controller: _controller,
focusNode: _focusNode,
autofocus: true,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.done,
maxLines: 1,
onSubmitted: (_) => _downloadModel(),
decoration: InputDecoration(
hintText: "gemma4:latest",
errorText: _errorText.call(),
errorMaxLines: 4,
),
),
if (_errorIsCorruption) ...[
ListTile(
contentPadding: EdgeInsets.zero,
minTileHeight: 0,
title: Text.rich(
TextSpan(
style: TextStyle(color: colorScheme.error),
children: [
TextSpan(text: appLocalizations.learnMore),
const TextSpan(text: " "),
WidgetSpan(
child: Builder(
builder: (context) => Transform.translate(
offset: const Offset(0, -1),
child: Icon(
Icons.open_in_new,
size: DefaultTextStyle.of(context).style.fontSize,
color: colorScheme.error,
),
),
),
),
],
),
),
onTap: () => launchUrl(
webOnlyWindowName: "_blank",
Uri.parse(
"https://github.com/ollama/ollama/issues/8088#issuecomment-2541889671",
),
),
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: appLocalizations
.modelDialogAddPromptCorruptionErrorSolution
.split("\n")
.first,
),
const TextSpan(text: "\n"),
TextSpan(
text: r"$ rm -f $OLLAMA_MODELS/blobs/sha256-*partial*",
style: textTheme.labelMedium?.copyWith(
fontFamily: "GoogleSansCode",
color: colorScheme.error,
),
),
const TextSpan(text: "\n"),
TextSpan(
text: appLocalizations
.modelDialogAddPromptCorruptionErrorSolution
.split("\n")
.last,
),
],
style: textTheme.labelMedium?.copyWith(
color: colorScheme.error,
),
),
),
],
if (_downloadingModels.isNotEmpty) ...[
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: Card.outlined(
margin: EdgeInsets.zero,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsetsDirectional.only(
top: 6,
start: 8,
),
child: Text(appLocalizations.modelDialogAddCurrently),
),
Padding(
padding: const EdgeInsetsDirectional.only(
top: 4,
bottom: 8,
start: 8,
end: 8,
),
child: Wrap(
spacing: 4,
runSpacing: 4,
children: _downloadingModels.keys
.map(
(modelName) => ActionChip(
label: Text(modelName),
onPressed: () => _downloadModel(modelName),
),
)
.toList(),
),
),
],
),
),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(appLocalizations.modelDialogAddCancelConfirm),
),
TextButton(
onPressed: _downloadModel,
child: Text(appLocalizations.modelDialogAdd),
),
],
);
const childLoading = SizedBox(
key: ValueKey("loading"),
width: 560,
child: Padding(
padding: EdgeInsets.all(24),
child: LinearProgressIndicator(),
),
);
final childDownload = Builder(
builder: (context) {
final closeInput = [
if (embeddedNavigation) const Divider(height: 1),
Padding(
padding: const EdgeInsetsDirectional.only(
top: 24,
bottom: 4,
start: 24,
end: 24,
),
child: Text(appLocalizations.modelDialogAddCancelTitle),
),
Padding(
padding: const EdgeInsetsDirectional.only(
bottom: 24,
start: 24,
end: 24,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton(
onPressed: () async => state!.cancelFlag = true,
style: FilledButton.styleFrom(
backgroundColor: colorScheme.error,
),
child: Text(appLocalizations.modelDialogAddCancelConfirm),
),
const SizedBox(width: 4),
OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(appLocalizations.modelDialogAddCancelHide),
),
],
),
),
if (!embeddedNavigation) const Divider(height: 1),
];
return SizedBox(
key: const ValueKey("download"),
width: 560,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_wantPop && !embeddedNavigation) ...closeInput,
Padding(
padding: const EdgeInsets.all(24),
child: ListTile(
minTileHeight: 0,
minVerticalPadding: 0,
contentPadding: EdgeInsets.zero,
title: Builder(
builder: (context) => Text(
modelName ?? appLocalizations.noSelectedModel,
style: DefaultTextStyle.of(
context,
).style.copyWith(fontFamily: "GoogleSansCode"),
),
),
subtitle: Builder(
builder: (context) => Wrap(
children: [
Text(
NumberFormat.decimalPercentPattern(
locale: appLocalizations.localeName,
decimalDigits: 2,
).format(state?.progress ?? 0),
style: DefaultTextStyle.of(context).style.copyWith(
fontFeatures: const [FontFeature.tabularFigures()],
),
),
Text(" " * 3),
const Text("/"),
Text(" " * 3),
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Transform.translate(
offset: const Offset(0, -1),
child: Icon(
Icons.sd_storage_outlined,
size: DefaultTextStyle.of(
context,
).style.fontSize,
),
),
),
const TextSpan(text: " "),
TextSpan(
text: bytesToHumanReadable(
state?.total ?? 0,
locale: appLocalizations.localeName,
),
),
],
),
),
],
),
),
trailing: CircularProgressIndicator(
value: state?.progress ?? 0,
),
),
),
if (_wantPop && embeddedNavigation) ...closeInput,
],
),
);
},
);
return PopScope(
canPop: _pageState == false,
onPopInvokedWithResult: (didPop, result) {
if (_pageState != true || _wantPop) return;
_wantPop = true;
if (mounted) setState(() {});
},
child: Dialog(
alignment: alignment,
constraints: constraints,
insetPadding: const EdgeInsets.all(12), // 24
child: AnimatedSize(
duration: ExpressiveCurves.expressiveSpatial.normalDuration,
curve: ExpressiveCurves.expressiveSpatial.normal,
alignment: embeddedNavigation
? AlignmentGeometry.topCenter
: AlignmentGeometry.bottomCenter,
child: AnimatedSwitcher(
duration: ExpressiveCurves.expressiveSpatial.normalDuration,
switchInCurve: ExpressiveCurves.expressiveSpatial.normal,
switchOutCurve: ExpressiveCurves.expressiveEffects.slow.flipped,
transitionBuilder: (child, animation) => SizeTransition(
sizeFactor: animation,
alignment: embeddedNavigation
? AlignmentDirectional.bottomCenter
: AlignmentDirectional.topCenter,
child: FadeTransition(opacity: animation, child: child),
),
child: _pageState == null
? childLoading
: (_pageState! ? childDownload : childInput),
),
),
),
);
}
}
Future<String?> showModelAdderDialog(BuildContext context) async =>
showDialog<String>(
context: context,
builder: (context) => const ModelAdder(),
);

View File

@ -0,0 +1,454 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get_time_ago/get_time_ago.dart';
import 'package:intl/intl.dart' as intl;
import '../l10n/gen/app_localizations.dart';
import '../services/services.dart';
import '../util/extensions.dart';
import 'model_adder.dart';
import 'sized_circular_progress_indicator.dart';
import 'two_state_widget.dart';
// MARK: Model Selector Dialog
class _ModelSelectorDialog extends StatelessWidget {
final GlobalKey? anchorKey;
const _ModelSelectorDialog({this.anchorKey});
@override
Widget build(BuildContext context) {
final breakpoint = Breakpoint.of(context);
final embeddedNavigation = breakpoint.panesRecommended >= 2;
if (!embeddedNavigation) {
return const Dialog.fullscreen(
child: ModelSelector(embeddedNavigation: false),
);
}
var anchorRect = Rect.zero;
final renderBox = anchorKey?.currentContext?.findRenderObject();
if (renderBox is RenderBox) {
anchorRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;
}
final screenSize = MediaQuery.sizeOf(context);
final isRtl = Directionality.of(context) == TextDirection.rtl;
const minWidth = 280.0;
const maxWidth = 560.0;
const edgeGap = 24.0;
final anchorEdge = isRtl
? screenSize.width - anchorRect.right
: anchorRect.left;
final position = anchorEdge
.clamp(0.0, max(0.0, screenSize.width - maxWidth))
.toDouble();
final effectiveMaxWidth = (screenSize.width - position - edgeGap).clamp(
minWidth,
maxWidth,
);
final top = anchorRect.bottom - 24;
final popup = ConstrainedBox(
constraints: BoxConstraints(
minWidth: minWidth,
maxWidth: effectiveMaxWidth,
maxHeight: screenSize.height - top - edgeGap,
),
child: Material(
type: MaterialType.card,
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
child: const ModelSelector(embeddedNavigation: true),
),
);
return Stack(
children: [
if (isRtl)
Positioned(right: position, top: top, child: popup)
else
Positioned(left: position, top: top, child: popup),
],
);
}
}
Future<void> showModelSelector({
required BuildContext context,
required GlobalKey? anchorKey,
}) => showDialog(
context: context,
barrierDismissible: true,
barrierColor: Colors.transparent,
builder: (context) => _ModelSelectorDialog(anchorKey: anchorKey),
);
// MARK: Model Selector
class ModelSelector extends StatefulWidget {
final bool embeddedNavigation;
const ModelSelector({super.key, required this.embeddedNavigation});
@override
State<ModelSelector> createState() => _ModelSelectorState();
}
class _ModelSelectorState extends State<ModelSelector> {
@override
void initState() {
super.initState();
ModelManager.instance.addListener(onUpdate);
}
@override
void dispose() {
ModelManager.instance.removeListener(onUpdate);
super.dispose();
}
void onUpdate() {
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
final breakpoint = Breakpoint.of(context);
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ModelSelectorHeader(embeddedNavigation: widget.embeddedNavigation),
...ModelManager.instance.models.map((model) {
final tmp = ModelSelectorTile(
model: model,
onTap: () {
ModelManager.instance.currentModel = model;
Navigator.of(context).pop();
},
);
if (widget.embeddedNavigation) {
return tmp;
} else {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: breakpoint.spacing / 2,
),
child: tmp,
);
}
}),
],
),
);
}
}
class ModelSelectorHeader extends StatefulWidget {
final bool embeddedNavigation;
const ModelSelectorHeader({super.key, required this.embeddedNavigation});
@override
State<ModelSelectorHeader> createState() => _ModelSelectorHeaderState();
}
class _ModelSelectorHeaderState extends State<ModelSelectorHeader> {
bool _isLoading = false;
bool _loadingFromAdd = false;
Future<void> refreshModels() async {
if (_isLoading) return;
if (HostManager.instance.host == null) {
Navigator.of(context).pop();
}
_isLoading = true;
if (mounted) setState(() {});
if (mounted) {
await errorGuard(context, "N3AI17J1", () async {
try {
await Future.wait([
ModelManager.instance.loadModels(),
Future.delayed(Durations.long1),
]).catchError(Error.throwWithStackTrace);
} finally {
_isLoading = false;
_loadingFromAdd = false;
if (mounted) setState(() {});
}
});
}
}
@override
Widget build(BuildContext context) {
final iconTheme = IconTheme.of(context);
final textTheme = TextTheme.of(context);
final appLocalizations = AppLocalizations.of(context);
final title = Text(
appLocalizations.modelDialogTitle,
style: widget.embeddedNavigation ? textTheme.titleMedium : null,
);
final actions = [
TwoStateWidgetManaged(
state: _isLoading && _loadingFromAdd,
dimension: (iconTheme.size ?? 24) + (8 * 2),
from: IconButton(
icon: const Icon(Icons.add),
tooltip: appLocalizations.modelDialogAdd,
onPressed: !_isLoading
? () async {
final addedModel = await showModelAdderDialog(context);
if (addedModel != null) {
ModelManager.instance.currentModelName = addedModel;
if (!context.mounted) return;
Navigator.of(context).pop();
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
appLocalizations.modelDialogAddDownloadSuccess,
),
),
);
}
}
: null,
),
to: const AspectRatio(
aspectRatio: 1 / 1,
child: SizedBox.expand(
child: Center(
child: SizedCircularProgressIndicator(
dimension: 24,
padding: EdgeInsets.zero,
),
),
),
),
),
Padding(
padding: EdgeInsetsDirectional.only(
end: widget.embeddedNavigation ? 0 : 8,
),
child: TwoStateWidgetManaged(
state: _isLoading && !_loadingFromAdd,
dimension: (iconTheme.size ?? 24) + (8 * 2),
from: IconButton(
icon: const Icon(Icons.refresh),
tooltip: appLocalizations.modelDialogRefresh,
onPressed: !_isLoading ? refreshModels : null,
),
to: const AspectRatio(
aspectRatio: 1 / 1,
child: SizedBox.expand(
child: Center(
child: SizedCircularProgressIndicator(
dimension: 24,
padding: EdgeInsets.zero,
),
),
),
),
),
),
];
return widget.embeddedNavigation
? Row(mainAxisAlignment: MainAxisAlignment.end, children: actions)
: AppBar(
leading: const AspectRatio(
aspectRatio: 1 / 1,
child: SizedBox.expand(child: BackButton()),
),
title: title,
actions: actions,
);
}
}
class ModelSelectorTile extends StatelessWidget {
final Model model;
final VoidCallback? onTap;
const ModelSelectorTile({
super.key,
required this.model,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final iconTheme = IconTheme.of(context);
final appLocalizations = AppLocalizations.of(context);
final Widget subtitle = Builder(
builder: (context) => Wrap(
children: [
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Transform.translate(
offset: const Offset(0, -1),
child: Icon(
Icons.functions,
size: DefaultTextStyle.of(context).style.fontSize,
),
),
),
const TextSpan(text: " "),
TextSpan(
text: model.parameterSize.replaceAll(
".",
intl.NumberFormat.decimalPattern(
appLocalizations.localeName,
).symbols.DECIMAL_SEP,
),
),
],
),
),
Text(" " * 3),
if ((model.families.isEmpty ? [model.family] : model.families)
.length !=
1 ||
(model.families.isEmpty ? [model.family] : model.families)
.first !=
model.name.split(":").first) ...[
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Transform.translate(
offset: const Offset(0, -1),
child: Icon(
Icons.group,
size: DefaultTextStyle.of(context).style.fontSize,
),
),
),
const TextSpan(text: " "),
TextSpan(
children:
(model.families.isEmpty
? [model.family]
: model.families)
.map((family) => TextSpan(text: family))
.joinElements(const TextSpan(text: ", "))
.toList(),
),
],
),
),
Text(" " * 3),
],
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Transform.translate(
offset: const Offset(0, -1),
child: Icon(
Icons.sd_storage_outlined,
size: DefaultTextStyle.of(context).style.fontSize,
),
),
),
const TextSpan(text: " "),
TextSpan(
text: bytesToHumanReadable(
model.size,
locale: appLocalizations.localeName,
),
),
],
),
),
Text(" " * 3),
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Transform.translate(
offset: const Offset(0, -1),
child: Icon(
Icons.calendar_today,
size: DefaultTextStyle.of(context).style.fontSize,
),
),
),
const TextSpan(text: " "),
TextSpan(
text: GetTimeAgo.parse(
model.modifiedAt,
locale: appLocalizations.localeName,
pattern: intl.DateFormat.yMd(
appLocalizations.localeName,
).pattern,
),
),
],
),
),
],
),
);
final tmp = ListTile(
title: Builder(
builder: (context) => Text.rich(
TextSpan(children: model.nameColored(context)),
style: DefaultTextStyle.of(
context,
).style.copyWith(fontFamily: "GoogleSansCode"),
),
),
subtitle: subtitle,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: model.capabilities
.map(
(capability) => Padding(
padding: const EdgeInsetsDirectional.only(start: 2),
child: Tooltip(
message: capability.name.toTitleCase(),
child: Icon(
capability.icon,
size: (iconTheme.size ?? 24) - ((iconTheme.size ?? 24) / 3),
),
),
),
)
.toList(),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: onTap,
);
return AnimatedContainer(
duration: ExpressiveCurves.expressiveEffects.fastDuration,
curve: ExpressiveCurves.expressiveEffects.fast,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: ModelManager.instance.currentModel == model
? Border.all(color: colorScheme.outline, width: 2)
: Border.all(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0),
width: 2,
),
),
child: tmp,
);
}
}

View File

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
class SizedCircularProgressIndicator extends StatelessWidget {
final double? dimension;
final double? value;
final Color? backgroundColor;
final Color? color;
final Animation<Color?>? valueColor;
final String? semanticsLabel;
final String? semanticsValue;
final double? strokeWidth;
final double? strokeAlign;
final StrokeCap? strokeCap;
final BoxConstraints? constraints;
final double? trackGap;
@Deprecated(
'Set this flag to false to opt into the 2024 progress indicator appearance. Defaults to true. '
'In the future, this flag will default to false. Use ProgressIndicatorThemeData to customize individual properties. '
'This feature was deprecated after v3.27.0-0.2.pre.',
)
final bool? year2023;
final EdgeInsetsGeometry? padding;
final AnimationController? controller;
const SizedCircularProgressIndicator({
super.key,
this.value,
this.dimension,
this.backgroundColor,
this.color,
this.valueColor,
this.strokeWidth,
this.strokeAlign,
this.semanticsLabel,
this.semanticsValue,
this.strokeCap,
this.constraints,
this.trackGap,
@Deprecated(
'Set this flag to false to opt into the 2024 progress indicator appearance. Defaults to true. '
'In the future, this flag will default to false. Use ProgressIndicatorThemeData to customize individual properties. '
'This feature was deprecated after v3.27.0-0.1.pre.',
)
this.year2023,
this.padding,
this.controller,
});
@override
Widget build(BuildContext context) {
final scale = (dimension ?? 40) / 40;
final indicator = CircularProgressIndicator(
value: value,
backgroundColor: backgroundColor,
color: color,
valueColor: valueColor,
strokeWidth: (strokeWidth ?? 4) / scale,
strokeAlign: strokeAlign,
semanticsLabel: semanticsLabel,
semanticsValue: semanticsValue,
strokeCap: strokeCap,
constraints: constraints,
trackGap: trackGap,
// pipe through
// ignore: deprecated_member_use
year2023: year2023,
padding: padding,
controller: controller,
);
if (scale == 1) return indicator;
return Transform.scale(scale: scale, child: indicator);
}
}

View File

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import '../services/services.dart';
/// Widget used to blend between two widget states, like icons.
class TwoStateWidget extends StatelessWidget {
final Animation<double> animation;
final double? dimension;
final Widget? from;
final Widget? to;
const TwoStateWidget({
super.key,
required this.animation,
this.dimension,
this.from,
this.to,
});
@override
Widget build(BuildContext context) {
final animationFrom = ReverseAnimation(animation);
final animationTo = animation;
final children = <Widget>[
if (from != null) ScaleTransition(scale: animationFrom, child: from),
if (to != null) ScaleTransition(scale: animationTo, child: to),
];
final child = Stack(
children: children.map((child) {
if (dimension != null) return Positioned.fill(child: child);
return child;
}).toList(),
);
if (dimension != null) {
return ConstrainedBox(
constraints: BoxConstraints.tight(Size.square(dimension!)),
child: child,
);
}
return child;
}
}
class TwoStateWidgetManaged extends StatefulWidget {
final bool state;
final double? dimension;
final Widget? from;
final Widget? to;
final Duration duration;
final Curve curve;
TwoStateWidgetManaged({
super.key,
required this.state,
this.dimension,
this.from,
this.to,
Duration? duration,
Curve? curve,
}) : duration = duration ?? ExpressiveCurves.expressiveSpatial.fastDuration,
curve = curve ?? ExpressiveCurves.expressiveSpatial.fast;
@override
State<TwoStateWidgetManaged> createState() => _TwoStateWidgetManagedState();
}
class _TwoStateWidgetManagedState extends State<TwoStateWidgetManaged>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Animation<double>? _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
value: widget.state ? 1.0 : 0.0,
duration: widget.duration,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: widget.curve,
);
}
@override
void didUpdateWidget(covariant TwoStateWidgetManaged oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.duration != oldWidget.duration) {
_animationController.duration = widget.duration;
}
if (widget.curve != oldWidget.curve) {
_animation = CurvedAnimation(
parent: _animationController,
curve: widget.curve,
);
}
if (widget.state != oldWidget.state) {
if (widget.state) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => TwoStateWidget(
animation: _animation!,
dimension: widget.dimension,
from: widget.from,
to: widget.to,
);
}

View File

@ -15,56 +15,81 @@ bool desktopFeature({bool web = false}) {
}
}
bool desktopLayout(BuildContext context,
{bool web = true, double? value, double valueCap = 1000}) {
bool desktopLayout(
BuildContext context, {
bool web = true,
double? value,
double valueCap = 1000,
}) {
value ??= MediaQuery.of(context).size.width;
return desktopFeature(web: web) || value >= valueCap;
}
bool desktopLayoutRequired(BuildContext context,
{bool web = true, double? value, double valueCap = 1000}) {
bool desktopLayoutRequired(
BuildContext context, {
bool web = true,
double? value,
double valueCap = 1000,
}) {
value ??= MediaQuery.of(context).size.width;
return desktopFeature(web: web) && value >= valueCap;
}
bool desktopLayoutNotRequired(BuildContext context,
{bool web = true, double? value, double valueCap = 1000}) {
bool desktopLayoutNotRequired(
BuildContext context, {
bool web = true,
double? value,
double valueCap = 1000,
}) {
value ??= MediaQuery.of(context).size.width;
return value >= valueCap;
}
Widget desktopControls(BuildContext context) {
return SizedBox(
height: 200,
child: WindowTitleBarBox(
child: Row(
height: 200,
child: WindowTitleBarBox(
child: Row(
children: [
SizedBox(
width: 46,
height: 200,
child: MinimizeWindowButton(
animate: true,
colors: WindowButtonColors(
iconNormal: Theme.of(context).colorScheme.primary))),
width: 46,
height: 200,
child: MinimizeWindowButton(
animate: true,
colors: WindowButtonColors(
iconNormal: Theme.of(context).colorScheme.primary,
),
),
),
SizedBox(
width: 46,
height: 72,
child: MaximizeWindowButton(
animate: true,
colors: WindowButtonColors(
iconNormal: Theme.of(context).colorScheme.primary))),
width: 46,
height: 72,
child: MaximizeWindowButton(
animate: true,
colors: WindowButtonColors(
iconNormal: Theme.of(context).colorScheme.primary,
),
),
),
SizedBox(
width: 46,
height: 72,
child: CloseWindowButton(
animate: true,
colors: WindowButtonColors(
iconNormal: Theme.of(context).colorScheme.primary))),
width: 46,
height: 72,
child: CloseWindowButton(
animate: true,
colors: WindowButtonColors(
iconNormal: Theme.of(context).colorScheme.primary,
),
),
),
],
)));
),
),
);
}
List<Widget>? desktopControlsActions(BuildContext context,
[List<Widget>? ifNotAvailable]) {
List<Widget>? desktopControlsActions(
BuildContext context, [
List<Widget>? ifNotAvailable,
]) {
return desktopFeature() ? <Widget>[desktopControls(context)] : ifNotAvailable;
}

View File

@ -27,23 +27,26 @@ Future<List<llama.Message>> getHistory([String? addToSystem]) async {
system += "\n$addToSystem";
}
var history = (prefs!.getBool("useSystem") ?? true)
final history = (prefs!.getBool("useSystem") ?? true)
? [llama.Message(role: llama.MessageRole.system, content: system)]
: <llama.Message>[];
var history2 = <llama.Message>[];
final history2 = <llama.Message>[];
images = [];
for (var i = 0; i < messages.length; i++) {
if (jsonDecode(jsonEncode(messages[i]))["text"] != null) {
history2.add(llama.Message(
history2.add(
llama.Message(
role: (messages[i].author.id == user.id)
? llama.MessageRole.user
: llama.MessageRole.system,
content: jsonDecode(jsonEncode(messages[i]))["text"],
images: (images.isNotEmpty) ? images : null));
images: (images.isNotEmpty) ? images : null,
),
);
images = [];
} else {
var uri = jsonDecode(jsonEncode(messages[i]))["uri"] as String;
var content = (uri.startsWith("data:image/png;base64,"))
final content = (uri.startsWith("data:image/png;base64,"))
? uri.removePrefix("data:image/png;base64,")
: base64.encode(await File(uri).readAsBytes());
uri = uri.removePrefix("data:image/png;base64,");
@ -61,7 +64,8 @@ List getHistoryString([String? uuid]) {
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == uuid) {
messages = jsonDecode(
jsonDecode((prefs!.getStringList("chats") ?? [])[i])["messages"]);
jsonDecode((prefs!.getStringList("chats") ?? [])[i])["messages"],
);
break;
}
}
@ -73,7 +77,7 @@ List getHistoryString([String? uuid]) {
if (messages[i]["type"] == "image") {
messages[i] = {
"role": messages[i]["role"]!,
"content": "<${messages[i]["role"]} inserted an image>"
"content": "<${messages[i]["role"]} inserted an image>",
};
}
}
@ -82,28 +86,34 @@ List getHistoryString([String? uuid]) {
}
Future<String> getTitleAi(List history) async {
var generated = await ollamaClient
final generated = await ollamaClient
.generateChatCompletion(
request: llama.GenerateChatCompletionRequest(
model: model!,
messages: [
const llama.Message(
role: llama.MessageRole.system,
content:
"Generate a three to six word title for the conversation provided by the user. If an object or person is very important in the conversation, put it in the title as well; keep the focus on the main subject. You must not put the assistant in the focus and you must not put the word 'assistant' in the title! Do preferably use title case. Use a formal tone, don't use dramatic words, like 'mystery' Use spaces between words, do not use camel case! You must not use markdown or any other formatting language! You must not use emojis or any other symbols! You must not use general clauses like 'assistance', 'help' or 'session' in your title! \n\n~~User Introduces Themselves~~ -> User Introduction\n~~User Asks for Help with a Problem~~ -> Problem Help\n~~User has a _**big**_ Problem~~ -> Big Problem"),
llama.Message(
role: llama.MessageRole.user,
content: "```\n${jsonEncode(history)}\n```")
],
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
model: model!,
messages: [
const llama.Message(
role: llama.MessageRole.system,
content:
"Generate a three to six word title for the conversation provided by the user. If an object or person is very important in the conversation, put it in the title as well; keep the focus on the main subject. You must not put the assistant in the focus and you must not put the word 'assistant' in the title! Do preferably use title case. Use a formal tone, don't use dramatic words, like 'mystery' Use spaces between words, do not use camel case! You must not use markdown or any other formatting language! You must not use emojis or any other symbols! You must not use general clauses like 'assistance', 'help' or 'session' in your title! \n\n~~User Introduces Themselves~~ -> User Introduction\n~~User Asks for Help with a Problem~~ -> Problem Help\n~~User has a _**big**_ Problem~~ -> Big Problem",
),
llama.Message(
role: llama.MessageRole.user,
content: "```\n${jsonEncode(history)}\n```",
),
],
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300"),
),
)
.timeout(Duration(
seconds:
(10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0)).round()));
.timeout(
Duration(
seconds: (10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
.round(),
),
);
var title = generated.message.content;
title = title.replaceAll("\n", " ");
var terms = [
final terms = [
'"',
"'",
"*",
@ -119,7 +129,7 @@ Future<String> getTitleAi(List history) async {
"[",
"]",
"{",
"}"
"}",
];
for (var i = 0; i < terms.length; i++) {
title = title.replaceAll(terms[i], "");
@ -138,12 +148,12 @@ Future<String> getTitleAi(List history) async {
Future<void> setTitleAi(List history) async {
try {
var title = await getTitleAi(history);
var tmp = prefs!.getStringList("chats") ?? [];
final title = await getTitleAi(history);
final tmp = prefs!.getStringList("chats") ?? [];
for (var i = 0; i < tmp.length; i++) {
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
chatUuid) {
var tmp2 = jsonDecode(tmp[i]);
final tmp2 = jsonDecode(tmp[i]);
tmp2["title"] = title;
tmp[i] = jsonEncode(tmp2);
break;
@ -153,18 +163,25 @@ Future<void> setTitleAi(List history) async {
} catch (_) {}
}
Future<String> send(String value, BuildContext context, Function setState,
{void Function(String currentText, bool done)? onStream,
String? addToSystem}) async {
Future<String> send(
String value,
BuildContext context,
Function setState, {
void Function(String currentText, bool done)? onStream,
String? addToSystem,
}) async {
selectionHaptic();
setState(() {
sendable = false;
});
if (host == null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noHostSelected),
showCloseIcon: true));
showCloseIcon: true,
),
);
if (onStream != null) {
onStream("", true);
}
@ -173,9 +190,12 @@ Future<String> send(String value, BuildContext context, Function setState,
if (!chatAllowed || model == null) {
if (model == null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noModelSelected),
showCloseIcon: true));
showCloseIcon: true,
),
);
}
if (onStream != null) {
onStream("", true);
@ -188,26 +208,30 @@ Future<String> send(String value, BuildContext context, Function setState,
newChat = true;
chatUuid = const Uuid().v4();
prefs!.setStringList(
"chats",
(prefs!.getStringList("chats") ?? []).append([
jsonEncode({
"title": AppLocalizations.of(context).newChatTitle,
"uuid": chatUuid,
"messages": []
})
]).toList());
"chats",
(prefs!.getStringList("chats") ?? []).append([
jsonEncode({
"title": AppLocalizations.of(context).newChatTitle,
"uuid": chatUuid,
"messages": [],
}),
]).toList(),
);
}
var history = await getHistory(addToSystem);
final history = await getHistory(addToSystem);
history.add(llama.Message(
history.add(
llama.Message(
role: llama.MessageRole.user,
content: value.trim(),
images: (images.isNotEmpty) ? images : null));
images: (images.isNotEmpty) ? images : null,
),
);
messages.insert(
0,
types.TextMessage(
author: user, id: const Uuid().v4(), text: value.trim()));
0,
types.TextMessage(author: user, id: const Uuid().v4(), text: value.trim()),
);
saveChat(chatUuid!, setState);
@ -215,20 +239,24 @@ Future<String> send(String value, BuildContext context, Function setState,
chatAllowed = false;
var text = "";
var newId = const Uuid().v4();
final newId = const Uuid().v4();
try {
if ((prefs!.getString("requestType") ?? "stream") == "stream") {
var stream = ollamaClient
final stream = ollamaClient
.generateChatCompletionStream(
request: llama.GenerateChatCompletionRequest(
model: model!,
messages: history,
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
model: model!,
messages: history,
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300"),
),
)
.timeout(Duration(
.timeout(
Duration(
seconds: (30.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
.round()));
.round(),
),
);
await for (var res in stream) {
text += res.message.content;
@ -240,7 +268,9 @@ Future<String> send(String value, BuildContext context, Function setState,
}
if (chatAllowed) return "";
messages.insert(
0, types.TextMessage(author: assistant, id: newId, text: text));
0,
types.TextMessage(author: assistant, id: newId, text: text),
);
//TODO: add functionality
//
// chatKey!.currentState!.scrollToMessage(messages[1].id,
@ -256,18 +286,26 @@ Future<String> send(String value, BuildContext context, Function setState,
request = await ollamaClient
.generateChatCompletion(
request: llama.GenerateChatCompletionRequest(
model: model!,
messages: history,
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
model: model!,
messages: history,
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300"),
),
)
.timeout(Duration(
.timeout(
Duration(
seconds: (30.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
.round()));
.round(),
),
);
if (chatAllowed) return "";
messages.insert(
0,
types.TextMessage(
author: assistant, id: newId, text: request.message.content));
0,
types.TextMessage(
author: assistant,
id: newId,
text: request.message.content,
),
);
text = request.message.content;
setState(() {});
heavyHaptic();
@ -283,7 +321,7 @@ Future<String> send(String value, BuildContext context, Function setState,
chatAllowed = true;
messages.removeAt(0);
if (messages.isEmpty) {
var tmp = prefs!.getStringList("chats") ?? [];
final tmp = prefs!.getStringList("chats") ?? [];
for (var i = 0; i < tmp.length; i++) {
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
chatUuid) {
@ -296,11 +334,14 @@ Future<String> send(String value, BuildContext context, Function setState,
}
});
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
// ignore: use_build_context_synchronously
Text(AppLocalizations.of(context).settingsHostInvalid("timeout")),
showCloseIcon: true));
showCloseIcon: true,
),
);
return "";
}
//TODO: add functionality

File diff suppressed because it is too large Load Diff

View File

@ -23,23 +23,26 @@ String? updateUrl;
String? latestVersion;
String? currentVersion;
String? updateChangeLog;
Future<bool> updatesSupported(Function setState,
[bool takeAction = false]) async {
Future<bool> updatesSupported(
Function setState, [
bool takeAction = false,
]) async {
var returnValue = true;
var installerApps = [
final installerApps = [
"org.fdroid.fdroid",
"org.gdroid.gdroid",
"eu.bubu1.fdroidclassic",
"in.sunilpaulmathew.izzyondroid",
"com.looker.droidify",
"com.machiav3lli.fdroid",
"nya.kitsunyan.foxydroid"
"nya.kitsunyan.foxydroid",
];
if (!desktopFeature(web: true)) {
if ((await InstallReferrer.referrer !=
InstallationAppReferrer.androidManually) ||
(installerApps
.contains((await InstallReferrer.app).packageName ?? ""))) {
(installerApps.contains(
(await InstallReferrer.app).packageName ?? "",
))) {
returnValue = false;
if (await InstallReferrer.referrer ==
InstallationAppReferrer.androidDebug) {
@ -75,20 +78,26 @@ Future<bool> checkUpdate(Function setState) async {
return false;
}
var repo = repoUrl.split("/");
final repo = repoUrl.split("/");
currentVersion = (await PackageInfo.fromPlatform()).version;
// currentVersion = "1.0.0";
String? version;
try {
var request = await httpClient
.get(Uri.parse(
"https://api.github.com/repos/${repo[3]}/${repo[4]}/releases"))
.timeout(Duration(
final request = await httpClient
.get(
Uri.parse(
"https://api.github.com/repos/${repo[3]}/${repo[4]}/releases",
),
)
.timeout(
Duration(
milliseconds:
(5000.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
.round()));
.round(),
),
);
if (request.statusCode == 403) {
setState(() {
updateStatus = "rateLimit";
@ -126,41 +135,53 @@ Future<bool> checkUpdate(Function setState) async {
Future<void> updateDialog(BuildContext context, Function title) async {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).settingsUpdateDialogTitle),
content: Column(mainAxisSize: MainAxisSize.min, children: [
Text(
AppLocalizations.of(context).settingsUpdateDialogDescription),
title(AppLocalizations.of(context).settingsUpdateChangeLog),
Flexible(
child: SingleChildScrollView(
child: Container(
constraints: const BoxConstraints(maxWidth: 1000),
child: MarkdownBody(
context: context,
builder: (context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).settingsUpdateDialogTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(context).settingsUpdateDialogDescription),
title(AppLocalizations.of(context).settingsUpdateChangeLog),
Flexible(
child: SingleChildScrollView(
child: Container(
constraints: const BoxConstraints(maxWidth: 1000),
child: MarkdownBody(
data: updateChangeLog ?? "No changelog given.",
shrinkWrap: true),
)))
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context).settingsUpdateDialogCancel)),
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
launchUrl(
mode: LaunchMode.inAppBrowserView,
Uri.parse(updateUrl!));
},
child: Text(
AppLocalizations.of(context).settingsUpdateDialogUpdate))
]);
});
shrinkWrap: true,
),
),
),
),
],
),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context).settingsUpdateDialogCancel,
),
),
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
launchUrl(
mode: LaunchMode.inAppBrowserView,
Uri.parse(updateUrl!),
);
},
child: Text(
AppLocalizations.of(context).settingsUpdateDialogUpdate,
),
),
],
);
},
);
}

View File

@ -1,6 +1,38 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
url: "https://pub.dev"
source: hosted
version: "100.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args:
dependency: transitive
description:
@ -17,6 +49,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
auto_route:
dependency: "direct main"
description:
name: auto_route
sha256: e9acfeb3df33d188fce4ad0239ef4238f333b7aa4d95ec52af3c2b9360dcd969
url: "https://pub.dev"
source: hosted
version: "11.1.0"
auto_route_generator:
dependency: "direct dev"
description:
name: auto_route_generator
sha256: f58fdb20c51b36afe7bf8a162aea3b22dff580a8a0b5c35e8e3a91367121c8f1
url: "https://pub.dev"
source: hosted
version: "10.6.0"
bitsdojo_window:
dependency: "direct main"
description:
@ -65,14 +113,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build:
dependency: transitive
description:
name: build
sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
url: "https://pub.dev"
source: hosted
version: "4.0.6"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
url: "https://pub.dev"
source: hosted
version: "2.15.0"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
url: "https://pub.dev"
source: hosted
version: "8.12.6"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
charcode:
dependency: transitive
description:
@ -81,6 +177,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@ -89,6 +201,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection:
dependency: transitive
description:
@ -97,6 +225,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
cross_file:
dependency: transitive
description:
@ -121,6 +257,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
url: "https://pub.dev"
source: hosted
version: "3.1.9"
dartx:
dependency: "direct main"
description:
@ -145,6 +289,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.1"
drift:
dependency: "direct main"
description:
name: drift
sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e"
url: "https://pub.dev"
source: hosted
version: "2.33.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "042a5fb507ab5697f67eb55b75cfff2f665701f6606926136d6d4e85f81ff837"
url: "https://pub.dev"
source: hosted
version: "2.34.1+1"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
duration_picker:
dependency: "direct main"
description:
@ -157,10 +325,10 @@ packages:
dependency: "direct main"
description:
name: dynamic_system_colors
sha256: "43794e658fa88cbdec9f397dd1afd2eb69b6c9717e99b93b16ba37c3aa3b3a8c"
sha256: "6cda994363fe362e3c433e068af83feffc2c9a26ba0be7ecdb2b96f676d2dfd8"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
version: "1.9.0"
equatable:
dependency: transitive
description:
@ -315,10 +483,10 @@ packages:
dependency: "direct main"
description:
name: flutter_install_referrer
sha256: "8f994ff0653be0483303af0a6f4ecbe2fc643a61456e5bd3bf5e28e38b035c77"
sha256: "4a82a6b09e9a3e38b41522e1d44413fe20f7dd48faa636412d0af2c8821d4de4"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "2.1.1"
flutter_link_previewer:
dependency: transitive
description:
@ -348,6 +516,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_markdown:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
url: "https://pub.dev"
source: hosted
version: "0.7.7+1"
flutter_math_fork:
dependency: "direct main"
description:
@ -389,23 +565,47 @@ packages:
dependency: "direct main"
description:
name: flutter_tts
sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4
sha256: ce5eb209b40e95f2f4a1397116c87ab2fcdff32257d04ed7a764e75894c03775
url: "https://pub.dev"
source: hosted
version: "4.2.3"
version: "4.2.5"
flutter_web_plugins:
dependency: transitive
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
frontend_server_client:
dependency: transitive
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "2.4.4"
version: "4.0.0"
get_time_ago:
dependency: "direct main"
description:
name: get_time_ago
sha256: "010abc128f885c223ccf4f86a88d4789bb4dd5cecdddd87fee600549dde5d4fb"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
highlight:
dependency: transitive
description:
@ -414,6 +614,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf"
url: "https://pub.dev"
source: hosted
version: "4.4.0"
html:
dependency: transitive
description:
@ -426,10 +642,18 @@ packages:
dependency: "direct main"
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
@ -438,6 +662,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: "direct main"
description:
name: image
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
url: "https://pub.dev"
source: hosted
version: "4.9.1"
image_picker:
dependency: "direct main"
description:
@ -510,6 +742,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.20.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
js:
dependency: transitive
description:
@ -550,6 +806,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lean_builder:
dependency: transitive
description:
name: lean_builder
sha256: "123eef9fba96ec12a18b8eb4889850368d78e8c0a6a63eaa45ec4cf3e73fbded"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
linkify:
dependency: transitive
description:
@ -566,6 +830,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
markdown:
dependency: "direct main"
description:
@ -578,34 +850,42 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.18.0"
mime:
dependency: transitive
dependency: "direct main"
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: f59351d28f49520cd3a74eb1f41c5f19ae15e53c65a3231d14af672e46510a96
url: "https://pub.dev"
source: hosted
version: "0.19.1"
nested:
dependency: transitive
description:
@ -614,6 +894,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
ogp_data_extract:
dependency: "direct main"
description:
@ -626,18 +914,26 @@ packages:
dependency: "direct main"
description:
name: ollama_dart
sha256: df4acde50fc4a055134f91919505d801170c2e1e5df798056a18e2811f20307b
sha256: dd60bf042a9deeb616f2e03ff6c189fabc72ed06c2762b48363dab4cb668684f
url: "https://pub.dev"
source: hosted
version: "0.2.4"
version: "2.2.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
url: "https://pub.dev"
source: hosted
version: "8.3.1"
version: "9.0.1"
package_info_plus_platform_interface:
dependency: transitive
description:
@ -662,6 +958,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@ -746,10 +1066,10 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
version: "7.0.2"
photo_view:
dependency: transitive
description:
@ -774,6 +1094,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
provider:
dependency: transitive
description:
@ -782,6 +1118,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
pwa_install:
dependency: "direct main"
description:
@ -790,6 +1142,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.6"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
scroll_to_index:
dependency: "direct main"
description:
@ -802,10 +1170,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.3"
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
@ -854,6 +1222,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
simple_icons:
dependency: "direct main"
description:
@ -875,14 +1259,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
url: "https://pub.dev"
source: hosted
version: "4.2.3"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.2"
speech_to_text:
dependency: "direct main"
description:
@ -915,6 +1307,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqlcipher_flutter_libs:
dependency: transitive
description:
name: sqlcipher_flutter_libs
sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
url: "https://pub.dev"
source: hosted
version: "0.7.0+eol"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "9488c7d2cdb1091c91cacf7e207cff81b28bff8e366f042bad3afe7d34afe189"
url: "https://pub.dev"
source: hosted
version: "3.3.2"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
url: "https://pub.dev"
source: hosted
version: "0.6.0+eol"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: ecdc06d4a7d79dcbc928d99afd2f7f5b0f98a637c46f89be83d911617f759978
url: "https://pub.dev"
source: hosted
version: "0.44.4"
stack_trace:
dependency: transitive
description:
@ -931,6 +1355,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -959,10 +1391,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
version: "0.7.11"
time:
dependency: transitive
description:
@ -1103,10 +1535,10 @@ packages:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
sha256: "7ee12e6dffe0fc8e755179d6d91b3b34f5924223fc104d85572ef9180d73d172"
url: "https://pub.dev"
source: hosted
version: "1.1.19"
version: "1.2.5"
vector_math:
dependency: transitive
description:
@ -1139,6 +1571,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
@ -1147,6 +1587,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
win32:
dependency: transitive
description:
@ -1167,10 +1623,26 @@ packages:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
version: "7.0.1"
xxh3:
dependency: transitive
description:
name: xxh3
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.35.0"
dart: ">=3.12.0 <4.0.0"
flutter: "3.44.7"

View File

@ -4,17 +4,22 @@ publish_to: 'none'
version: 1.2.0+9
environment:
sdk: '>=3.8.1 <4.0.0'
flutter: 3.35.0
sdk: '>=3.12.0 <4.0.0'
flutter: 3.44.7
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
ollama_dart: ^0.2.4
flutter_web_plugins:
sdk: flutter
ollama_dart: ^2.2.0
intl: any
shared_preferences: ^2.2.3
image: ^4.9.1
mime: ^2.0.0
get_time_ago: ^2.4.1
shared_preferences: ^2.5.5
flutter_chat_ui: ^1.6.13
scroll_to_index: ^3.0.1
uuid: ^4.4.0
@ -29,13 +34,13 @@ dependencies:
file_picker: ^8.0.3
file_selector: ^1.0.3
bitsdojo_window: ^0.1.6
flutter_install_referrer: ^2.0.2
package_info_plus: ^8.0.0
flutter_install_referrer: ^2.1.1
package_info_plus: ^9.0.1
version: ^3.0.2
flutter_displaymode: ^0.6.0
duration_picker: ^1.2.0
speech_to_text: ^7.0.0
flutter_tts: ^4.2.3
speech_to_text: ^7.3.0
flutter_tts: ^4.2.5
permission_handler: ^11.3.1
datetime_loop: ^1.2.0
universal_html: ^2.2.4
@ -48,14 +53,24 @@ dependencies:
url: https://github.com/JHubi1/highlight.dart
path: flutter_highlight
ogp_data_extract: ^0.2.2
auto_route: ^11.1.0
# dynamic_color: ^1.7.0
dynamic_system_colors: ^1.8.0
dynamic_system_colors: ^1.9.0
drift: ^2.33.0
drift_flutter: ^0.3.0
path_provider: ^2.1.5
# dynamic_color: any
flutter_markdown: any
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
drift_dev: ^2.33.0
auto_route_generator: ^10.6.0
build_runner: ^2.15.0
flutter:
uses-material-design: true
@ -69,3 +84,9 @@ flutter:
- assets/welcome/1dark.png
- assets/welcome/2dark.png
- assets/welcome/3dark.png
fonts:
- family: GoogleSansCode
fonts:
- asset: assets/fonts/GoogleSansCode-VariableFont_MONO,wght.ttf
- asset: assets/fonts/GoogleSansCode-Italic-VariableFont_MONO,wght.ttf
style: italic

View File

@ -4,7 +4,7 @@ import 'dart:io';
void main() async {
Directory.current = Directory(Platform.script.toFilePath()).parent.parent;
var flutterExecutable = Platform.isWindows ? 'flutter.bat' : 'flutter';
final flutterExecutable = Platform.isWindows ? 'flutter.bat' : 'flutter';
print("Build script for Ollama App by JHubi1");
print("Report issues at: https://github.com/JHubi1/ollama-app/issues");
@ -12,14 +12,15 @@ void main() async {
print('----------');
print('Extracting version from pubspec.yaml ...');
var pubspec = File('pubspec.yaml');
var versionLine = await pubspec
.readAsLines()
.then((lines) => lines.firstWhere((line) => line.contains('version')));
var version = versionLine.split(':').last.trim().split('+').first.trim();
var versionCode = versionLine.split(':').last.trim().split('+')[1].trim();
final pubspec = File('pubspec.yaml');
final versionLine = await pubspec.readAsLines().then(
(lines) => lines.firstWhere((line) => line.contains('version')),
);
final version = versionLine.split(':').last.trim().split('+').first.trim();
final versionCode = versionLine.split(':').last.trim().split('+')[1].trim();
print(
"Building Ollama App v$version (build $versionCode) - this may take a while");
"Building Ollama App v$version (build $versionCode) - this may take a while",
);
print('----------');
@ -27,7 +28,7 @@ void main() async {
'build',
'apk',
'--obfuscate',
'--split-debug-info=build\\debugAndroid'
'--split-debug-info=build\\debugAndroid',
]);
// ----------
@ -71,17 +72,20 @@ void main() async {
stdout.write('Copying build output to build\\.output ');
try {
var outputDir = Directory('build\\.output');
final outputDir = Directory('build\\.output');
if (await outputDir.exists()) {
await outputDir.delete(recursive: true);
}
await outputDir.create();
await copyFile('build\\app\\outputs\\flutter-apk\\app-release.apk',
'build\\.output\\ollama-android-v$version.apk');
await copyFile(
'build\\windows\\x64\\runner\\ollama-windows-x64-v$version.exe',
'build\\.output\\ollama-windows-x64-v$version.exe');
'build\\app\\outputs\\flutter-apk\\app-release.apk',
'build\\.output\\ollama-android-v$version.apk',
);
await copyFile(
'build\\windows\\x64\\runner\\ollama-windows-x64-v$version.exe',
'build\\.output\\ollama-windows-x64-v$version.exe',
);
print('- done');
} catch (_) {
print('- failed');
@ -95,14 +99,18 @@ void main() async {
}
Future<void> copyFile(String sourcePath, String destinationPath) async {
var sourceFile = File(sourcePath);
final sourceFile = File(sourcePath);
if (await sourceFile.exists()) {
await sourceFile.copy(destinationPath);
}
}
Future<void> execute(String title, String command, List<String> arguments,
[String? errorText]) async {
Future<void> execute(
String title,
String command,
List<String> arguments, [
String? errorText,
]) async {
stdout.write('$title ');
ProcessResult process;
try {

View File

@ -1,8 +1,8 @@
import 'dart:math';
String random(int length) {
var rand = Random();
var codeUnits = List.generate(length, (index) {
final rand = Random();
final codeUnits = List.generate(length, (index) {
return rand.nextInt(33) + 89;
});

View File

@ -6,13 +6,17 @@ import 'functions.dart';
void main() {
testWidgets("Widget: button", (WidgetTester tester) async {
var text = random(10);
final text = random(10);
var clicked = false;
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: button(text, Icons.add_rounded, () {
clicked = true;
}))));
body: button(text, Icons.add_rounded, () {
clicked = true;
}),
),
),
);
expect(find.text(text), findsOneWidget);
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
@ -23,14 +27,21 @@ void main() {
expect(clicked, true);
});
testWidgets("Widget: toggle", (WidgetTester tester) async {
var text = random(10);
final text = random(10);
var toggled = false;
await tester.pumpWidget(
MaterialApp(home: Scaffold(body: Builder(builder: (context) {
return toggle(context, text, toggled, (value) {
toggled = value;
});
}))));
MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
return toggle(context, text, toggled, (value) {
toggled = value;
});
},
),
),
),
);
expect(find.textContaining(text), findsOneWidget);
expect(find.byType(Switch), findsOneWidget);

View File

@ -4,7 +4,7 @@
import 'dart:math';
void main() {
var random = Random.secure();
final random = Random.secure();
var logId = "";
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

View File

@ -1,41 +1,181 @@
{
"de": [
"optionChatDetails",
"optionChatDetailsNoChat",
"newChatGreeting1",
"newChatGreeting2",
"newChatGreeting3",
"newChatGreeting4",
"newChatGreeting5",
"newChatGreetingNameFallback",
"modelDialogTitle",
"modelDialogRefresh",
"errorGuardTitle",
"errorGuardDetails",
"errorGuardException",
"errorGuardStackTrace",
"errorGuardReport"
"errorGuardReport",
"settingsTitleOverview",
"settingsDescriptionOverview",
"settingsHostHeaderHeaderName",
"settingsHostHeaderHeaderValue",
"settingsHostHeaderUnsupported",
"settingsHostHeaderDuplicate"
],
"fa": [
"learnMore",
"optionNoChatFoundSearch",
"optionSearchChats",
"optionChatDetails",
"optionChatDetailsNoChat",
"newChatGreeting1",
"newChatGreeting2",
"newChatGreeting3",
"newChatGreeting4",
"newChatGreeting5",
"newChatGreetingNameFallback",
"messageInputPlaceholderModelPlaceholder",
"tooltipMessageOptions",
"modelDialogTitle",
"modelDialogAdd",
"modelDialogRefresh",
"modelDialogAddCurrently",
"modelDialogAddPromptNotFound",
"modelDialogAddPromptNetworkError",
"modelDialogAddPromptCorruptionError",
"modelDialogAddPromptCorruptionErrorSolution",
"modelDialogAddCancelTitle",
"modelDialogAddCancelConfirm",
"modelDialogAddCancelHide",
"errorGuardTitle",
"errorGuardDetails",
"errorGuardException",
"errorGuardStackTrace",
"errorGuardReport"
"errorGuardReport",
"settingsTitleOverview",
"settingsDescriptionOverview",
"settingsHostMissing",
"settingsHostHeaderHeaderName",
"settingsHostHeaderHeaderValue",
"settingsHostHeaderUnsupported",
"settingsHostHeaderDuplicate"
],
"it": [
"learnMore",
"optionNoChatFoundSearch",
"optionSearchChats",
"optionChatDetails",
"optionChatDetailsNoChat",
"newChatGreeting1",
"newChatGreeting2",
"newChatGreeting3",
"newChatGreeting4",
"newChatGreeting5",
"newChatGreetingNameFallback",
"messageInputPlaceholderModelPlaceholder",
"tooltipMessageOptions",
"modelDialogTitle",
"modelDialogAdd",
"modelDialogRefresh",
"modelDialogAddCurrently",
"modelDialogAddPromptNotFound",
"modelDialogAddPromptNetworkError",
"modelDialogAddPromptCorruptionError",
"modelDialogAddPromptCorruptionErrorSolution",
"modelDialogAddCancelTitle",
"modelDialogAddCancelConfirm",
"modelDialogAddCancelHide",
"errorGuardTitle",
"errorGuardDetails",
"errorGuardException",
"errorGuardStackTrace",
"errorGuardReport"
"errorGuardReport",
"settingsTitleOverview",
"settingsDescriptionOverview",
"settingsHostMissing",
"settingsHostHeaderHeaderName",
"settingsHostHeaderHeaderValue",
"settingsHostHeaderUnsupported",
"settingsHostHeaderDuplicate"
],
"tr": [
"learnMore",
"optionNoChatFoundSearch",
"optionSearchChats",
"optionChatDetails",
"optionChatDetailsNoChat",
"newChatGreeting1",
"newChatGreeting2",
"newChatGreeting3",
"newChatGreeting4",
"newChatGreeting5",
"newChatGreetingNameFallback",
"messageInputPlaceholderModelPlaceholder",
"tooltipMessageOptions",
"modelDialogTitle",
"modelDialogAdd",
"modelDialogRefresh",
"modelDialogAddCurrently",
"modelDialogAddPromptNotFound",
"modelDialogAddPromptNetworkError",
"modelDialogAddPromptCorruptionError",
"modelDialogAddPromptCorruptionErrorSolution",
"modelDialogAddCancelTitle",
"modelDialogAddCancelConfirm",
"modelDialogAddCancelHide",
"errorGuardTitle",
"errorGuardDetails",
"errorGuardException",
"errorGuardStackTrace",
"errorGuardReport"
"errorGuardReport",
"settingsTitleOverview",
"settingsDescriptionOverview",
"settingsHostMissing",
"settingsHostHeaderHeaderName",
"settingsHostHeaderHeaderValue",
"settingsHostHeaderUnsupported",
"settingsHostHeaderDuplicate"
],
"zh": [
"learnMore",
"optionNoChatFoundSearch",
"optionSearchChats",
"optionChatDetails",
"optionChatDetailsNoChat",
"newChatGreeting1",
"newChatGreeting2",
"newChatGreeting3",
"newChatGreeting4",
"newChatGreeting5",
"newChatGreetingNameFallback",
"messageInputPlaceholderModelPlaceholder",
"tooltipMessageOptions",
"modelDialogTitle",
"modelDialogAdd",
"modelDialogRefresh",
"modelDialogAddCurrently",
"modelDialogAddPromptNotFound",
"modelDialogAddPromptNetworkError",
"modelDialogAddPromptCorruptionError",
"modelDialogAddPromptCorruptionErrorSolution",
"modelDialogAddCancelTitle",
"modelDialogAddCancelConfirm",
"modelDialogAddCancelHide",
"errorGuardTitle",
"errorGuardDetails",
"errorGuardException",
"errorGuardStackTrace",
"errorGuardReport"
"errorGuardReport",
"settingsTitleOverview",
"settingsDescriptionOverview",
"settingsHostMissing",
"settingsHostHeaderHeaderName",
"settingsHostHeaderHeaderValue",
"settingsHostHeaderUnsupported",
"settingsHostHeaderDuplicate"
]
}

13271
web/drift_worker.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
<html>
<head>
<!-- <base href="$FLUTTER_BASE_HREF"> -->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">

BIN
web/sqlite3.wasm Normal file

Binary file not shown.

View File

@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)