ChatDB, complete UI, formatting, everything else
This commit is contained in:
parent
ba63e0fc50
commit
27e31e2ab1
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = "../.."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "What’s 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": "Couldn’t 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": "Couldn’t 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 shouldn’t 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 isn’t 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 doesn’t respond as expected by Ollama App.\nIt may not be an Ollama server. Are you sure you entered it correctly?} timeout{The host couldn’t 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 model’s 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": "Don’t 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 won’t be merged! You’ll 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": "Can’t 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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
/// **'What’s 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'**
|
||||
/// **'Couldn’t 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;
|
||||
/// **'Couldn’t 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 shouldn’t 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 isn’t 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 doesn’t respond as expected by Ollama App.\nIt may not be an Ollama server. Are you sure you entered it correctly?} timeout{The host couldn’t 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 model’s 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'**
|
||||
/// **'Don’t 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 won’t be merged! You’ll 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'**
|
||||
/// **'Can’t 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
|
||||
|
|
|
|||
|
|
@ -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 'What’s 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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 'What’s 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 =>
|
||||
'Couldn’t 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 =>
|
||||
'Couldn’t 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 shouldn’t 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 isn’t 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 doesn’t respond as expected by Ollama App.\nIt may not be an Ollama server. Are you sure you entered it correctly?',
|
||||
'timeout':
|
||||
'The host couldn’t 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 model’s 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 => 'Don’t 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 won’t be merged! You’ll 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 => 'Can’t 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';
|
||||
|
|
|
|||
|
|
@ -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 'What’s 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 =>
|
||||
'Couldn’t 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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 'What’s 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 =>
|
||||
'Couldn’t 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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 'What’s 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 =>
|
||||
'Couldn’t 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ı';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 'What’s 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 =>
|
||||
'Couldn’t 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 => '系统信息';
|
||||
|
||||
|
|
|
|||
220
lib/main.dart
220
lib/main.dart
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
))
|
||||
]),
|
||||
)
|
||||
]),
|
||||
))));
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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) {}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)!;
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
552
pubspec.lock
552
pubspec.lock
|
|
@ -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"
|
||||
|
|
|
|||
39
pubspec.yaml
39
pubspec.yaml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
void main() {
|
||||
var random = Random.secure();
|
||||
final random = Random.secure();
|
||||
var logId = "";
|
||||
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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">
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
jni
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
|
|
|||
Loading…
Reference in New Issue