Re-implementation Chat + Model system, ErrorGuard
This commit is contained in:
parent
58437ad377
commit
c5571b645a
|
@ -19,7 +19,7 @@ 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 "1.8.22" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
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
|
|
@ -3,6 +3,5 @@ template-arb-file: app_en.arb
|
|||
output-dir: lib/l10n/gen
|
||||
preferred-supported-locales: [ en ]
|
||||
untranslated-messages-file: untranslated_messages.json
|
||||
synthetic-package: false
|
||||
nullable-getter: false
|
||||
format: true
|
||||
|
|
|
@ -3,197 +3,192 @@
|
|||
"appTitle": "Ollama",
|
||||
"@appTitle": {
|
||||
"description": "Title of the application",
|
||||
"context": "Visible in the side bar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"optionNewChat": "New Chat",
|
||||
"@optionNewChat": {
|
||||
"description": "Text displayed for new chat option",
|
||||
"context": "Visible in the side bar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"optionSettings": "Settings",
|
||||
"@optionSettings": {
|
||||
"description": "Text displayed for settings option",
|
||||
"context": "Visible in the side bar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"optionInstallPwa": "Install Webapp",
|
||||
"@optionInstallPwa": {
|
||||
"description": "Text displayed for install PWA option",
|
||||
"context": "Visible in the side bar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"optionNoChatFound": "No chats found",
|
||||
"@optionNoChatFound": {
|
||||
"description": "Text displayed when no chats are found",
|
||||
"context": "Visible in the side bar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"tipPrefix": "Tip: ",
|
||||
"@tipPrefix": {
|
||||
"description": "Prefix for tips",
|
||||
"context": "Visible in the sidebar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"tip0": "Edit messages by long taping on them",
|
||||
"tip0": "Edit messages by long tapping on them",
|
||||
"@tip0": {
|
||||
"description": "First tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"tip1": "Delete messages by double tapping on them",
|
||||
"@tip1": {
|
||||
"description": "Second tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"tip2": "You can change the theme in settings",
|
||||
"@tip2": {
|
||||
"description": "Third tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"tip3": "Select a multimodal model to input images",
|
||||
"@tip3": {
|
||||
"description": "Fourth tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"tip4": "Chats are automatically saved",
|
||||
"@tip4": {
|
||||
"description": "Fifth tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"deleteChat": "Delete",
|
||||
"@deleteChat": {
|
||||
"description": "Text displayed for delete chat option",
|
||||
"context": "Visible in the chat view, desktop only"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"renameChat": "Rename",
|
||||
"@renameChat": {
|
||||
"description": "Text displayed for rename chat option",
|
||||
"context": "Visible in the chat view, desktop only"
|
||||
"context": "app:sidebar"
|
||||
},
|
||||
"takeImage": "Take Image",
|
||||
"@takeImage": {
|
||||
"description": "Text displayed for take image button",
|
||||
"context": "Visible in attachment menu"
|
||||
"context": "app:chat:attachment"
|
||||
},
|
||||
"uploadImage": "Upload Image",
|
||||
"@uploadImage": {
|
||||
"description": "Text displayed for image upload button",
|
||||
"context": "Visible in attachment menu"
|
||||
"context": "app:chat:attachment"
|
||||
},
|
||||
"notAValidImage": "Not a valid image",
|
||||
"@notAValidImage": {
|
||||
"description": "Text displayed when an image is not valid",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat:attachment"
|
||||
},
|
||||
"imageOnlyConversation": "Image Only Conversation",
|
||||
"@imageOnlyConversation": {
|
||||
"description": "Title, if 'Generate Title' is executed on a conversation with no text messages",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat:title"
|
||||
},
|
||||
"messageInputPlaceholder": "Message",
|
||||
"@messageInputPlaceholder": {
|
||||
"description": "Placeholder text for message input",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat:input"
|
||||
},
|
||||
"tooltipAttachment": "Add attachment",
|
||||
"@tooltipAttachment": {
|
||||
"description": "Tooltip for attachment button",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat:tooltips"
|
||||
},
|
||||
"tooltipSend": "Send",
|
||||
"@tooltipSend": {
|
||||
"description": "Tooltip for send button",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat:tooltips"
|
||||
},
|
||||
"tooltipSave": "Save",
|
||||
"@tooltipSave": {
|
||||
"description": "Tooltip for save button",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat:tooltips"
|
||||
},
|
||||
"tooltipLetAIThink": "Let AI think",
|
||||
"@tooltipLetAIThink": {
|
||||
"description": "Tooltip for let AI think button",
|
||||
"context": "Visible in the chat view"
|
||||
},
|
||||
"tooltipAddHostHeaders": "Add host headers",
|
||||
"@tooltipAddHostHeaders": {
|
||||
"description": "Tooltip for add host headers button",
|
||||
"context": "Visible in settings view"
|
||||
"context": "app:chat:tooltips"
|
||||
},
|
||||
"tooltipReset": "Reset current chat",
|
||||
"@tooltipReset": {
|
||||
"description": "Tooltip for reset button",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat:tooltips"
|
||||
},
|
||||
"tooltipOptions": "Show options",
|
||||
"@tooltipOptions": {
|
||||
"description": "Tooltip for options button",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat:tooltips"
|
||||
},
|
||||
"noModelSelected": "No model selected",
|
||||
"@noModelSelected": {
|
||||
"description": "Text displayed when no model is selected",
|
||||
"context": "Visible in the chat view"
|
||||
"context": "app:chat"
|
||||
},
|
||||
"noHostSelected": "No host selected, open setting to set one",
|
||||
"noHostSelected": "No host selected, open settings to set one",
|
||||
"@noHostSelected": {
|
||||
"description": "Text displayed when no host is selected",
|
||||
"context": "Visible in the chat view, opens the settings dialog when clicked"
|
||||
"context": "app:chat"
|
||||
},
|
||||
"noSelectedModel": "<selector>",
|
||||
"@noSelectedModel": {
|
||||
"description": "Text displayed when no model is selected",
|
||||
"context": "Visible in the chat view, opens the model dialog when clicked"
|
||||
"context": "app:chat"
|
||||
},
|
||||
"newChatTitle": "Unnamed Chat",
|
||||
"@newChatTitle": {
|
||||
"description": "Title of a new chat",
|
||||
"context": "Visible in the new chat dialog"
|
||||
"context": "app:chat:title"
|
||||
},
|
||||
"modelDialogAddModel": "Add",
|
||||
"@modelDialogAddModel": {
|
||||
"description": "Text displayed for add model button",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddPromptTitle": "Add new model",
|
||||
"@modelDialogAddPromptTitle": {
|
||||
"description": "Title of the add model dialog",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddPromptDescription": "This can have either be a normal name (e.g. 'llama3') or name and tag (e.g. 'llama3:70b').",
|
||||
"modelDialogAddPromptDescription": "This can either be a normal name (e.g. 'llama3') or a name and tag (e.g. 'llama3:70b').",
|
||||
"@modelDialogAddPromptDescription": {
|
||||
"description": "Description of the add model dialog",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddPromptAlreadyExists": "Model already exists",
|
||||
"@modelDialogAddPromptAlreadyExists": {
|
||||
"description": "Text displayed when the model already exists",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddPromptInvalid": "Invalid model name",
|
||||
"@modelDialogAddPromptInvalid": {
|
||||
"description": "Text displayed when the model name is invalid",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddAllowanceTitle": "Allow Proxy",
|
||||
"@modelDialogAddAllowanceTitle": {
|
||||
"description": "Title of the allow proxy dialog",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"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.",
|
||||
"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",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddAllowanceAllow": "Allow",
|
||||
"@modelDialogAddAllowanceAllow": {
|
||||
"description": "Text displayed for allow button, should be capitalized",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddAllowanceDeny": "Deny",
|
||||
"@modelDialogAddAllowanceDeny": {
|
||||
"description": "Text displayed for deny button, should be capitalized",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddAssuranceTitle": "Add {model}?",
|
||||
"@modelDialogAddAssuranceTitle": {
|
||||
"description": "Title of the add model assurance dialog",
|
||||
"context": "Visible in the model dialog",
|
||||
"context": "app:modelDialog",
|
||||
"placeholders": {
|
||||
"model": {
|
||||
"type": "String",
|
||||
|
@ -201,10 +196,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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'll resume if you enter the name into the model dialog again.",
|
||||
"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": "Visible in the model dialog",
|
||||
"context": "app:modelDialog",
|
||||
"placeholders": {
|
||||
"model": {
|
||||
"type": "String",
|
||||
|
@ -215,22 +210,22 @@
|
|||
"modelDialogAddAssuranceAdd": "Add",
|
||||
"@modelDialogAddAssuranceAdd": {
|
||||
"description": "Text displayed for add button, should be capitalized",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddAssuranceCancel": "Cancel",
|
||||
"@modelDialogAddAssuranceCancel": {
|
||||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog"
|
||||
},
|
||||
"modelDialogAddDownloadPercentLoading": "loading progress",
|
||||
"@modelDialogAddDownloadPercentLoading": {
|
||||
"description": "Text displayed while loading the download progress",
|
||||
"context": "Visible in the model dialog; 'loading progress in the moment'"
|
||||
"context": "app:modelDialog:download"
|
||||
},
|
||||
"modelDialogAddDownloadPercent": "download at {percent}%",
|
||||
"@modelDialogAddDownloadPercent": {
|
||||
"description": "Text displayed while downloading a model",
|
||||
"context": "Visible in the model dialog; download is at x percent",
|
||||
"context": "app:modelDialog:download",
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "String",
|
||||
|
@ -241,162 +236,187 @@
|
|||
"modelDialogAddDownloadFailed": "Disconnected, try again",
|
||||
"@modelDialogAddDownloadFailed": {
|
||||
"description": "Text displayed when the download of a model fails",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog:download"
|
||||
},
|
||||
"modelDialogAddDownloadSuccess": "Download successful",
|
||||
"@modelDialogAddDownloadSuccess": {
|
||||
"description": "Text displayed when the download of a model is successful",
|
||||
"context": "Visible in the model dialog"
|
||||
"context": "app:modelDialog:download"
|
||||
},
|
||||
"deleteDialogTitle": "Delete Chat",
|
||||
"@deleteDialogTitle": {
|
||||
"description": "Title of the delete dialog",
|
||||
"context": "Visible in the delete dialog"
|
||||
"context": "app:deleteDialog"
|
||||
},
|
||||
"deleteDialogDescription": "Are you sure you want to continue? This will wipe all memory of this chat and cannot be undone.\nTo disable this dialog, visit the settings.",
|
||||
"@deleteDialogDescription": {
|
||||
"description": "Description of the delete dialog",
|
||||
"context": "Visible in the delete dialog"
|
||||
"context": "app:deleteDialog"
|
||||
},
|
||||
"deleteDialogDelete": "Delete",
|
||||
"@deleteDialogDelete": {
|
||||
"description": "Text displayed for delete button, should be capitalized",
|
||||
"context": "Visible in the delete dialog"
|
||||
"context": "app:deleteDialog"
|
||||
},
|
||||
"deleteDialogCancel": "Cancel",
|
||||
"@deleteDialogCancel": {
|
||||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the delete dialog"
|
||||
"context": "app:deleteDialog"
|
||||
},
|
||||
"errorGuardTitle": "ErrorGuard",
|
||||
"@errorGuardTitle": {
|
||||
"description": "Title of the error guard dialog. Do not translate if not required!",
|
||||
"context": "app:errorGuard"
|
||||
},
|
||||
"errorGuardDetails": "Details",
|
||||
"@errorGuardDetails": {
|
||||
"description": "Text displayed for details button and the details section in the error guard dialog",
|
||||
"context": "app:errorGuard"
|
||||
},
|
||||
"errorGuardException": "Exception",
|
||||
"@errorGuardException": {
|
||||
"description": "Text displayed for exception section in the error guard dialog. Reference: https://en.wikipedia.org/wiki/Exception_handling",
|
||||
"context": "app:errorGuard"
|
||||
},
|
||||
"errorGuardStackTrace": "Stack Trace",
|
||||
"@errorGuardStackTrace": {
|
||||
"description": "Text displayed for stack trace section in the error guard dialog. Use the commonly agreed on spelling in your language, e.g. 'Stacktrace' in German; use Wikipedia as reference: https://en.wikipedia.org/wiki/Stack_trace",
|
||||
"context": "app:errorGuard"
|
||||
},
|
||||
"errorGuardReport": "Report",
|
||||
"@errorGuardReport": {
|
||||
"description": "Text displayed for report button in the error guard dialog",
|
||||
"context": "app:errorGuard"
|
||||
},
|
||||
"dialogEnterNewTitle": "Enter new title",
|
||||
"@dialogEnterNewTitle": {
|
||||
"description": "Text displayed as description for new title input",
|
||||
"context": "Visible in the rename dialog"
|
||||
"context": "app:renameDialog"
|
||||
},
|
||||
"dialogEditMessageTitle": "Edit message",
|
||||
"@dialogEditMessageTitle": {
|
||||
"description": "Title of the edit message dialog",
|
||||
"context": "Visible in the edit message dialog"
|
||||
"context": "app:editMessageDialog"
|
||||
},
|
||||
"settingsTitleBehavior": "Behavior",
|
||||
"@settingsTitleBehavior": {
|
||||
"description": "Title of the behavior settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsDescriptionBehavior": "Change the behavior of the AI to your liking.",
|
||||
"@settingsDescriptionBehavior": {
|
||||
"description": "Description of the behavior settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsTitleInterface": "Interface",
|
||||
"@settingsTitleInterface": {
|
||||
"description": "Title of the interface settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsDescriptionInterface": "Edit how Ollama App looks and behaves.",
|
||||
"@settingsDescriptionInterface": {
|
||||
"description": "Description of the interface settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsTitleVoice": "Voice",
|
||||
"@settingsTitleVoice": {
|
||||
"description": "Title of the voice settings section. Do not translate if not required!",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsDescriptionVoice": "Enable voice mode and configure voice settings.",
|
||||
"@settingsDescriptionVoice": {
|
||||
"description": "Description of the voice settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsTitleExport": "Export",
|
||||
"@settingsTitleExport": {
|
||||
"description": "Title of the export settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsDescriptionExport": "Export and import your chat history.",
|
||||
"@settingsDescriptionExport": {
|
||||
"description": "Description of the export settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsTitleAbout": "About",
|
||||
"@settingsTitleAbout": {
|
||||
"description": "Title of the about settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsDescriptionAbout": "Check for updates and learn more about Ollama App.",
|
||||
"@settingsDescriptionAbout": {
|
||||
"description": "Description of the about settings section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsSavedAutomatically": "Settings are saved automatically",
|
||||
"@settingsSavedAutomatically": {
|
||||
"description": "Text displayed when settings are saved automatically",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsExperimentalAlpha": "alpha",
|
||||
"@settingsExperimentalAlpha": {
|
||||
"description": "Text displayed when a feature is in alpha",
|
||||
"context": "Visible in the settings view"
|
||||
"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": {
|
||||
"description": "Description of the alpha feature",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsExperimentalAlphaFeature": "Alpha feature, hold to learn more",
|
||||
"@settingsExperimentalAlphaFeature": {
|
||||
"description": "Text displayed when a feature is in alpha",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsExperimentalBeta": "beta",
|
||||
"@settingsExperimentalBeta": {
|
||||
"description": "Text displayed when a feature is in beta",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsExperimentalBetaDescription": "This feature is in beta and may not work 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": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsExperimentalBetaFeature": "Beta feature, hold to learn more",
|
||||
"@settingsExperimentalBetaFeature": {
|
||||
"description": "Text displayed when a feature is in beta",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsExperimentalDeprecated": "deprecated",
|
||||
"@settingsExperimentalDeprecated": {
|
||||
"description": "Text displayed when a feature is deprecated",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsExperimentalDeprecatedDescription": "This feature is deprecated and will be removed in a future version.\nIt may not work as intended or expected. Use at your own risk.",
|
||||
"@settingsExperimentalDeprecatedDescription": {
|
||||
"description": "Description of the deprecated feature",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsExperimentalDeprecatedFeature": "Deprecated feature, hold to learn more",
|
||||
"@settingsExperimentalDeprecatedFeature": {
|
||||
"description": "Text displayed when a feature is deprecated",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings"
|
||||
},
|
||||
"settingsHost": "Host",
|
||||
"@settingsHost": {
|
||||
"description": "Text displayed as description for host input",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:host"
|
||||
},
|
||||
"settingsHostValid": "Valid Host",
|
||||
"@settingsHostValid": {
|
||||
"description": "Text displayed when the host is valid",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:host"
|
||||
},
|
||||
"settingsHostChecking": "Checking Host",
|
||||
"@settingsHostChecking": {
|
||||
"description": "Text displayed when the host is being checked",
|
||||
"context": "Visible in the settings view"
|
||||
"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": "Issue: {type, select, url{Invalid URL} host{Invalid Host} timeout{Request failed. Server issues} ratelimit{Too many requests} other{Request Failed}}",
|
||||
"@settingsHostInvalid": {
|
||||
"description": "Text displayed when the host is invalid",
|
||||
"context": "Visible in the settings view",
|
||||
"context": "app:settings:host",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"type": "String",
|
||||
|
@ -404,20 +424,25 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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": "Visible in the settings view"
|
||||
"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": "Visible in the settings view"
|
||||
"context": "app:settings:host"
|
||||
},
|
||||
"settingsHostInvalidDetailed": "{type, select, url{The URL you entered is invalid. It isn't an 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, 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": {
|
||||
"description": "Text displayed when the host is invalid",
|
||||
"context": "Visible in the settings view",
|
||||
"context": "app:settings:host",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"type": "String",
|
||||
|
@ -428,92 +453,92 @@
|
|||
"settingsSystemMessage": "System message",
|
||||
"@settingsSystemMessage": {
|
||||
"description": "Text displayed as description for system message input",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:behavior"
|
||||
},
|
||||
"settingsUseSystem": "Use system message",
|
||||
"@settingsUseSystem": {
|
||||
"description": "Text displayed as description for use system message toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:behavior"
|
||||
},
|
||||
"settingsUseSystemDescription": "Disables setting the system message above and use the one of the model 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": "Visible in the settings view by long pressing the toggle"
|
||||
"context": "app:settings:behavior"
|
||||
},
|
||||
"settingsDisableMarkdown": "Disable markdown",
|
||||
"@settingsDisableMarkdown": {
|
||||
"description": "Text displayed as description for disable markdown toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:behavior"
|
||||
},
|
||||
"settingsBehaviorNotUpdatedForOlderChats": "Behavior settings are not updated for older chats",
|
||||
"@settingsBehaviorNotUpdatedForOlderChats": {
|
||||
"description": "Text displayed when behavior settings are not updated for older chats",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:behavior"
|
||||
},
|
||||
"settingsShowModelTags": "Show model tags",
|
||||
"@settingsShowModelTags": {
|
||||
"description": "Text displayed as description for show model tags toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsPreloadModels": "Preload models",
|
||||
"@settingsPreloadModels": {
|
||||
"description": "Text displayed as description for preload models toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsResetOnModelChange": "Reset on model change",
|
||||
"@settingsResetOnModelChange": {
|
||||
"description": "Text displayed as description for reset on model change toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsRequestTypeStream": "Stream",
|
||||
"@settingsRequestTypeStream": {
|
||||
"description": "Text displayed as description for stream request type. Do not translate if not required!",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:request"
|
||||
},
|
||||
"settingsRequestTypeRequest": "Request",
|
||||
"@settingsRequestTypeRequest": {
|
||||
"description": "Text displayed as description for request request type. Do not translate if not required!",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:request"
|
||||
},
|
||||
"settingsGenerateTitles": "Generate titles",
|
||||
"@settingsGenerateTitles": {
|
||||
"description": "Text displayed as description for generate titles toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsEnableEditing": "Message editing",
|
||||
"@settingsEnableEditing": {
|
||||
"description": "Text displayed as description for enable editing toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsAskBeforeDelete": "Ask before chat deletion",
|
||||
"@settingsAskBeforeDelete": {
|
||||
"description": "Text displayed as description for ask before deletion toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsShowTips": "Show tips in sidebar",
|
||||
"@settingsShowTips": {
|
||||
"description": "Text displayed as description for show tips toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsKeepModelLoadedAlways": "Keep model always loaded",
|
||||
"@settingsKeepModelLoadedAlways": {
|
||||
"description": "Text displayed as description for keep model loaded always toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:keepModelLoaded"
|
||||
},
|
||||
"settingsKeepModelLoadedNever": "Don't keep model loaded",
|
||||
"@settingsKeepModelLoadedNever": {
|
||||
"description": "Text displayed as description for don't keep model loaded toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:keepModelLoaded"
|
||||
},
|
||||
"settingsKeepModelLoadedFor": "Set specific time to keep model loaded",
|
||||
"@settingsKeepModelLoadedFor": {
|
||||
"description": "Text displayed as description for keep model loaded for toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:keepModelLoaded"
|
||||
},
|
||||
"settingsKeepModelLoadedSet": "Keep model loaded for {minutes} minutes",
|
||||
"@settingsKeepModelLoadedSet": {
|
||||
"description": "Text displayed as description for keep model loaded for set time toggle",
|
||||
"context": "Visible in the settings view",
|
||||
"context": "app:settings:interface:keepModelLoaded",
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String",
|
||||
|
@ -524,192 +549,192 @@
|
|||
"settingsTimeoutMultiplier": "Timeout multiplier",
|
||||
"@settingsTimeoutMultiplier": {
|
||||
"description": "Text displayed as title for the timeout multiplier section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:timeout"
|
||||
},
|
||||
"settingsTimeoutMultiplierDescription": "Select the multiplier that is applied to every timeout value in the app. Can be useful with a slow internet connection or a slow host.",
|
||||
"@settingsTimeoutMultiplierDescription": {
|
||||
"description": "Description of the timeout multiplier section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:timeout"
|
||||
},
|
||||
"settingsTimeoutMultiplierExample": "E.g. message timeout:",
|
||||
"@settingsTimeoutMultiplierExample": {
|
||||
"description": "Example for the timeout multiplier",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:timeout"
|
||||
},
|
||||
"settingsEnableHapticFeedback": "Enable haptic feedback",
|
||||
"@settingsEnableHapticFeedback": {
|
||||
"description": "Text displayed as description for enable haptic feedback toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsMaximizeOnStart": "Start maximized",
|
||||
"@settingsMaximizeOnStart": {
|
||||
"description": "Text displayed as description for maximize on start toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface"
|
||||
},
|
||||
"settingsBrightnessSystem": "System",
|
||||
"@settingsBrightnessSystem": {
|
||||
"description": "Text displayed as description for system brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:brightness"
|
||||
},
|
||||
"settingsBrightnessLight": "Light",
|
||||
"@settingsBrightnessLight": {
|
||||
"description": "Text displayed as description for light brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:brightness"
|
||||
},
|
||||
"settingsBrightnessDark": "Dark",
|
||||
"@settingsBrightnessDark": {
|
||||
"description": "Text displayed as description for dark brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:brightness"
|
||||
},
|
||||
"settingsThemeDevice": "Device",
|
||||
"@settingsThemeDevice": {
|
||||
"description": "Text displayed as description for device theme option",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:theme"
|
||||
},
|
||||
"settingsThemeOllama": "Ollama",
|
||||
"@settingsThemeOllama": {
|
||||
"description": "Text displayed as description for Ollama theme option",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:theme"
|
||||
},
|
||||
"settingsTemporaryFixes": "Temporary interface fixes",
|
||||
"@settingsTemporaryFixes": {
|
||||
"description": "Text displayed as description for temporary fixes section",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:temporaryFixes"
|
||||
},
|
||||
"settingsTemporaryFixesDescription": "Enable temporary fixes for interface issues.\nLong press on the individual options to learn more.",
|
||||
"@settingsTemporaryFixesDescription": {
|
||||
"description": "Description of the temporary fixes section",
|
||||
"context": "Visible in the settings view"
|
||||
"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": {
|
||||
"description": "Instructions and warnings for the temporary fixes",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:temporaryFixes"
|
||||
},
|
||||
"settingsTemporaryFixesNoFixes": "No fixes available",
|
||||
"@settingsTemporaryFixesNoFixes": {
|
||||
"description": "Text displayed when no fixes are available",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:interface:temporaryFixes"
|
||||
},
|
||||
"settingsVoicePermissionLoading": "Loading voice permissions ...",
|
||||
"@settingsVoicePermissionLoading": {
|
||||
"description": "Text displayed while loading voice permissions",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoiceTtsNotSupported": "Text-to-speech not supported",
|
||||
"@settingsVoiceTtsNotSupported": {
|
||||
"description": "Text displayed when text-to-speech is not supported",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoiceTtsNotSupportedDescription": "Text-to-speech services are not supported for the selected language. Select a different language in the language drawer to reenable them.\nOther services like voice recognition and AI thinking will still work as usual, but interaction might not be as fluent.",
|
||||
"settingsVoiceTtsNotSupportedDescription": "Text-to-speech services are not supported for the selected language. Select a different language in the language drawer to re-enable them.\nOther services like voice recognition and AI thinking will still work as usual, but interaction might not be as fluent.",
|
||||
"@settingsVoiceTtsNotSupportedDescription": {
|
||||
"description": "Description of the text-to-speech not supported message",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoicePermissionNot": "Permissions not granted",
|
||||
"@settingsVoicePermissionNot": {
|
||||
"description": "Text displayed when voice permissions are not granted",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoiceNotEnabled": "Voice mode not enabled",
|
||||
"@settingsVoiceNotEnabled": {
|
||||
"description": "Text displayed when voice mode is not enabled",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoiceNotSupported": "Voice mode not supported",
|
||||
"@settingsVoiceNotSupported": {
|
||||
"description": "Text displayed when voice mode is not supported",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoiceEnable": "Enable voice mode",
|
||||
"@settingsVoiceEnable": {
|
||||
"description": "Text displayed as description for enable voice mode toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoiceNoLanguage": "No language selected",
|
||||
"@settingsVoiceNoLanguage": {
|
||||
"description": "Text displayed when no language is selected",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoiceLimitLanguage": "Limit to selected language",
|
||||
"@settingsVoiceLimitLanguage": {
|
||||
"description": "Text displayed as description for limit language toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsVoicePunctuation": "Enable AI punctuation",
|
||||
"@settingsVoicePunctuation": {
|
||||
"description": "Text displayed as description for enable AI punctuation toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:voice"
|
||||
},
|
||||
"settingsExportChats": "Export chats",
|
||||
"@settingsExportChats": {
|
||||
"description": "Text displayed as description for export chats button",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsExportChatsSuccess": "Chats exported successfully",
|
||||
"@settingsExportChatsSuccess": {
|
||||
"description": "Text displayed when chats are exported successfully",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsImportChats": "Import chats",
|
||||
"@settingsImportChats": {
|
||||
"description": "Text displayed as description for import chats button",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsImportChatsTitle": "Import",
|
||||
"@settingsImportChatsTitle": {
|
||||
"description": "Title of the import dialog",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsImportChatsDescription": "The following step will import the chats from the selected file. This will overwrite all currently available chats.\nDo you want to continue?",
|
||||
"@settingsImportChatsDescription": {
|
||||
"description": "Description of the import dialog",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsImportChatsImport": "Import and Erase",
|
||||
"@settingsImportChatsImport": {
|
||||
"description": "Text displayed for import button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsImportChatsCancel": "Cancel",
|
||||
"@settingsImportChatsCancel": {
|
||||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsImportChatsSuccess": "Chats imported successfully",
|
||||
"@settingsImportChatsSuccess": {
|
||||
"description": "Text displayed when chats are imported successfully",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsExportInfo": "This options allows you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or backup 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": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsExportWarning": "Multiple chat histories won't be merged! You'll loose 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": "Visible in the settings view"
|
||||
"context": "app:settings:export"
|
||||
},
|
||||
"settingsUpdateCheck": "Check for updates",
|
||||
"@settingsUpdateCheck": {
|
||||
"description": "Text displayed as description for check for updates button",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateChecking": "Checking for updates ...",
|
||||
"@settingsUpdateChecking": {
|
||||
"description": "Text displayed while looking for updates",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateLatest": "You are on the latest version",
|
||||
"@settingsUpdateLatest": {
|
||||
"description": "Text displayed when the app is up to date",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateAvailable": "Update available (v{version})",
|
||||
"@settingsUpdateAvailable": {
|
||||
"description": "Text displayed when an update is available",
|
||||
"context": "Visible in the settings view",
|
||||
"context": "app:settings:info",
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String",
|
||||
|
@ -720,62 +745,62 @@
|
|||
"settingsUpdateRateLimit": "Can't check, API rate limit exceeded",
|
||||
"@settingsUpdateRateLimit": {
|
||||
"description": "Text displayed when the API rate limit is exceeded",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateIssue": "An issue occurred",
|
||||
"@settingsUpdateIssue": {
|
||||
"description": "Text displayed when an issue occurs while checking for updates",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateDialogTitle": "New version available",
|
||||
"@settingsUpdateDialogTitle": {
|
||||
"description": "Title of the update dialog",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateDialogDescription": "A new version of Ollama is available. Do you want to download and install it now?",
|
||||
"settingsUpdateDialogDescription": "A new version of Ollama App is available. Do you want to download and install it now?",
|
||||
"@settingsUpdateDialogDescription": {
|
||||
"description": "Description of the update dialog",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateChangeLog": "Change Log",
|
||||
"@settingsUpdateChangeLog": {
|
||||
"description": "Text displayed as description for change log button",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateDialogUpdate": "Update",
|
||||
"@settingsUpdateDialogUpdate": {
|
||||
"description": "Text displayed for update button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsUpdateDialogCancel": "Cancel",
|
||||
"@settingsUpdateDialogCancel": {
|
||||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsCheckForUpdates": "Check for updates on open",
|
||||
"@settingsCheckForUpdates": {
|
||||
"description": "Text displayed as description for check for updates toggle",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsGithub": "GitHub",
|
||||
"@settingsGithub": {
|
||||
"description": "Text displayed as description for GitHub button",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsReportIssue": "Report Issue",
|
||||
"@settingsReportIssue": {
|
||||
"description": "Text displayed as description for report issue button",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsLicenses": "Licenses",
|
||||
"@settingsLicenses": {
|
||||
"description": "Text displayed as description for licenses button",
|
||||
"context": "Visible in the settings view"
|
||||
"context": "app:settings:info"
|
||||
},
|
||||
"settingsVersion": "Ollama App v{version}",
|
||||
"@settingsVersion": {
|
||||
"description": "Text displayed as description for version",
|
||||
"context": "Visible in the settings view",
|
||||
"context": "app:settings:info",
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String",
|
||||
|
|
|
@ -103,7 +103,7 @@ abstract class AppLocalizations {
|
|||
Locale('fa'),
|
||||
Locale('it'),
|
||||
Locale('tr'),
|
||||
Locale('zh')
|
||||
Locale('zh'),
|
||||
];
|
||||
|
||||
/// Title of the application
|
||||
|
@ -145,7 +145,7 @@ abstract class AppLocalizations {
|
|||
/// First tip displayed in the sidebar
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit messages by long taping on them'**
|
||||
/// **'Edit messages by long tapping on them'**
|
||||
String get tip0;
|
||||
|
||||
/// Second tip displayed in the sidebar
|
||||
|
@ -238,12 +238,6 @@ abstract class AppLocalizations {
|
|||
/// **'Let AI think'**
|
||||
String get tooltipLetAIThink;
|
||||
|
||||
/// Tooltip for add host headers button
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add host headers'**
|
||||
String get tooltipAddHostHeaders;
|
||||
|
||||
/// Tooltip for reset button
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
@ -265,7 +259,7 @@ abstract class AppLocalizations {
|
|||
/// Text displayed when no host is selected
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No host selected, open setting to set one'**
|
||||
/// **'No host selected, open settings to set one'**
|
||||
String get noHostSelected;
|
||||
|
||||
/// Text displayed when no model is selected
|
||||
|
@ -295,7 +289,7 @@ abstract class AppLocalizations {
|
|||
/// Description of the add model dialog
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This can have either be a normal name (e.g. \'llama3\') or name and tag (e.g. \'llama3:70b\').'**
|
||||
/// **'This can either be a normal name (e.g. \'llama3\') or a name and tag (e.g. \'llama3:70b\').'**
|
||||
String get modelDialogAddPromptDescription;
|
||||
|
||||
/// Text displayed when the model already exists
|
||||
|
@ -319,7 +313,7 @@ abstract class AppLocalizations {
|
|||
/// Description of the allow proxy dialog
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'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.'**
|
||||
/// **'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;
|
||||
|
||||
/// Text displayed for allow button, should be capitalized
|
||||
|
@ -343,7 +337,7 @@ abstract class AppLocalizations {
|
|||
/// 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\'ll resume if you enter the name into the model dialog again.'**
|
||||
/// **'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
|
||||
|
@ -406,6 +400,36 @@ abstract class AppLocalizations {
|
|||
/// **'Cancel'**
|
||||
String get deleteDialogCancel;
|
||||
|
||||
/// Title of the error guard dialog. Do not translate if not required!
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'ErrorGuard'**
|
||||
String get errorGuardTitle;
|
||||
|
||||
/// Text displayed for details button and the details section in the error guard dialog
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Details'**
|
||||
String get errorGuardDetails;
|
||||
|
||||
/// Text displayed for exception section in the error guard dialog. Reference: https://en.wikipedia.org/wiki/Exception_handling
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Exception'**
|
||||
String get errorGuardException;
|
||||
|
||||
/// Text displayed for stack trace section in the error guard dialog. Use the commonly agreed on spelling in your language, e.g. 'Stacktrace' in German; use Wikipedia as reference: https://en.wikipedia.org/wiki/Stack_trace
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stack Trace'**
|
||||
String get errorGuardStackTrace;
|
||||
|
||||
/// Text displayed for report button in the error guard dialog
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Report'**
|
||||
String get errorGuardReport;
|
||||
|
||||
/// Text displayed as description for new title input
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
@ -484,7 +508,7 @@ abstract class AppLocalizations {
|
|||
/// **'Settings are saved automatically'**
|
||||
String get settingsSavedAutomatically;
|
||||
|
||||
/// Text displayed when a feature is in alpha
|
||||
/// Text displayed when a feature is in alpha phase
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'alpha'**
|
||||
|
@ -511,7 +535,7 @@ abstract class AppLocalizations {
|
|||
/// Description of the beta feature
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This feature is in beta and may not work 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
|
||||
|
@ -559,9 +583,15 @@ abstract class AppLocalizations {
|
|||
/// Text 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}}'**
|
||||
/// **'Issue: {type, select, url{Invalid URL} host{Invalid Host} timeout{Request failed. Server issues} ratelimit{Too many requests} other{Request Failed}}'**
|
||||
String settingsHostInvalid(String type);
|
||||
|
||||
/// Tooltip for add host headers button
|
||||
///
|
||||
/// 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:
|
||||
|
@ -577,7 +607,7 @@ abstract class AppLocalizations {
|
|||
/// 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 an a standardized URL format.} other{The host you entered is invalid. It cannot be reached. Please check the host and try again.}}'**
|
||||
/// **'{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.}}'**
|
||||
String settingsHostInvalidDetailed(String type);
|
||||
|
||||
/// Text displayed as description for system message input
|
||||
|
@ -595,7 +625,7 @@ abstract class AppLocalizations {
|
|||
/// Description of the use system message toggle
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Disables setting the system message above and use the one of the model 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
|
||||
|
@ -787,7 +817,7 @@ abstract class AppLocalizations {
|
|||
/// Description of the text-to-speech not supported message
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Text-to-speech services are not supported for the selected language. Select a different language in the language drawer to reenable them.\nOther services like voice recognition and AI thinking will still work as usual, but interaction might not be as fluent.'**
|
||||
/// **'Text-to-speech services are not supported for the selected language. Select a different language in the language drawer to re-enable them.\nOther services like voice recognition and AI thinking will still work as usual, but interaction might not be as fluent.'**
|
||||
String get settingsVoiceTtsNotSupportedDescription;
|
||||
|
||||
/// Text displayed when voice permissions are not granted
|
||||
|
@ -883,13 +913,13 @@ abstract class AppLocalizations {
|
|||
/// Information displayed for export and import options
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This options allows you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or backup 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 loose 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
|
||||
|
@ -937,7 +967,7 @@ abstract class AppLocalizations {
|
|||
/// Description of the update dialog
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A new version of Ollama is available. Do you want to download and install it now?'**
|
||||
/// **'A new version of Ollama App is available. Do you want to download and install it now?'**
|
||||
String get settingsUpdateDialogDescription;
|
||||
|
||||
/// Text displayed as description for change log button
|
||||
|
@ -1005,7 +1035,7 @@ class _AppLocalizationsDelegate
|
|||
'fa',
|
||||
'it',
|
||||
'tr',
|
||||
'zh'
|
||||
'zh',
|
||||
].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
|
@ -1033,5 +1063,6 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
|
|||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.');
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
|
|
|
@ -74,9 +74,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get tooltipLetAIThink => 'Lass KI denken';
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Host-Header festlegen';
|
||||
|
||||
@override
|
||||
String get tooltipReset => 'Aktuellen Chat zurücksetzen';
|
||||
|
||||
|
@ -168,6 +165,21 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get deleteDialogCancel => 'Abbrechen';
|
||||
|
||||
@override
|
||||
String get errorGuardTitle => 'ErrorGuard';
|
||||
|
||||
@override
|
||||
String get errorGuardDetails => 'Details';
|
||||
|
||||
@override
|
||||
String get errorGuardException => 'Exception';
|
||||
|
||||
@override
|
||||
String get errorGuardStackTrace => 'Stack Trace';
|
||||
|
||||
@override
|
||||
String get errorGuardReport => 'Report';
|
||||
|
||||
@override
|
||||
String get dialogEnterNewTitle => 'Gib bitte einen neuen Titel ein';
|
||||
|
||||
|
@ -257,19 +269,19 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalid(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
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',
|
||||
},
|
||||
);
|
||||
});
|
||||
return 'Fehler: $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Host-Header festlegen';
|
||||
|
||||
@override
|
||||
String get settingsHostHeaderTitle => 'Host-Header festlegen';
|
||||
|
||||
|
@ -279,15 +291,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalidDetailed(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
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.',
|
||||
},
|
||||
);
|
||||
});
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get tipPrefix => 'Tip: ';
|
||||
|
||||
@override
|
||||
String get tip0 => 'Edit messages by long taping on them';
|
||||
String get tip0 => 'Edit messages by long tapping on them';
|
||||
|
||||
@override
|
||||
String get tip1 => 'Delete messages by double tapping on them';
|
||||
|
@ -74,9 +74,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get tooltipLetAIThink => 'Let AI think';
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Add host headers';
|
||||
|
||||
@override
|
||||
String get tooltipReset => 'Reset current chat';
|
||||
|
||||
|
@ -87,7 +84,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get noModelSelected => 'No model selected';
|
||||
|
||||
@override
|
||||
String get noHostSelected => 'No host selected, open setting to set one';
|
||||
String get noHostSelected => 'No host selected, open settings to set one';
|
||||
|
||||
@override
|
||||
String get noSelectedModel => '<selector>';
|
||||
|
@ -103,7 +100,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get modelDialogAddPromptDescription =>
|
||||
'This can have either be a normal name (e.g. \'llama3\') or name and tag (e.g. \'llama3:70b\').';
|
||||
'This can either be a normal name (e.g. \'llama3\') or a name and tag (e.g. \'llama3:70b\').';
|
||||
|
||||
@override
|
||||
String get modelDialogAddPromptAlreadyExists => 'Model already exists';
|
||||
|
@ -116,7 +113,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@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.';
|
||||
'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.';
|
||||
|
||||
@override
|
||||
String get modelDialogAddAllowanceAllow => 'Allow';
|
||||
|
@ -131,7 +128,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@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.';
|
||||
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.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -167,6 +164,21 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get deleteDialogCancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get errorGuardTitle => 'ErrorGuard';
|
||||
|
||||
@override
|
||||
String get errorGuardDetails => 'Details';
|
||||
|
||||
@override
|
||||
String get errorGuardException => 'Exception';
|
||||
|
||||
@override
|
||||
String get errorGuardStackTrace => 'Stack Trace';
|
||||
|
||||
@override
|
||||
String get errorGuardReport => 'Report';
|
||||
|
||||
@override
|
||||
String get dialogEnterNewTitle => 'Enter new title';
|
||||
|
||||
|
@ -227,7 +239,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settingsExperimentalBetaDescription =>
|
||||
'This feature is in beta and may not work 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 =>
|
||||
|
@ -255,19 +267,19 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalid(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url': 'Invalid URL',
|
||||
'host': 'Invalid Host',
|
||||
'timeout': 'Request Failed. Server issues',
|
||||
'timeout': 'Request failed. Server issues',
|
||||
'ratelimit': 'Too many requests',
|
||||
'other': 'Request Failed',
|
||||
},
|
||||
);
|
||||
});
|
||||
return 'Issue: $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Add host headers';
|
||||
|
||||
@override
|
||||
String get settingsHostHeaderTitle => 'Set host header';
|
||||
|
||||
|
@ -277,15 +289,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalidDetailed(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url':
|
||||
'The URL you entered is invalid. It isn\'t an a standardized URL format.',
|
||||
'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.',
|
||||
},
|
||||
);
|
||||
});
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
|
@ -297,7 +306,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settingsUseSystemDescription =>
|
||||
'Disables setting the system message above and use the one of the model 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';
|
||||
|
@ -401,7 +410,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settingsVoiceTtsNotSupportedDescription =>
|
||||
'Text-to-speech services are not supported for the selected language. Select a different language in the language drawer to reenable them.\nOther services like voice recognition and AI thinking will still work as usual, but interaction might not be as fluent.';
|
||||
'Text-to-speech services are not supported for the selected language. Select a different language in the language drawer to re-enable them.\nOther services like voice recognition and AI thinking will still work as usual, but interaction might not be as fluent.';
|
||||
|
||||
@override
|
||||
String get settingsVoicePermissionNot => 'Permissions not granted';
|
||||
|
@ -451,11 +460,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settingsExportInfo =>
|
||||
'This options allows you to export and import your chat history. This can be useful if you want to transfer your chat history to another device or backup 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 loose 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';
|
||||
|
@ -482,7 +491,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settingsUpdateDialogDescription =>
|
||||
'A new version of Ollama is available. Do you want to download and install it now?';
|
||||
'A new version of Ollama App is available. Do you want to download and install it now?';
|
||||
|
||||
@override
|
||||
String get settingsUpdateChangeLog => 'Change Log';
|
||||
|
|
|
@ -74,9 +74,6 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
@override
|
||||
String get tooltipLetAIThink => 'Let AI think';
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Add host headers';
|
||||
|
||||
@override
|
||||
String get tooltipReset => 'Reset current chat';
|
||||
|
||||
|
@ -167,6 +164,21 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
@override
|
||||
String get deleteDialogCancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get errorGuardTitle => 'ErrorGuard';
|
||||
|
||||
@override
|
||||
String get errorGuardDetails => 'Details';
|
||||
|
||||
@override
|
||||
String get errorGuardException => 'Exception';
|
||||
|
||||
@override
|
||||
String get errorGuardStackTrace => 'Stack Trace';
|
||||
|
||||
@override
|
||||
String get errorGuardReport => 'Report';
|
||||
|
||||
@override
|
||||
String get dialogEnterNewTitle => 'Enter new title';
|
||||
|
||||
|
@ -255,19 +267,19 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalid(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
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',
|
||||
},
|
||||
);
|
||||
});
|
||||
return 'Issue: $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Add host headers';
|
||||
|
||||
@override
|
||||
String get settingsHostHeaderTitle => 'Set host header';
|
||||
|
||||
|
@ -277,15 +289,12 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalidDetailed(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url':
|
||||
'The URL you entered is invalid. It isn\'t an a standardized URL format.',
|
||||
'other':
|
||||
'The host you entered is invalid. It cannot be reached. Please check the host and try again.',
|
||||
},
|
||||
);
|
||||
});
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
|
|
|
@ -75,9 +75,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get tooltipLetAIThink => 'Lasciamo che sia IA a pensare';
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Aggiungi host headers';
|
||||
|
||||
@override
|
||||
String get tooltipReset => 'Reimposta la chat corrente';
|
||||
|
||||
|
@ -170,6 +167,21 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get deleteDialogCancel => 'Annulla';
|
||||
|
||||
@override
|
||||
String get errorGuardTitle => 'ErrorGuard';
|
||||
|
||||
@override
|
||||
String get errorGuardDetails => 'Details';
|
||||
|
||||
@override
|
||||
String get errorGuardException => 'Exception';
|
||||
|
||||
@override
|
||||
String get errorGuardStackTrace => 'Stack Trace';
|
||||
|
||||
@override
|
||||
String get errorGuardReport => 'Report';
|
||||
|
||||
@override
|
||||
String get dialogEnterNewTitle => 'Immetti nuovo titolo';
|
||||
|
||||
|
@ -259,19 +271,19 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalid(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url': 'URL invalido',
|
||||
'host': 'Host invalido',
|
||||
'timeout': 'Richiesta fallita. Problema col server',
|
||||
'ratelimit': 'Troppe richieste',
|
||||
'other': 'Richiesta fallita',
|
||||
},
|
||||
);
|
||||
});
|
||||
return 'Problema: $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Aggiungi host headers';
|
||||
|
||||
@override
|
||||
String get settingsHostHeaderTitle => 'Imposta header host';
|
||||
|
||||
|
@ -281,15 +293,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalidDetailed(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url':
|
||||
'L\'URL inserito non è valido. Non è un formato URL standardizzato.',
|
||||
'other':
|
||||
'L\'host inserito non è valido. Non può essere raggiunto. Controlla l\'host e riprova.',
|
||||
},
|
||||
);
|
||||
});
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
|
|
|
@ -74,9 +74,6 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||
@override
|
||||
String get tooltipLetAIThink => 'AI\'\'nın düşünmesine izin ver';
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Ana bilgisayar başlıkları ekle';
|
||||
|
||||
@override
|
||||
String get tooltipReset => 'Mevcut sohbeti sıfırla';
|
||||
|
||||
|
@ -169,6 +166,21 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||
@override
|
||||
String get deleteDialogCancel => 'İptal';
|
||||
|
||||
@override
|
||||
String get errorGuardTitle => 'ErrorGuard';
|
||||
|
||||
@override
|
||||
String get errorGuardDetails => 'Details';
|
||||
|
||||
@override
|
||||
String get errorGuardException => 'Exception';
|
||||
|
||||
@override
|
||||
String get errorGuardStackTrace => 'Stack Trace';
|
||||
|
||||
@override
|
||||
String get errorGuardReport => 'Report';
|
||||
|
||||
@override
|
||||
String get dialogEnterNewTitle => 'Yeni başlık girin';
|
||||
|
||||
|
@ -257,19 +269,19 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalid(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url': 'Geçersiz URL',
|
||||
'host': 'Geçersiz Ana Bilgisayar',
|
||||
'timeout': 'İstek Başarısız. Sunucu sorunları',
|
||||
'ratelimit': 'Çok fazla istek',
|
||||
'other': 'İstek Başarısız',
|
||||
},
|
||||
);
|
||||
});
|
||||
return 'Sorun: $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => 'Ana bilgisayar başlıkları ekle';
|
||||
|
||||
@override
|
||||
String get settingsHostHeaderTitle => 'Ana bilgisayar başlığını ayarla';
|
||||
|
||||
|
@ -279,14 +291,11 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalidDetailed(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url': 'Girdiğiniz URL geçersiz. Standart bir URL formatında değil.',
|
||||
'other':
|
||||
'Girdiğiniz ana bilgisayar geçersiz. Ulaşılamıyor. Lütfen ana bilgisayarı kontrol edin ve tekrar deneyin.',
|
||||
},
|
||||
);
|
||||
});
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
|
|
|
@ -74,9 +74,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get tooltipLetAIThink => '让AI思考';
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => '设置主机请求头';
|
||||
|
||||
@override
|
||||
String get tooltipReset => '重置当前聊天';
|
||||
|
||||
|
@ -167,6 +164,21 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get deleteDialogCancel => '取消';
|
||||
|
||||
@override
|
||||
String get errorGuardTitle => 'ErrorGuard';
|
||||
|
||||
@override
|
||||
String get errorGuardDetails => 'Details';
|
||||
|
||||
@override
|
||||
String get errorGuardException => 'Exception';
|
||||
|
||||
@override
|
||||
String get errorGuardStackTrace => 'Stack Trace';
|
||||
|
||||
@override
|
||||
String get errorGuardReport => 'Report';
|
||||
|
||||
@override
|
||||
String get dialogEnterNewTitle => '输入新标题';
|
||||
|
||||
|
@ -247,18 +259,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalid(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url': '无效的URL',
|
||||
'host': '无效的主机地址',
|
||||
'timeout': '请求失败。服务器问题',
|
||||
'other': '请求失败',
|
||||
},
|
||||
);
|
||||
});
|
||||
return '问题:$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipAddHostHeaders => '设置主机请求头';
|
||||
|
||||
@override
|
||||
String get settingsHostHeaderTitle => '设置主机请求头';
|
||||
|
||||
|
@ -267,13 +279,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settingsHostInvalidDetailed(String type) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
type,
|
||||
{
|
||||
String _temp0 = intl.Intl.selectLogic(type, {
|
||||
'url': '您输入的 URL 无效。它不是一个标准的 URL 格式。',
|
||||
'other': '您输入的主机地址无效。无法连接。请检查主机地址并再试一次',
|
||||
},
|
||||
);
|
||||
});
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
|
|
1592
lib/main.dart
1592
lib/main.dart
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -11,10 +11,10 @@ import 'package:version/version.dart';
|
|||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../main.dart';
|
||||
import '../services/desktop.dart';
|
||||
import '../services/haptic.dart';
|
||||
import '../services/setter.dart';
|
||||
import '../services/update.dart';
|
||||
import '../worker/desktop.dart';
|
||||
import '../worker/haptic.dart';
|
||||
import '../worker/setter.dart';
|
||||
import '../worker/update.dart';
|
||||
import 'settings/about.dart';
|
||||
import 'settings/behavior.dart';
|
||||
import 'settings/export.dart';
|
||||
|
|
|
@ -7,9 +7,9 @@ import 'package:version/version.dart';
|
|||
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../main.dart';
|
||||
import '../../services/desktop.dart';
|
||||
import '../../services/haptic.dart';
|
||||
import '../../services/update.dart';
|
||||
import '../../worker/desktop.dart';
|
||||
import '../../worker/haptic.dart';
|
||||
import '../../worker/update.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class ScreenSettingsAbout extends StatefulWidget {
|
||||
|
|
|
@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../main.dart';
|
||||
import '../../services/desktop.dart';
|
||||
import '../../services/haptic.dart';
|
||||
import '../../worker/desktop.dart';
|
||||
import '../../worker/haptic.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class ScreenSettingsBehavior extends StatefulWidget {
|
||||
|
|
|
@ -12,8 +12,8 @@ import 'package:universal_html/html.dart' as html;
|
|||
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../main.dart';
|
||||
import '../../services/desktop.dart';
|
||||
import '../../services/haptic.dart';
|
||||
import '../../worker/desktop.dart';
|
||||
import '../../worker/haptic.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class ScreenSettingsExport extends StatefulWidget {
|
||||
|
|
|
@ -7,9 +7,9 @@ import 'package:url_launcher/url_launcher.dart';
|
|||
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../main.dart';
|
||||
import '../../services/desktop.dart';
|
||||
import '../../services/haptic.dart';
|
||||
import '../../services/theme.dart';
|
||||
import '../../worker/desktop.dart';
|
||||
import '../../worker/haptic.dart';
|
||||
import '../../worker/theme.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class ScreenSettingsInterface extends StatefulWidget {
|
||||
|
|
|
@ -7,8 +7,8 @@ import 'package:permission_handler/permission_handler.dart';
|
|||
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../main.dart';
|
||||
import '../../services/haptic.dart';
|
||||
import '../../services/theme.dart';
|
||||
import '../../worker/haptic.dart';
|
||||
import '../../worker/theme.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class ScreenSettingsVoice extends StatefulWidget {
|
||||
|
|
|
@ -6,10 +6,10 @@ import 'package:speech_to_text/speech_to_text.dart' as stt;
|
|||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../main.dart';
|
||||
import '../services/clients.dart';
|
||||
import '../services/haptic.dart';
|
||||
import '../services/sender.dart';
|
||||
import '../services/setter.dart';
|
||||
import '../services/theme.dart';
|
||||
import '../worker/haptic.dart';
|
||||
import '../worker/sender.dart';
|
||||
import '../worker/setter.dart';
|
||||
import '../worker/theme.dart';
|
||||
import 'settings/voice.dart';
|
||||
|
||||
class ScreenVoice extends StatefulWidget {
|
||||
|
|
|
@ -3,7 +3,8 @@ import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
|||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../services/theme.dart';
|
||||
import '../worker/theme.dart';
|
||||
import 'main.dart';
|
||||
|
||||
class ScreenWelcome extends StatefulWidget {
|
||||
const ScreenWelcome({super.key});
|
||||
|
@ -56,8 +57,10 @@ class _ScreenWelcomeState extends State<ScreenWelcome> {
|
|||
curve: Curves.easeInOut);
|
||||
} else {
|
||||
prefs!.setBool("welcomeFinished", true);
|
||||
Navigator.pushReplacement(context,
|
||||
MaterialPageRoute(builder: (context) => const MainApp()));
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ScreenMain()));
|
||||
}
|
||||
},
|
||||
child: (page < 2)
|
||||
|
|
|
@ -0,0 +1,681 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ollama_dart/ollama_dart.dart' as ollama;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../main.dart';
|
||||
import 'clients.dart' as clients;
|
||||
import 'haptic.dart';
|
||||
import 'model.dart';
|
||||
import 'preferences.dart';
|
||||
|
||||
enum MessageSender { user, assistant }
|
||||
|
||||
class Message extends ChangeNotifier {
|
||||
final String id;
|
||||
|
||||
final MessageSender sender;
|
||||
final DateTime createdAt;
|
||||
|
||||
Message({required this.sender, DateTime? createdAt})
|
||||
: id = const Uuid().v4(),
|
||||
createdAt = createdAt ?? DateTime.now();
|
||||
|
||||
void modify() => notifyListeners();
|
||||
|
||||
Map<String, dynamic> toJson() =>
|
||||
throw UnimplementedError("toJson() must be implemented in subclasses");
|
||||
|
||||
@override
|
||||
String toString() => toJson().toString();
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is Message && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
class TextMessage extends Message {
|
||||
bool _locked = false;
|
||||
|
||||
String _content;
|
||||
String get content => _content;
|
||||
|
||||
bool _includesError = false;
|
||||
bool get includesError => _includesError;
|
||||
|
||||
TextMessage(String content, {required super.sender, super.createdAt})
|
||||
: _content = content;
|
||||
|
||||
@override
|
||||
void modify({String? content}) {
|
||||
if (_locked) throw StateError("Cannot modify a locked message.");
|
||||
if (content != null) _content = content;
|
||||
super.modify();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
"type": "text",
|
||||
"role": sender.name,
|
||||
"content": content,
|
||||
"includesError": includesError,
|
||||
"createdAt": createdAt.toUtc().millisecondsSinceEpoch,
|
||||
};
|
||||
|
||||
factory TextMessage.fromStream(
|
||||
Stream<ollama.GenerateChatCompletionResponse> stream, {
|
||||
required MessageSender sender,
|
||||
Completer<void>? completer,
|
||||
void Function(String content)? onContent,
|
||||
}) {
|
||||
return TextMessage("", sender: sender, createdAt: DateTime.now())
|
||||
.._contentFromStream(stream, completer: completer, onContent: onContent);
|
||||
}
|
||||
|
||||
Future<void> _contentFromStream(
|
||||
Stream<ollama.GenerateChatCompletionResponse> stream, {
|
||||
Completer<void>? completer,
|
||||
void Function(String content)? onContent,
|
||||
}) async {
|
||||
assert(!_locked, "Cannot modify a locked message.");
|
||||
_locked = true;
|
||||
|
||||
try {
|
||||
await for (var response in stream) {
|
||||
if (completer?.isCompleted ?? false) return;
|
||||
_content += response.message.content;
|
||||
chatHaptic();
|
||||
|
||||
notifyListeners();
|
||||
onContent?.call(_content);
|
||||
}
|
||||
} catch (e, s) {
|
||||
if (completer?.isCompleted ?? false) return;
|
||||
completer?.completeError(e, s);
|
||||
}
|
||||
if (completer?.isCompleted ?? false) return;
|
||||
_content = _content.trim();
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 250), heavyHaptic);
|
||||
completer?.complete();
|
||||
_locked = false;
|
||||
}
|
||||
}
|
||||
|
||||
class ImageMessage extends Message {
|
||||
final Uri image;
|
||||
final String? name;
|
||||
|
||||
ImageMessage({
|
||||
required this.image,
|
||||
this.name,
|
||||
required super.sender,
|
||||
super.createdAt,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
"type": "image",
|
||||
"role": sender.name,
|
||||
"image": image.toString(),
|
||||
"name": name,
|
||||
"createdAt": createdAt.toUtc().millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
class Chat extends ChangeNotifier {
|
||||
Completer<void>? completer;
|
||||
|
||||
bool get alive => ChatManager.instance.chats.contains(this);
|
||||
bool get active => alive && ChatManager.instance.currentChatId == id;
|
||||
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
|
||||
String? _modelName;
|
||||
String? get modelName => _modelName;
|
||||
set modelName(String? name) {
|
||||
assert(alive, "Chat must be alive to be modified.");
|
||||
|
||||
_modelName = name;
|
||||
if (ChatManager.instance.currentChatId == id) {
|
||||
ModelManager.instance.currentModelName = name;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Model? get model => ModelManager.instance.models.firstOrNullWhere(
|
||||
(m) => m.name == _modelName,
|
||||
);
|
||||
set model(Model? model) => modelName = model?.name;
|
||||
|
||||
String _title;
|
||||
String get title => _title;
|
||||
set title(String title) {
|
||||
assert(alive, "Chat must be alive to be modified.");
|
||||
|
||||
_title = title;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
final Set<Message> _messages;
|
||||
Set<Message> get messages => Set.unmodifiable(_messages);
|
||||
|
||||
final String? system;
|
||||
|
||||
Chat._({
|
||||
required String? modelName,
|
||||
required String title,
|
||||
required this.createdAt,
|
||||
Set<Message>? messages,
|
||||
String? system,
|
||||
}) : id = const Uuid().v4(),
|
||||
_modelName = modelName,
|
||||
_title = title,
|
||||
_messages = messages ?? {},
|
||||
system = system ?? Preferences.instance.system {
|
||||
addListener(ChatManager.instance.notifyListeners);
|
||||
for (var m in _messages) {
|
||||
m.addListener(notifyListeners);
|
||||
}
|
||||
}
|
||||
|
||||
List<ollama.Message> toApi() {
|
||||
var messages = <ollama.Message>[];
|
||||
var images = <ImageMessage>[];
|
||||
|
||||
var systemMessage = system;
|
||||
if (systemMessage != null) {
|
||||
messages.add(
|
||||
ollama.Message(role: ollama.MessageRole.system, content: systemMessage),
|
||||
);
|
||||
}
|
||||
|
||||
for (var message in _messages) {
|
||||
switch (message) {
|
||||
case TextMessage message:
|
||||
messages.add(
|
||||
ollama.Message(
|
||||
role: message.sender == MessageSender.user
|
||||
? ollama.MessageRole.user
|
||||
: ollama.MessageRole.assistant,
|
||||
content: message.content,
|
||||
images: images.map((e) => e.image.toString()).toList(),
|
||||
),
|
||||
);
|
||||
images.clear();
|
||||
case ImageMessage message:
|
||||
images.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"model": model?.name,
|
||||
"createdAt": createdAt.toUtc().millisecondsSinceEpoch,
|
||||
"title": title,
|
||||
"messages": _messages.map((e) => e.toJson()).toList(),
|
||||
"system": system,
|
||||
};
|
||||
|
||||
Future<void> generateTitle({
|
||||
required BuildContext context,
|
||||
bool? think = false,
|
||||
}) async {
|
||||
assert(alive, "Chat must be alive to be modified.");
|
||||
assert(model != null, "Chat model must be set to generate a title.");
|
||||
|
||||
if (_messages.isEmpty ||
|
||||
model == null ||
|
||||
!Preferences.instance.generateTitles) {
|
||||
_title = AppLocalizations.of(mainContext!).newChatTitle;
|
||||
return;
|
||||
}
|
||||
|
||||
var effectiveThink =
|
||||
(think ?? false) &&
|
||||
model!.capabilities.contains(ModelCapability.thinking);
|
||||
|
||||
var content = jsonEncode(
|
||||
(toJson()["messages"] as List<Map<String, dynamic>>)
|
||||
.map(
|
||||
(e) => e
|
||||
..removeWhere(
|
||||
(k, _) => !["type", "role", "content", "name"].contains(k),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
var request = ollama.GenerateChatCompletionRequest(
|
||||
model: modelName!,
|
||||
messages: [
|
||||
const ollama.Message(
|
||||
role: ollama.MessageRole.system,
|
||||
content:
|
||||
"Generate a two to five 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. Also make an assumption about things happening in the conversation following the messages provided. "
|
||||
"You must not put the assistant in the focus and you must not put the word 'assistant' in the title! "
|
||||
"Use a factual, formal tone; don't make the title dramatic using dramatic words. Preferably use nouns and adjectives, not verbs. Also avoid using words like 'simple' or 'easy' to not belittle the user or their problem. "
|
||||
"Do preferably use title 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"
|
||||
"Example bad titles compared to good titles:\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",
|
||||
),
|
||||
ollama.Message(
|
||||
role: ollama.MessageRole.user,
|
||||
content: "```json\n$content\n```",
|
||||
),
|
||||
],
|
||||
keepAlive: Preferences.instance.keepAlive,
|
||||
think: effectiveThink,
|
||||
);
|
||||
|
||||
ollama.GenerateChatCompletionResponse generated;
|
||||
try {
|
||||
generated = await clients.ollamaClient
|
||||
.generateChatCompletion(request: request)
|
||||
.timeout(TimeoutMultiplier.long);
|
||||
} catch (e, s) {
|
||||
if (alive) Error.throwWithStackTrace(e, s);
|
||||
return;
|
||||
}
|
||||
var newTitle = generated.message.content;
|
||||
newTitle = newTitle.replaceAll("\n", " ");
|
||||
|
||||
for (var term in [
|
||||
'"',
|
||||
"'",
|
||||
"*",
|
||||
"_",
|
||||
".",
|
||||
",",
|
||||
"!",
|
||||
"?",
|
||||
":",
|
||||
";",
|
||||
"(",
|
||||
")",
|
||||
"[",
|
||||
"]",
|
||||
"{",
|
||||
"}",
|
||||
"<",
|
||||
">",
|
||||
]) {
|
||||
newTitle = newTitle.replaceAll(term, "");
|
||||
}
|
||||
|
||||
while (newTitle.contains(" " * 2)) {
|
||||
newTitle = newTitle.replaceAll(" " * 2, " " * 1);
|
||||
}
|
||||
|
||||
title = newTitle.trim();
|
||||
}
|
||||
|
||||
Future<void> send(
|
||||
Message message, {
|
||||
bool awaitCompletion = true,
|
||||
bool? think,
|
||||
void Function(String)? onContent,
|
||||
}) async {
|
||||
assert(alive, "Chat must be alive to be modified.");
|
||||
|
||||
assert(model != null, "Chat model must be set to send messages.");
|
||||
if (model == null) return;
|
||||
|
||||
_messages.add(message..addListener(notifyListeners));
|
||||
|
||||
var finalThink =
|
||||
(think ?? Preferences.instance.thinking) &&
|
||||
model!.capabilities.contains(ModelCapability.thinking);
|
||||
|
||||
if (message is TextMessage && message.sender == MessageSender.user) {
|
||||
completer = Completer<void>();
|
||||
var message = TextMessage.fromStream(
|
||||
clients.ollamaClient.generateChatCompletionStream(
|
||||
request: ollama.GenerateChatCompletionRequest(
|
||||
model: model!.name,
|
||||
messages: toApi(),
|
||||
stream: true,
|
||||
keepAlive: Preferences.instance.keepAlive,
|
||||
think: finalThink,
|
||||
),
|
||||
),
|
||||
sender: MessageSender.assistant,
|
||||
completer: completer,
|
||||
onContent: onContent,
|
||||
)..addListener(notifyListeners);
|
||||
_messages.add(message);
|
||||
|
||||
var future = completer!.future
|
||||
.then((_) => ChatManager.instance.saveChats())
|
||||
.catchError((e, s) {
|
||||
message
|
||||
.._includesError = true
|
||||
..modify();
|
||||
Error.throwWithStackTrace(e, s);
|
||||
});
|
||||
if (awaitCompletion) await future;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
await ChatManager.instance.saveChats();
|
||||
}
|
||||
|
||||
void deleteMessage(Message message) {
|
||||
assert(alive, "Chat must be alive to be modified.");
|
||||
|
||||
_messages.remove(message);
|
||||
message.removeListener(notifyListeners);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class ChatManager extends ChangeNotifier {
|
||||
static final ChatManager _instance = ChatManager._();
|
||||
static ChatManager get instance => _instance;
|
||||
|
||||
String? _currentChatId;
|
||||
String? get currentChatId => _currentChatId;
|
||||
set currentChatId(String? id) {
|
||||
_currentChatId = id;
|
||||
if (id != null) {
|
||||
ModelManager.instance.currentModelName = _chats
|
||||
.singleWhere((e) => e.id == id)
|
||||
.model!
|
||||
.name;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Chat? get currentChat => _currentChatId == null
|
||||
? null
|
||||
: _chats.singleWhere((e) => e.id == _currentChatId);
|
||||
set currentChat(Chat? chat) => currentChatId = chat?.id;
|
||||
|
||||
final Set<Chat> _chats = {};
|
||||
Set<Chat> get chats => Set.unmodifiable(_chats);
|
||||
|
||||
ChatManager._();
|
||||
|
||||
Future<void> loadChats() async {
|
||||
DateTime getDateTimeFromMilliseconds(int? milliseconds) {
|
||||
if (milliseconds == null) return DateTime.now();
|
||||
return DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: true);
|
||||
}
|
||||
|
||||
var stored = prefs!.getStringList("chats") ?? [];
|
||||
_chats.clear();
|
||||
for (var chatJson in stored) {
|
||||
try {
|
||||
var chatData = jsonDecode(chatJson);
|
||||
|
||||
var messages = <Message>{};
|
||||
String? system;
|
||||
for (var message in chatData["messages"]) {
|
||||
if (message["role"] == "system") {
|
||||
system = message["content"];
|
||||
}
|
||||
|
||||
switch (message["type"]) {
|
||||
case "text":
|
||||
messages.add(
|
||||
TextMessage(
|
||||
message["content"],
|
||||
sender: MessageSender.values.byName(message["role"]),
|
||||
createdAt: getDateTimeFromMilliseconds(message["createdAt"]),
|
||||
),
|
||||
);
|
||||
case "image":
|
||||
messages.add(
|
||||
ImageMessage(
|
||||
image: Uri.parse(message["image"]),
|
||||
name: message["name"],
|
||||
sender: MessageSender.values.byName(message["role"]),
|
||||
createdAt: getDateTimeFromMilliseconds(message["createdAt"]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_chats.add(
|
||||
Chat._(
|
||||
modelName: chatData["model"],
|
||||
createdAt: getDateTimeFromMilliseconds(chatData["createdAt"]),
|
||||
title: chatData["title"],
|
||||
messages: messages,
|
||||
system: system,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> saveChats() async {
|
||||
prefs!.setStringList(
|
||||
"chats",
|
||||
_chats.map((c) => jsonEncode(c.toJson())).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Chat createChat({
|
||||
required BuildContext? context,
|
||||
required Model? model,
|
||||
String? title,
|
||||
String? system,
|
||||
}) {
|
||||
assert(
|
||||
allowMultipleChats || chats.isEmpty,
|
||||
"Cannot create a new chat when multiple chats are not allowed and there is already a chat.",
|
||||
);
|
||||
|
||||
var chat = Chat._(
|
||||
modelName: model?.name,
|
||||
createdAt: DateTime.now(),
|
||||
title:
|
||||
title ??
|
||||
((context != null)
|
||||
? AppLocalizations.of(context).newChatTitle
|
||||
: "Unnamed Chat"),
|
||||
system: system ?? Preferences.instance.system,
|
||||
);
|
||||
_chats.add(chat);
|
||||
_currentChatId = chat.id;
|
||||
|
||||
notifyListeners();
|
||||
saveChats();
|
||||
|
||||
return chat;
|
||||
}
|
||||
|
||||
void deleteChat(Chat chat) {
|
||||
_chats.remove(chat);
|
||||
if (chat.completer?.isCompleted == false) chat.completer!.complete();
|
||||
chat.removeListener(notifyListeners);
|
||||
|
||||
if (_currentChatId == chat.id) _currentChatId = null;
|
||||
|
||||
notifyListeners();
|
||||
saveChats();
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Delete Chat Dialog
|
||||
|
||||
Future<bool> showDeleteChatDialog(
|
||||
BuildContext context, {
|
||||
Chat? chat,
|
||||
FutureOr<void> Function()? onDelete,
|
||||
}) {
|
||||
chat ??= ChatManager.instance.currentChat;
|
||||
var completer = Completer<bool>();
|
||||
if (Preferences.instance.askBeforeDeletion) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DeleteChatDialog(
|
||||
chat: chat!,
|
||||
onDelete: onDelete,
|
||||
completer: completer,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ChatManager.instance.deleteChat(chat!);
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
class DeleteChatDialog extends StatelessWidget {
|
||||
final Chat chat;
|
||||
final FutureOr<void> Function()? onDelete;
|
||||
final Completer<bool> completer;
|
||||
|
||||
const DeleteChatDialog({
|
||||
super.key,
|
||||
required this.chat,
|
||||
required this.onDelete,
|
||||
required this.completer,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (_, _) {
|
||||
if (!completer.isCompleted) completer.complete(false);
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).deleteDialogTitle),
|
||||
content: Text(AppLocalizations.of(context).deleteDialogDescription),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(AppLocalizations.of(context).deleteDialogCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ChatManager.instance.deleteChat(chat);
|
||||
completer.complete(true);
|
||||
onDelete?.call();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).deleteDialogDelete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Chat Text Widget
|
||||
|
||||
class ChatText extends StatefulWidget {
|
||||
final String content;
|
||||
final Duration? flyInDuration;
|
||||
final Widget? placeholder;
|
||||
|
||||
const ChatText(
|
||||
this.content, {
|
||||
super.key,
|
||||
this.flyInDuration,
|
||||
this.placeholder,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatText> createState() => _ChatTextState();
|
||||
}
|
||||
|
||||
class _ChatTextState extends State<ChatText> with TickerProviderStateMixin {
|
||||
late List<String> _words;
|
||||
late final List<AnimationController?> _controllers;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_words = _splitWords(widget.content);
|
||||
_controllers = List.generate(_words.length, (_) => null);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var c in _controllers) {
|
||||
c?.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<String> _splitWords(String s) {
|
||||
if (s.trim().isEmpty) return <String>[];
|
||||
return s.split(RegExp(r"(?=\s+)"));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var newWords = _splitWords(
|
||||
widget.content.replaceFirst(
|
||||
RegExp("^${RegExp.escape(_words.join())}"),
|
||||
"",
|
||||
),
|
||||
);
|
||||
|
||||
for (var word in newWords.asMap().entries) {
|
||||
_words.add(word.value);
|
||||
_controllers.add(
|
||||
AnimationController(
|
||||
value: 0,
|
||||
vsync: this,
|
||||
duration: widget.flyInDuration ?? Durations.medium1,
|
||||
)
|
||||
..addListener(() {
|
||||
var index = word.key + _words.length - newWords.length;
|
||||
if (_controllers[index]?.value == 1) {
|
||||
_controllers[index]!.dispose();
|
||||
_controllers[index] = null;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
})
|
||||
..forward(),
|
||||
);
|
||||
}
|
||||
|
||||
var finalColor =
|
||||
Theme.of(context).textTheme.bodyMedium?.color ?? Colors.black;
|
||||
return (_words.isEmpty && widget.placeholder != null)
|
||||
? widget.placeholder!
|
||||
: AnimatedSize(
|
||||
alignment: Alignment.topLeft,
|
||||
duration: Durations.short2,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: _words
|
||||
.asMap()
|
||||
.entries
|
||||
.map(
|
||||
(e) => TextSpan(
|
||||
text: e.value,
|
||||
style: TextStyle(
|
||||
color: finalColor.withValues(
|
||||
alpha: _controllers[e.key]?.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,726 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:ollama_dart/ollama_dart.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
|
||||
final _logIdRegex = RegExp(r"^[A-Z0-9]{2,16}$");
|
||||
|
||||
typedef ErrorGuardErrorMessageGenerator = String Function(Object exception);
|
||||
typedef ErrorGuardDetailsMessageGenerator =
|
||||
String? Function(Object exception, StackTrace stackTrace);
|
||||
typedef ErrorGuardIgnoreIfGenerator = bool Function(Object exception);
|
||||
|
||||
String _defaultErrorMessage(Object exception) => switch (exception) {
|
||||
OllamaClientException _ => "Unknown client error",
|
||||
AssertionError _ =>
|
||||
exception.toString().split(": ").elementAtOrNull(4) ??
|
||||
"An assertion failed",
|
||||
TimeoutException _ => "Request timed out",
|
||||
SocketException _ || HttpException _ => "Could not connect to server",
|
||||
TlsException _ => "Could not establish secure connection",
|
||||
StateError _ => "Invalid state encountered",
|
||||
_ => "An unknown error occurred",
|
||||
};
|
||||
String? _defaultDetailsMessage(
|
||||
Object exception,
|
||||
StackTrace stackTrace,
|
||||
) => switch (exception) {
|
||||
OllamaClientException e =>
|
||||
e.body.toString().startsWith("ClientException with SocketException")
|
||||
? "A network error occurred while trying to connect to the server."
|
||||
"\n\nYou may check your network connection or server reachability and try again."
|
||||
: "The Ollama API client received a faulty response with code `${e.code}`."
|
||||
"\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."
|
||||
"\n\nAssertions are used to check for conditions that should never happen."
|
||||
"\n\n<https://en.wikipedia.org/wiki/Assertion_(software_development)>",
|
||||
TimeoutException _ =>
|
||||
"Time ran out while waiting for a response from the server."
|
||||
"\n\nThis might be caused by a slow or unresponsive server, or a network issue.\nYou may try increasing the Timeout Multiplier in the settings.",
|
||||
SocketException _ || HttpException _ =>
|
||||
"A ${exception.runtimeType.toString().split(RegExp(r"(?=[A-Z])")).join(" ").toLowerCase()} might be caused by a slow or unresponsive server, or a network issue."
|
||||
"\n\nYou may check your network connection and try again.",
|
||||
TlsException _ =>
|
||||
"An error occurred while trying to establish a secure connection via TLS."
|
||||
"\n\nThis might be caused by an invalid or expired certificate, though this should not happen. Please report this issue to the developers.",
|
||||
StateError _ =>
|
||||
"Now, this is not good.\n\nYou should report this issue to the developers. Try restarting the app.",
|
||||
_ => null,
|
||||
};
|
||||
bool _defaultIgnoreIf(Object exception) => false;
|
||||
|
||||
ErrorGuardErrorMessageGenerator errorGuardErrorMessageWithFallback(
|
||||
String? Function(Object exception) errorMessage,
|
||||
) => (Object exception) {
|
||||
try {
|
||||
return errorMessage.call(exception)!;
|
||||
} catch (e) {
|
||||
return _defaultErrorMessage(exception);
|
||||
}
|
||||
};
|
||||
ErrorGuardDetailsMessageGenerator errorGuardDetailsMessageWithFallback(
|
||||
ErrorGuardDetailsMessageGenerator detailsMessage,
|
||||
) => (Object exception, StackTrace stackTrace) {
|
||||
try {
|
||||
// throws error if null, so catch block is called
|
||||
return detailsMessage.call(exception, stackTrace)!;
|
||||
} catch (_) {
|
||||
return _defaultDetailsMessage(exception, stackTrace);
|
||||
}
|
||||
};
|
||||
|
||||
ErrorGuardErrorMessageGenerator errorGuardErrorMessageWithFallbackSingle(
|
||||
Type exceptionType,
|
||||
String message,
|
||||
) => errorGuardErrorMessageWithFallback(
|
||||
(exception) => (exception.runtimeType == exceptionType) ? message : null,
|
||||
);
|
||||
ErrorGuardDetailsMessageGenerator errorGuardDetailsMessageWithFallbackSingle(
|
||||
Type exception,
|
||||
String message,
|
||||
) => errorGuardDetailsMessageWithFallback(
|
||||
(e, _) => (e.runtimeType == exception) ? message : null,
|
||||
);
|
||||
|
||||
/// Runs the given [action] and catches any exceptions that occur.
|
||||
///
|
||||
/// If the execution of [action] is successful, the result of the function is
|
||||
/// returned.
|
||||
///
|
||||
/// If an exception occurs, a SnackBar is shown with the error message. The user
|
||||
/// can then view more details about the error and where is occurred and is able
|
||||
/// to report it.
|
||||
///
|
||||
/// If this function returns `null`, all code flow following this call should
|
||||
/// be skipped.
|
||||
///
|
||||
/// ***Important:*** You must catch all async functions carefully! Do this using
|
||||
/// [Future.catchError] with [Error.throwWithStackTrace] as the callback.
|
||||
///
|
||||
/// [logId] is an optional identifier for this [errorGuard] call. It is
|
||||
/// displayed in the UI dialog and submitted with the report, if done. This
|
||||
/// should not be a word, but rather a random string. This should be constant
|
||||
/// across app restarts or re-compiles. It must be a string of 2 to 16
|
||||
/// 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`
|
||||
///
|
||||
/// 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.
|
||||
/// It should not contain any technical details or stack traces, but rather
|
||||
/// a simple description of what task went wrong. The [errorMessage.exception]
|
||||
/// parameter should only be used to determine the type of error and should not
|
||||
/// be used to generate the message directly. The message should not be longer
|
||||
/// than about 60 characters, while it is recommended to keep it under 35
|
||||
/// characters.
|
||||
/// An example would be "Unable to connect to the server" or "Server didn't
|
||||
/// return a valid response".
|
||||
///
|
||||
/// The [detailsMessage] function is used to generate a more detailed message.
|
||||
/// This should contain more information about the error, such as the
|
||||
/// circumstances and maybe a prediction of the cause. This message should not
|
||||
/// include any exception or stack trace information, because those are printed
|
||||
/// separately, but rather a more detailed description of the error. The message
|
||||
/// can be longer, it is not limited to a specific length, but should still be
|
||||
/// direct and to the point. This message may contain a link to information
|
||||
/// about the error, such as a documentation page or a Wiki article separated
|
||||
/// by a new paragraph using the `<https://example.com>` syntax.
|
||||
/// An example would be "The server might be down for maintenance" or "This
|
||||
/// might be because of an invalid server configuration".
|
||||
///
|
||||
/// Both [errorMessage] and [detailsMessage] support a rudimentary Markdown-like
|
||||
/// formatting for inline code (`` `code` ``), italic (`*italic*`) and links
|
||||
/// (`<https://example.com>`).
|
||||
///
|
||||
/// [enableReporting] can be used to disable the reporting feature. This can be
|
||||
/// 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.
|
||||
Future<T?> errorGuard<T>(
|
||||
BuildContext context,
|
||||
String? logId,
|
||||
FutureOr<T> Function() action, {
|
||||
ErrorGuardErrorMessageGenerator errorMessage = _defaultErrorMessage,
|
||||
ErrorGuardDetailsMessageGenerator detailsMessage = _defaultDetailsMessage,
|
||||
ErrorGuardIgnoreIfGenerator ignoreIf = _defaultIgnoreIf,
|
||||
bool instantDialog = false,
|
||||
bool enableDetails = true,
|
||||
bool enableReporting = true,
|
||||
bool forceReporting = false,
|
||||
}) async {
|
||||
assert(
|
||||
logId == null || _logIdRegex.hasMatch(logId),
|
||||
"`logId` must be a string of 2 to 16 alphanumeric characters",
|
||||
);
|
||||
|
||||
assert(
|
||||
enableDetails || !instantDialog,
|
||||
"`enableDetails` must be `true` if `instantDialog` is `true`",
|
||||
);
|
||||
if (instantDialog) enableDetails = true;
|
||||
|
||||
assert(
|
||||
enableReporting || !forceReporting,
|
||||
"`enableReporting` must be `true` if `forceReporting` is true",
|
||||
);
|
||||
if (forceReporting) {
|
||||
enableReporting = true;
|
||||
instantDialog = true;
|
||||
}
|
||||
|
||||
try {
|
||||
return await Future.value(
|
||||
action.call(),
|
||||
).catchError(Error.throwWithStackTrace);
|
||||
} catch (exception, stackTrace) {
|
||||
if (context.mounted && !ignoreIf.call(exception)) {
|
||||
var dateTime = DateTime.now();
|
||||
var colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
logId = logId?.toUpperCase();
|
||||
if (logId != null && !_logIdRegex.hasMatch(logId)) {
|
||||
logId = null;
|
||||
}
|
||||
|
||||
String? exceptionText;
|
||||
try {
|
||||
exceptionText = exception.toString().trim();
|
||||
if (exceptionText.isEmpty) {
|
||||
exceptionText = null;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
String errorMessageText;
|
||||
try {
|
||||
errorMessageText = errorMessage.call(exception);
|
||||
errorMessageText = errorMessageText.trim();
|
||||
assert(errorMessageText.isNotEmpty, "Error message must not be empty");
|
||||
if (!errorMessageText.endsWith(".")) errorMessageText += ".";
|
||||
} catch (_) {
|
||||
errorMessageText = _defaultErrorMessage(exception).trim();
|
||||
}
|
||||
|
||||
String? detailsMessageText;
|
||||
try {
|
||||
detailsMessageText = detailsMessage.call(exception, stackTrace);
|
||||
detailsMessageText = detailsMessageText?.trim();
|
||||
if (detailsMessageText != null) {
|
||||
assert(
|
||||
detailsMessageText.isNotEmpty,
|
||||
"Details message must not be empty",
|
||||
);
|
||||
if (detailsMessageText.isEmpty) {
|
||||
detailsMessageText = null;
|
||||
} else if (!detailsMessageText.endsWith(".") &&
|
||||
!detailsMessageText.endsWith(">")) {
|
||||
detailsMessageText += ".";
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
detailsMessageText = _defaultDetailsMessage(exception, stackTrace);
|
||||
}
|
||||
|
||||
void showErrorDialog() {
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => _ErrorGuardDetailsDialog(
|
||||
logId: logId,
|
||||
dateTime: dateTime,
|
||||
exception: exceptionText,
|
||||
stackTrace: stackTrace,
|
||||
errorMessage: errorMessageText,
|
||||
detailsMessage: detailsMessageText,
|
||||
enableReporting: enableReporting,
|
||||
forceReporting: forceReporting,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (instantDialog) {
|
||||
showErrorDialog();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
_removeNewlines(errorMessageText),
|
||||
style: TextStyle(color: colorScheme.onErrorContainer),
|
||||
),
|
||||
backgroundColor: colorScheme.errorContainer,
|
||||
action: !enableDetails
|
||||
? null
|
||||
: SnackBarAction(
|
||||
label: AppLocalizations.of(context).errorGuardDetails,
|
||||
textColor: colorScheme.onErrorContainer,
|
||||
onPressed: showErrorDialog,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _removeNewlines(String content) =>
|
||||
content.replaceAll(RegExp(r"\s*\n\s*"), " ");
|
||||
|
||||
class _ErrorGuardDetailsDialog extends StatefulWidget {
|
||||
final String? logId;
|
||||
final DateTime dateTime;
|
||||
final String? exception;
|
||||
final StackTrace stackTrace;
|
||||
final String errorMessage;
|
||||
final String? detailsMessage;
|
||||
final bool enableReporting;
|
||||
final bool forceReporting;
|
||||
|
||||
const _ErrorGuardDetailsDialog({
|
||||
required this.logId,
|
||||
required this.dateTime,
|
||||
required this.exception,
|
||||
required this.stackTrace,
|
||||
required this.errorMessage,
|
||||
required this.detailsMessage,
|
||||
required this.enableReporting,
|
||||
required this.forceReporting,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ErrorGuardDetailsDialog> createState() =>
|
||||
_ErrorGuardDetailsDialogState();
|
||||
}
|
||||
|
||||
class _ErrorGuardDetailsDialogState extends State<_ErrorGuardDetailsDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: !widget.forceReporting,
|
||||
child: AlertDialog(
|
||||
title: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, 2),
|
||||
child: Text(
|
||||
widget.dateTime
|
||||
.toIso8601String()
|
||||
.split(".")
|
||||
.first
|
||||
.replaceFirst("T", "\n"),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(AppLocalizations.of(context).errorGuardTitle),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.logId != null)
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -20),
|
||||
child: Text("@${widget.logId}"),
|
||||
),
|
||||
|
||||
Text.rich(_contentFormat(context, widget.errorMessage)),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.detailsMessage != null) ...[
|
||||
_ErrorGuardDetailsPanel(
|
||||
icon: const Icon(Icons.announcement_outlined),
|
||||
title: AppLocalizations.of(context).errorGuardDetails,
|
||||
content: widget.detailsMessage!,
|
||||
isExpanded: true,
|
||||
monospaced: false,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
_ErrorGuardDetailsPanel(
|
||||
icon: const Icon(Icons.cancel_outlined),
|
||||
title: AppLocalizations.of(context).errorGuardException,
|
||||
content:
|
||||
widget.exception ?? "Could not retrieve exception message",
|
||||
isExpanded: kDebugMode,
|
||||
monospaced: widget.exception != null,
|
||||
italic: widget.exception == null,
|
||||
),
|
||||
if (kDebugMode) ...[
|
||||
const SizedBox(height: 8),
|
||||
_ErrorGuardDetailsPanel(
|
||||
icon: const Icon(Icons.format_list_numbered),
|
||||
title: AppLocalizations.of(context).errorGuardStackTrace,
|
||||
content: widget.stackTrace.toString(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (widget.enableReporting)
|
||||
TextButton.icon(
|
||||
onPressed: _report,
|
||||
onLongPress: () =>
|
||||
Clipboard.setData(ClipboardData(text: _reportText())),
|
||||
icon: const Icon(Icons.bug_report),
|
||||
label: Text(AppLocalizations.of(context).errorGuardReport),
|
||||
),
|
||||
if (!widget.forceReporting)
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||||
),
|
||||
],
|
||||
scrollable: true,
|
||||
alignment: Alignment.bottomCenter,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static TextSpan _contentFormat(BuildContext context, String content) {
|
||||
content = content.trim().split("\n").map((l) => l.trim()).join("\n");
|
||||
if (content.isEmpty) return const TextSpan(text: "");
|
||||
// content = content.replaceAll(RegExp(r"(?=)"), "\u{00AD}");
|
||||
|
||||
var paragraphs = content.split("\n\n").map((p) => p.trim()).toList();
|
||||
|
||||
InlineSpan parseInline(String text) {
|
||||
var root = <String, dynamic>{
|
||||
"type": "root",
|
||||
"children": <Map<String, dynamic>>[],
|
||||
};
|
||||
var stack = <Map<String, dynamic>>[root];
|
||||
var buf = StringBuffer();
|
||||
|
||||
void flushBufferToCurrent() {
|
||||
if (buf.isEmpty) return;
|
||||
var textNode = {"type": "text", "text": buf.toString()};
|
||||
(stack.last["children"] as List).add(textNode);
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
while (i < text.length) {
|
||||
var ch = text[i];
|
||||
if (ch == "\\" && i + 1 < text.length) {
|
||||
buf.write(text[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
var inCode = stack.last["type"] == "code";
|
||||
if (inCode) {
|
||||
if (ch == "`") {
|
||||
flushBufferToCurrent();
|
||||
stack.removeLast();
|
||||
i++;
|
||||
} else {
|
||||
buf.write(ch);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '<') {
|
||||
var endIndex = text.indexOf('>', i + 1);
|
||||
if (endIndex != -1) {
|
||||
var url = text.substring(i + 1, endIndex);
|
||||
if (url.startsWith('https://') || url.startsWith('http://')) {
|
||||
flushBufferToCurrent();
|
||||
var linkNode = {"type": "link", "url": url};
|
||||
(stack.last["children"] as List).add(linkNode);
|
||||
i = endIndex + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ch == "`") {
|
||||
flushBufferToCurrent();
|
||||
var codeNode = {"type": "code", "children": <Map<String, dynamic>>[]};
|
||||
(stack.last["children"] as List).add(codeNode);
|
||||
stack.add(codeNode);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == "*") {
|
||||
flushBufferToCurrent();
|
||||
if (stack.last["type"] == "italic") {
|
||||
stack.removeLast();
|
||||
} else {
|
||||
var n = {"type": "italic", "children": <Map<String, dynamic>>[]};
|
||||
(stack.last['children'] as List).add(n);
|
||||
stack.add(n);
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.write(ch);
|
||||
i++;
|
||||
}
|
||||
|
||||
flushBufferToCurrent();
|
||||
|
||||
InlineSpan build(Map<String, dynamic> node) {
|
||||
var type = node["type"] as String;
|
||||
if (type == "text") {
|
||||
return TextSpan(text: node["text"] as String);
|
||||
}
|
||||
|
||||
if (type == "link") {
|
||||
var url = node["url"] as String;
|
||||
return TextSpan(
|
||||
text: url,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => launchUrl(Uri.parse(url)),
|
||||
);
|
||||
}
|
||||
|
||||
var children = (node["children"] as List)
|
||||
.map<InlineSpan>((c) => build(c as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
switch (type) {
|
||||
case "root":
|
||||
return TextSpan(children: children);
|
||||
case "italic":
|
||||
return TextSpan(
|
||||
children: children,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
);
|
||||
case "code":
|
||||
var codeText = (node["children"] as List)
|
||||
.where((c) => (c as Map<String, dynamic>)["type"] == "text")
|
||||
.map((c) => (c as Map<String, dynamic>)["text"] as String)
|
||||
.join();
|
||||
var widget = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: Text(
|
||||
codeText,
|
||||
style: const TextStyle(fontFamily: "monospace"),
|
||||
),
|
||||
),
|
||||
);
|
||||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: widget,
|
||||
);
|
||||
default:
|
||||
return TextSpan(children: children);
|
||||
}
|
||||
}
|
||||
|
||||
return build(root);
|
||||
}
|
||||
|
||||
return TextSpan(
|
||||
children: List.generate(paragraphs.length * 2 - 1, (index) {
|
||||
if (index.isOdd) {
|
||||
return const TextSpan(
|
||||
text: "\n\n",
|
||||
style: TextStyle(height: 0.5, color: Colors.transparent),
|
||||
);
|
||||
}
|
||||
var text = paragraphs[index ~/ 2];
|
||||
return parseInline(text);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
String _reportText() =>
|
||||
"""
|
||||
An exception was thrown during the execution of the app.
|
||||
|
||||
<details open>
|
||||
<summary>Exception</summary>
|
||||
|
||||
${(widget.exception != null) ? "```\n${widget.exception}\n```" : "> Not available"}
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Stack Trace</summary>
|
||||
|
||||
```
|
||||
${widget.stackTrace.toString().trim()}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
The app suggested the following cause of the issue:
|
||||
|
||||
- ***Error Message:*** ${_removeNewlines(widget.errorMessage)}
|
||||
- ***Details Message:*** ${_removeNewlines((widget.detailsMessage ?? "None provided").trim())}"""
|
||||
.trim();
|
||||
|
||||
void _report() {
|
||||
var url =
|
||||
"https://github.com/JHubi1/ollama-app/issues/new?template=bug.yaml";
|
||||
|
||||
url +=
|
||||
"&description=${Uri.encodeComponent('Received error: "${widget.errorMessage.replaceFirst(RegExp(r".$"), "")}"${(widget.logId != null) ? " (@${widget.logId})" : ""}')}";
|
||||
|
||||
var contextText = _reportText();
|
||||
url += "&context=${Uri.encodeComponent(contextText)}";
|
||||
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
launchUrl(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorGuardDetailsPanel extends StatefulWidget {
|
||||
final Widget? icon;
|
||||
final String title;
|
||||
final String content;
|
||||
final bool isExpanded;
|
||||
final bool monospaced;
|
||||
final bool italic;
|
||||
|
||||
const _ErrorGuardDetailsPanel({
|
||||
this.icon,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.isExpanded = false,
|
||||
this.monospaced = true,
|
||||
this.italic = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ErrorGuardDetailsPanel> createState() =>
|
||||
_ErrorGuardDetailsPanelState();
|
||||
}
|
||||
|
||||
class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
|
||||
with TickerProviderStateMixin {
|
||||
bool _expanded = false;
|
||||
late final AnimationController _animationController;
|
||||
late final Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
value: widget.isExpanded ? 1.0 : 0.0,
|
||||
duration: kThemeAnimationDuration,
|
||||
vsync: this,
|
||||
);
|
||||
_expanded = widget.isExpanded;
|
||||
|
||||
_animation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleAnimation() {
|
||||
setState(() {
|
||||
_expanded = !_expanded;
|
||||
_expanded
|
||||
? _animationController.forward()
|
||||
: _animationController.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _monospacedContent({required Widget child}) {
|
||||
if (!widget.monospaced) return child;
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
Feedback.forLongPress(context);
|
||||
Clipboard.setData(ClipboardData(text: widget.content.trim()));
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: widget.icon,
|
||||
title: Text(widget.title),
|
||||
trailing: ExpandIcon(
|
||||
onPressed: (_) => _toggleAnimation(),
|
||||
isExpanded: _expanded,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onTap: _toggleAnimation,
|
||||
dense: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(left: 12),
|
||||
),
|
||||
SizeTransition(
|
||||
sizeFactor: _animation,
|
||||
axisAlignment: -1,
|
||||
child: _monospacedContent(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12),
|
||||
child: widget.monospaced
|
||||
? Text(
|
||||
widget.content.trim(),
|
||||
style: const TextStyle(
|
||||
fontFamily: "monospace",
|
||||
height: kTextHeightNone,
|
||||
),
|
||||
)
|
||||
: Text.rich(
|
||||
_ErrorGuardDetailsDialogState._contentFormat(
|
||||
context,
|
||||
widget.content.trim(),
|
||||
),
|
||||
textAlign: TextAlign.justify,
|
||||
style: TextStyle(
|
||||
fontStyle: widget.italic ? FontStyle.italic : null,
|
||||
color: widget.italic ? theme.disabledColor : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,24 +1,38 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import '../main.dart';
|
||||
|
||||
import 'preferences.dart';
|
||||
|
||||
void lightHaptic() {
|
||||
if (!(prefs!.getBool("enableHaptic") ?? true)) return;
|
||||
if (!Preferences.instance.enableHaptic) return;
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
|
||||
void mediumHaptic() {
|
||||
if (!(prefs!.getBool("enableHaptic") ?? true)) return;
|
||||
if (!Preferences.instance.enableHaptic) return;
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
|
||||
void heavyHaptic() {
|
||||
if (!(prefs!.getBool("enableHaptic") ?? true)) return;
|
||||
if (!Preferences.instance.enableHaptic) return;
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
|
||||
void selectionHaptic() {
|
||||
if (!(prefs!.getBool("enableHaptic") ?? true)) return;
|
||||
// same name but for better experience, change behavior
|
||||
HapticFeedback.lightImpact();
|
||||
// HapticFeedback.selectionClick();
|
||||
if (!Preferences.instance.enableHaptic) return;
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
// MARK: Chat Haptic
|
||||
|
||||
const Duration _hapticChatDelay = Duration(milliseconds: 45);
|
||||
DateTime _lastHapticChat = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
void chatHaptic() {
|
||||
if (!Preferences.instance.enableHaptic) return;
|
||||
|
||||
var now = DateTime.now();
|
||||
if (now.difference(_lastHapticChat) < _hapticChatDelay) return;
|
||||
_lastHapticChat = now;
|
||||
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.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;
|
||||
|
||||
class Model {
|
||||
final String name;
|
||||
|
||||
String _family;
|
||||
String get family => _family;
|
||||
|
||||
Set<String> _families;
|
||||
Set<String> get families => Set.unmodifiable(_families);
|
||||
|
||||
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;
|
||||
|
||||
factory Model.fromApi({required ollama.Model model, ollama.ModelInfo? info}) {
|
||||
return Model._(
|
||||
model.model!,
|
||||
family: model.details!.family!,
|
||||
families: model.details!.families?.toSet() ?? {},
|
||||
capabilities: info?.capabilities?.toSet() ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
var data = await clients.ollamaClient.showModelInfo(
|
||||
request: ollama.ModelInfoRequest(model: name),
|
||||
);
|
||||
|
||||
// just in case, but should always be present
|
||||
if (data.details != null) {
|
||||
_family = data.details!.family!;
|
||||
_families = data.details!.families?.toSet() ?? {};
|
||||
}
|
||||
_capabilities = data.capabilities!.toSet();
|
||||
}
|
||||
|
||||
Future<void> loadIntoMemory() async {
|
||||
// unable to use [ollamaClient] here, because the library does not support
|
||||
// sending a [GenerateChatCompletionRequest] without [messages], which is
|
||||
// required for loading, otherwise a message will be generated
|
||||
|
||||
var headers = <String, String>{
|
||||
"Content-Type": "application/json",
|
||||
...(jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map),
|
||||
};
|
||||
var body = {
|
||||
"model": name,
|
||||
"keep_alive": int.parse(prefs!.getString("keepAlive") ?? "300"),
|
||||
};
|
||||
|
||||
await clients.httpClient
|
||||
.post(
|
||||
Uri.parse("${clients.ollamaClient.baseUrl}/api/generate"),
|
||||
headers: headers,
|
||||
body: jsonEncode(body),
|
||||
)
|
||||
.timeout(TimeoutMultiplier.medium);
|
||||
}
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is Model && other.name == name;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => name.hashCode;
|
||||
}
|
||||
|
||||
class ModelManager extends ChangeNotifier {
|
||||
static final ModelManager _instance = ModelManager._();
|
||||
static ModelManager get instance => _instance;
|
||||
|
||||
bool _initialized = false;
|
||||
bool get initialized => _initialized;
|
||||
|
||||
String? _currentModelName;
|
||||
String? get currentModelName => _currentModelName;
|
||||
set currentModelName(String? name) {
|
||||
_currentModelName = name;
|
||||
Preferences.instance.model = name;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Model? get currentModel => _currentModelName == null
|
||||
? null
|
||||
: _models.singleWhere((e) => e.name == _currentModelName);
|
||||
set currentModel(Model? model) => currentModelName = model?.name;
|
||||
|
||||
final Set<Model> _models = {};
|
||||
Set<Model> get models => Set.unmodifiable(_models);
|
||||
|
||||
ModelManager._() : _currentModelName = Preferences.instance.model;
|
||||
|
||||
Future<void> loadModels({bool fetchCapabilitiesInBackground = true}) async {
|
||||
var data = await clients.ollamaClient.listModels();
|
||||
_models.clear();
|
||||
for (var model in data.models!) {
|
||||
_models.add(Model.fromApi(model: model));
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
notifyListeners();
|
||||
|
||||
if (fetchCapabilitiesInBackground) {
|
||||
compute((_) async {
|
||||
for (var model in _instance.models) {
|
||||
await model.updateData().catchError((_) {});
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Model Set Dialog
|
||||
|
||||
// class ModelSetDialog extends StatefulWidget {
|
||||
// final bool contentOnly;
|
||||
|
||||
// const ModelSetDialog({super.key, this.contentOnly = false});
|
||||
|
||||
// @override
|
||||
// State<ModelSetDialog> createState() => _ModelSetDialogState();
|
||||
// }
|
||||
|
||||
// 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());
|
||||
// }
|
||||
// }
|
|
@ -0,0 +1,140 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
class Preferences extends ChangeNotifier {
|
||||
static final Preferences _instance = Preferences._();
|
||||
static Preferences get instance => _instance;
|
||||
|
||||
Preferences._() {
|
||||
if (!prefsReady.isCompleted) {
|
||||
prefsReady.future.then((_) => notifyListeners());
|
||||
}
|
||||
}
|
||||
|
||||
String? get host => prefs?.getString("host") ?? (useHost ? fixedHost : null);
|
||||
set host(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
prefs?.remove("host");
|
||||
} else {
|
||||
prefs?.setString("host", value);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Map<String, String> get hostHeaders =>
|
||||
(jsonDecode(prefs?.getString("hostHeaders") ?? "{}") as Map).cast();
|
||||
set hostHeaders(Map value) {
|
||||
if (value.isEmpty) {
|
||||
prefs?.remove("hostHeaders");
|
||||
} else {
|
||||
prefs?.setString("hostHeaders", jsonEncode(value));
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
double get timeoutMultiplier => prefs?.getDouble("timeoutMultiplier") ?? 1.0;
|
||||
set timeoutMultiplier(double value) {
|
||||
prefs?.setDouble("timeoutMultiplier", value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get useSystem => prefs?.getBool("useSystem") ?? true;
|
||||
set useSystem(bool value) {
|
||||
prefs?.setBool("useSystem", value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String? get system => useSystem
|
||||
? prefs?.getString("system") ?? "You are a helpful assistant."
|
||||
: null;
|
||||
set system(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
prefs?.remove("system");
|
||||
} else {
|
||||
prefs?.setString("system", value);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get thinking => prefs?.getBool("thinking") ?? true;
|
||||
set thinking(bool value) {
|
||||
prefs?.setBool("thinking", value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get generateTitles => prefs?.getBool("generateTitles") ?? true;
|
||||
set generateTitles(bool value) {
|
||||
prefs?.setBool("generateTitles", value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get askBeforeDeletion => prefs!.getBool("askBeforeDeletion") ?? false;
|
||||
set askBeforeDeletion(bool value) {
|
||||
prefs!.setBool("askBeforeDeletion", value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int get keepAlive => int.parse(prefs!.getString("keepAlive") ?? "300");
|
||||
set keepAlive(int value) {
|
||||
prefs!.setString("keepAlive", value.toString());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String? get model => prefs?.getString("model");
|
||||
set model(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
prefs?.remove("model");
|
||||
} else {
|
||||
prefs?.setString("model", value);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get welcomeFinished => prefs?.getBool("welcomeFinished") ?? false;
|
||||
set welcomeFinished(bool value) {
|
||||
prefs?.setBool("welcomeFinished", value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get enableHaptic => prefs?.getBool("enableHaptic") ?? true;
|
||||
set enableHaptic(bool value) {
|
||||
prefs?.setBool("enableHaptic", value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ThemeMode get themeMode =>
|
||||
ThemeMode.values.byName(prefs?.getString("themeMode") ?? "system");
|
||||
set themeMode(ThemeMode value) {
|
||||
prefs?.setString("themeMode", value.name);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get themeSystem => prefs?.getBool("themeSystem") ?? false;
|
||||
set themeSystem(bool value) {
|
||||
prefs?.setBool("themeSystem", value);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutMultiplier {
|
||||
static Duration calculate(Duration base) =>
|
||||
base * Preferences.instance.timeoutMultiplier;
|
||||
|
||||
/// Short time interval. Equals 3 seconds with default multiplier.
|
||||
///
|
||||
/// This multiplier should not be used for AI tasks, as it is too short.
|
||||
/// Instead, use it for quick checks or UI updates.
|
||||
static Duration get short => calculate(const Duration(seconds: 3));
|
||||
|
||||
/// Medium time interval. Equals 10 seconds with default multiplier.
|
||||
static Duration get medium => calculate(const Duration(seconds: 10));
|
||||
|
||||
/// Long time interval. Equals 30 seconds with default multiplier.
|
||||
static Duration get long => calculate(const Duration(seconds: 30));
|
||||
|
||||
/// Very long time interval. Equals 60 seconds with default multiplier.
|
||||
static Duration get veryLong => calculate(const Duration(seconds: 60));
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import 'dart:io' as io;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Display {
|
||||
late final Size size;
|
||||
|
||||
Display.from(BuildContext context) {
|
||||
size = MediaQuery.sizeOf(context);
|
||||
}
|
||||
|
||||
bool get isMobile => size.width < 600;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class LayoutFeature {
|
||||
LayoutFeature();
|
||||
|
||||
static bool desktop({bool allowWeb = false}) {
|
||||
if (!kIsWeb) {
|
||||
return io.Platform.isWindows ||
|
||||
io.Platform.isLinux ||
|
||||
io.Platform.isMacOS;
|
||||
}
|
||||
return allowWeb && kIsWeb;
|
||||
}
|
||||
}
|
|
@ -1,78 +1,211 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import 'preferences.dart';
|
||||
import 'responsive.dart';
|
||||
|
||||
ColorScheme? colorSchemeLight;
|
||||
ColorScheme? colorSchemeDark;
|
||||
typedef ThemeBuilderBuilder =
|
||||
Widget Function(
|
||||
ThemeMode themeMode,
|
||||
ThemeData themeLight,
|
||||
ThemeData themeDark,
|
||||
);
|
||||
|
||||
ThemeData themeModifier(ThemeData theme) {
|
||||
class ThemeBuilderData {
|
||||
ThemeBuilderData? _current;
|
||||
ThemeBuilderData? get current => _current;
|
||||
|
||||
final ColorScheme? dynamicLight;
|
||||
final ColorScheme? dynamicDark;
|
||||
|
||||
ThemeBuilderData({required this.dynamicLight, required this.dynamicDark}) {
|
||||
_current = this;
|
||||
}
|
||||
|
||||
ThemeData themeModifier(BuildContext context, ThemeData theme) {
|
||||
var isMobile = Display.from(context).isMobile;
|
||||
return theme.copyWith(
|
||||
// https://docs.flutter.dev/platform-integration/android/predictive-back#set-up-your-app
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
sliderTheme: theme.sliderTheme.copyWith(year2023: false));
|
||||
}
|
||||
|
||||
ThemeData themeCurrent(BuildContext context) {
|
||||
if (themeMode() == ThemeMode.system) {
|
||||
if (MediaQuery.of(context).platformBrightness == Brightness.light) {
|
||||
return themeLight();
|
||||
} else {
|
||||
return themeDark();
|
||||
}
|
||||
} else {
|
||||
if (themeMode() == ThemeMode.light) {
|
||||
return themeLight();
|
||||
} else {
|
||||
return themeDark();
|
||||
}
|
||||
}
|
||||
sliderTheme: theme.sliderTheme.copyWith(year2023: false),
|
||||
progressIndicatorTheme: theme.progressIndicatorTheme.copyWith(
|
||||
year2023: false,
|
||||
),
|
||||
snackBarTheme: theme.snackBarTheme.copyWith(
|
||||
behavior: isMobile ? null : SnackBarBehavior.floating,
|
||||
width: isMobile ? null : 288,
|
||||
),
|
||||
listTileTheme: theme.listTileTheme.copyWith(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData themeLight() {
|
||||
if (!(prefs?.getBool("useDeviceTheme") ?? false) ||
|
||||
colorSchemeLight == null) {
|
||||
return themeModifier(ThemeData.from(
|
||||
colorScheme: const ColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primary: Colors.black,
|
||||
onPrimary: Colors.white,
|
||||
secondary: Colors.white,
|
||||
onSecondary: Colors.black,
|
||||
error: Colors.red,
|
||||
onError: Colors.white,
|
||||
surface: Colors.white,
|
||||
onSurface: Colors.black)));
|
||||
if (Preferences.instance.themeSystem && dynamicLight != null) {
|
||||
return ThemeData.from(colorScheme: dynamicLight!);
|
||||
} else {
|
||||
return themeModifier(ThemeData.from(colorScheme: colorSchemeLight!));
|
||||
return ThemeData.from(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.black,
|
||||
dynamicSchemeVariant: DynamicSchemeVariant.content,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ThemeData themeDark() {
|
||||
if (!(prefs?.getBool("useDeviceTheme") ?? false) || colorSchemeDark == null) {
|
||||
return themeModifier(ThemeData.from(
|
||||
colorScheme: const ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primary: Colors.white,
|
||||
onPrimary: Colors.black,
|
||||
secondary: Colors.black,
|
||||
onSecondary: Colors.white,
|
||||
error: Colors.red,
|
||||
onError: Colors.black,
|
||||
surface: Colors.black,
|
||||
onSurface: Colors.white)));
|
||||
if (Preferences.instance.themeSystem && dynamicDark != null) {
|
||||
return ThemeData.from(colorScheme: dynamicDark!);
|
||||
} else {
|
||||
return themeModifier(ThemeData.from(colorScheme: colorSchemeDark!));
|
||||
return ThemeData.from(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.white,
|
||||
dynamicSchemeVariant: DynamicSchemeVariant.content,
|
||||
brightness: Brightness.dark,
|
||||
).copyWith(surface: Colors.black),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThemeMode themeMode() {
|
||||
return ((prefs?.getString("brightness") ?? "system") == "system")
|
||||
? ThemeMode.system
|
||||
: ((prefs!.getString("brightness") == "dark")
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light);
|
||||
class ThemeBuilder extends StatefulWidget {
|
||||
final ThemeBuilderData data;
|
||||
final ThemeBuilderBuilder builder;
|
||||
|
||||
const ThemeBuilder({super.key, required this.data, required this.builder});
|
||||
|
||||
@override
|
||||
State<ThemeBuilder> createState() => _ThemeBuilderState();
|
||||
}
|
||||
|
||||
class _ThemeBuilderState extends State<ThemeBuilder> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Preferences.instance.addListener(onChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Preferences.instance.removeListener(onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onChange() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder.call(
|
||||
Preferences.instance.themeMode,
|
||||
widget.data.themeModifier(context, widget.data.themeLight()),
|
||||
widget.data.themeModifier(context, widget.data.themeDark()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeModeSwitch extends StatefulWidget {
|
||||
const ThemeModeSwitch({super.key});
|
||||
|
||||
@override
|
||||
State<ThemeModeSwitch> createState() => _ThemeModeSwitchState();
|
||||
}
|
||||
|
||||
class _ThemeModeSwitchState extends State<ThemeModeSwitch> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Preferences.instance.addListener(onChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Preferences.instance.removeListener(onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onChange() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SegmentedButton(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: "dark",
|
||||
icon: const Icon(Icons.dark_mode),
|
||||
label: Text(AppLocalizations.of(context).settingsBrightnessDark),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: "system",
|
||||
icon: const Icon(Icons.brightness_auto),
|
||||
label: Text(AppLocalizations.of(context).settingsBrightnessSystem),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: "light",
|
||||
icon: const Icon(Icons.light_mode),
|
||||
label: Text(AppLocalizations.of(context).settingsBrightnessLight),
|
||||
),
|
||||
],
|
||||
selected: switch (Preferences.instance.themeMode) {
|
||||
ThemeMode.light => {"light"},
|
||||
ThemeMode.dark => {"dark"},
|
||||
_ => {"system"},
|
||||
},
|
||||
onSelectionChanged: (selection) => Preferences.instance.themeMode =
|
||||
ThemeMode.values.byName(selection.first),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeSwitch extends StatefulWidget {
|
||||
const ThemeSwitch({super.key});
|
||||
|
||||
@override
|
||||
State<ThemeSwitch> createState() => _ThemeSwitchState();
|
||||
}
|
||||
|
||||
class _ThemeSwitchState extends State<ThemeSwitch> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Preferences.instance.addListener(onChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Preferences.instance.removeListener(onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onChange() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SegmentedButton(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: "system",
|
||||
icon: const Icon(Icons.app_shortcut),
|
||||
label: Text(AppLocalizations.of(context).settingsThemeDevice),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: "ollama",
|
||||
icon: const ImageIcon(AssetImage("assets/logo512.png")),
|
||||
label: Text(AppLocalizations.of(context).settingsThemeOllama),
|
||||
),
|
||||
],
|
||||
selected: Preferences.instance.themeSystem ? {"system"} : {"ollama"},
|
||||
onSelectionChanged: (selection) =>
|
||||
Preferences.instance.themeSystem = selection.first == "system",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget? _defaultListTileSetterTitle(dynamic value) {
|
||||
try {
|
||||
return Text("Value of type ${value.runtimeType.toString().trim()}");
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget? _defaultListTileSetterSubtitleBuilder(dynamic value) {
|
||||
try {
|
||||
return Text(value.toString());
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class ListTileSetter<T extends Object> extends StatefulWidget {
|
||||
final T initialValue;
|
||||
final ValueChanged<T>? onChanged;
|
||||
final FutureOr<T> Function(T oldValue) action;
|
||||
|
||||
final Widget? leading;
|
||||
final Widget? Function(T value)? titleBuilder;
|
||||
final Widget? Function(T value)? subtitleBuilder;
|
||||
final Widget? trailing;
|
||||
final bool? isThreeLine;
|
||||
final bool? dense;
|
||||
final VisualDensity? visualDensity;
|
||||
final ShapeBorder? shape;
|
||||
final ListTileStyle? style;
|
||||
final Color? selectedColor;
|
||||
final Color? iconColor;
|
||||
final Color? textColor;
|
||||
final TextStyle? titleTextStyle;
|
||||
final TextStyle? subtitleTextStyle;
|
||||
final TextStyle? leadingAndTrailingTextStyle;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final bool enabled;
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
final MouseCursor? mouseCursor;
|
||||
final bool Function(T value)? selected;
|
||||
final Color? focusColor;
|
||||
final Color? hoverColor;
|
||||
final Color? splashColor;
|
||||
final FocusNode? focusNode;
|
||||
final bool autofocus;
|
||||
final Color? tileColor;
|
||||
final Color? selectedTileColor;
|
||||
final bool? enableFeedback;
|
||||
final double? horizontalTitleGap;
|
||||
final double? minVerticalPadding;
|
||||
final double? minLeadingWidth;
|
||||
final double? minTileHeight;
|
||||
final ListTileTitleAlignment? titleAlignment;
|
||||
|
||||
const ListTileSetter({
|
||||
super.key,
|
||||
required this.initialValue,
|
||||
this.onChanged,
|
||||
required this.action,
|
||||
|
||||
this.leading,
|
||||
this.titleBuilder = _defaultListTileSetterTitle,
|
||||
this.subtitleBuilder = _defaultListTileSetterSubtitleBuilder,
|
||||
this.trailing,
|
||||
|
||||
this.isThreeLine,
|
||||
this.dense,
|
||||
this.visualDensity,
|
||||
this.shape,
|
||||
this.style,
|
||||
this.selectedColor,
|
||||
this.iconColor,
|
||||
this.textColor,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.leadingAndTrailingTextStyle,
|
||||
this.contentPadding,
|
||||
this.enabled = true,
|
||||
this.onFocusChange,
|
||||
this.mouseCursor,
|
||||
this.selected,
|
||||
this.focusColor,
|
||||
this.hoverColor,
|
||||
this.splashColor,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.tileColor,
|
||||
this.selectedTileColor,
|
||||
this.enableFeedback,
|
||||
this.horizontalTitleGap,
|
||||
this.minVerticalPadding,
|
||||
this.minLeadingWidth,
|
||||
this.minTileHeight,
|
||||
this.titleAlignment,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ListTileSetter<T>> createState() => _ListTileSetterState();
|
||||
}
|
||||
|
||||
class _ListTileSetterState<T extends Object> extends State<ListTileSetter<T>> {
|
||||
late T value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
value = widget.initialValue;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: widget.leading,
|
||||
title: widget.titleBuilder?.call(value),
|
||||
subtitle: widget.subtitleBuilder?.call(value),
|
||||
trailing: widget.trailing,
|
||||
isThreeLine: widget.isThreeLine,
|
||||
dense: widget.dense,
|
||||
visualDensity: widget.visualDensity,
|
||||
shape: widget.shape,
|
||||
style: widget.style,
|
||||
selectedColor: widget.selectedColor,
|
||||
iconColor: widget.iconColor,
|
||||
textColor: widget.textColor,
|
||||
titleTextStyle: widget.titleTextStyle,
|
||||
subtitleTextStyle: widget.subtitleTextStyle,
|
||||
leadingAndTrailingTextStyle: widget.leadingAndTrailingTextStyle,
|
||||
contentPadding: widget.contentPadding,
|
||||
enabled: widget.enabled,
|
||||
onFocusChange: widget.onFocusChange,
|
||||
mouseCursor: widget.mouseCursor,
|
||||
selected: widget.selected?.call(value) ?? false,
|
||||
focusColor: widget.focusColor,
|
||||
hoverColor: widget.hoverColor,
|
||||
splashColor: widget.splashColor,
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
tileColor: widget.tileColor,
|
||||
selectedTileColor: widget.selectedTileColor,
|
||||
enableFeedback: widget.enableFeedback,
|
||||
horizontalTitleGap: widget.horizontalTitleGap,
|
||||
minVerticalPadding: widget.minVerticalPadding,
|
||||
minLeadingWidth: widget.minLeadingWidth,
|
||||
minTileHeight: widget.minTileHeight,
|
||||
titleAlignment: widget.titleAlignment,
|
||||
onTap: () async {
|
||||
value = await widget.action(value);
|
||||
if (widget.onChanged != null) widget.onChanged!.call(value);
|
||||
|
||||
if (!mounted || !context.mounted) return;
|
||||
setState(() {});
|
||||
FocusScope.of(context).requestFocus(widget.focusNode);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ListTileSlide extends StatelessWidget {
|
||||
const ListTileSlide({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.secondaryTrackValue,
|
||||
this.onChanged,
|
||||
this.valueMin,
|
||||
this.valueMax,
|
||||
this.divisions,
|
||||
this.thumbColor,
|
||||
this.overlayColor,
|
||||
this.mouseCursor,
|
||||
this.focusNode,
|
||||
this.allowedInteraction,
|
||||
this.autofocus = false,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.tileColor,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.secondary,
|
||||
this.isThreeLine,
|
||||
this.dense = false,
|
||||
this.contentPadding,
|
||||
this.selected = false,
|
||||
this.controlAffinity = ListTileControlAffinity.platform,
|
||||
this.shape,
|
||||
this.selectedTileColor,
|
||||
this.visualDensity,
|
||||
this.enableFeedback,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final double? secondaryTrackValue;
|
||||
final ValueChanged<double>? onChanged;
|
||||
final double? valueMin;
|
||||
final double? valueMax;
|
||||
final int? divisions;
|
||||
final Color? thumbColor;
|
||||
final WidgetStateProperty<Color?>? overlayColor;
|
||||
final MouseCursor? mouseCursor;
|
||||
final FocusNode? focusNode;
|
||||
final SliderInteraction? allowedInteraction;
|
||||
final bool autofocus;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final Color? tileColor;
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final Widget? secondary;
|
||||
final bool? isThreeLine;
|
||||
final bool? dense;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final bool selected;
|
||||
final ListTileControlAffinity? controlAffinity;
|
||||
final ShapeBorder? shape;
|
||||
final Color? selectedTileColor;
|
||||
final VisualDensity? visualDensity;
|
||||
final bool? enableFeedback;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (subtitle != null) subtitle!,
|
||||
Slider(
|
||||
value: value,
|
||||
secondaryTrackValue: (value / (valueMax ?? 1.0)) < 0.98
|
||||
? secondaryTrackValue
|
||||
: 0,
|
||||
onChanged: onChanged,
|
||||
min: valueMin ?? 0.0,
|
||||
max: valueMax ?? 1.0,
|
||||
divisions: divisions,
|
||||
thumbColor: thumbColor,
|
||||
overlayColor: overlayColor,
|
||||
mouseCursor: mouseCursor,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
allowedInteraction: allowedInteraction,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
var theme = Theme.of(context);
|
||||
var switchTheme = SliderTheme.of(context);
|
||||
var effectiveActiveColor =
|
||||
thumbColor ?? switchTheme.thumbColor ?? theme.colorScheme.secondary;
|
||||
|
||||
return MergeSemantics(
|
||||
child: ListTile(
|
||||
selectedColor: effectiveActiveColor,
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: content,
|
||||
trailing: trailing,
|
||||
isThreeLine: isThreeLine,
|
||||
dense: dense,
|
||||
contentPadding: contentPadding,
|
||||
enabled: onChanged != null,
|
||||
selected: selected,
|
||||
selectedTileColor: selectedTileColor,
|
||||
autofocus: autofocus,
|
||||
shape: shape,
|
||||
tileColor: tileColor,
|
||||
visualDensity: visualDensity,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,363 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ListTileSwitchInteractive extends StatelessWidget {
|
||||
const ListTileSwitchInteractive({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
this.activeThumbColor,
|
||||
this.activeTrackColor,
|
||||
this.inactiveThumbColor,
|
||||
this.inactiveTrackColor,
|
||||
this.activeThumbImage,
|
||||
this.onActiveThumbImageError,
|
||||
this.inactiveThumbImage,
|
||||
this.onInactiveThumbImageError,
|
||||
this.thumbColor,
|
||||
this.trackColor,
|
||||
this.trackOutlineColor,
|
||||
this.thumbIcon,
|
||||
this.materialTapTargetSize,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.mouseCursor,
|
||||
this.overlayColor,
|
||||
this.splashRadius,
|
||||
this.focusNode,
|
||||
this.onFocusChange,
|
||||
this.autofocus = false,
|
||||
this.tileColor,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.isThreeLine,
|
||||
this.dense,
|
||||
this.contentPadding,
|
||||
this.secondary,
|
||||
this.selected = false,
|
||||
this.controlAffinity,
|
||||
this.shape,
|
||||
this.selectedTileColor,
|
||||
this.visualDensity,
|
||||
this.enableFeedback,
|
||||
this.hoverColor,
|
||||
});
|
||||
|
||||
final bool value;
|
||||
final ValueChanged<bool>? onChanged;
|
||||
final GestureTapCallback? onTap;
|
||||
final GestureLongPressCallback? onLongPress;
|
||||
final Color? activeThumbColor;
|
||||
final Color? activeTrackColor;
|
||||
final Color? inactiveThumbColor;
|
||||
final Color? inactiveTrackColor;
|
||||
final ImageProvider? activeThumbImage;
|
||||
final ImageErrorListener? onActiveThumbImageError;
|
||||
final ImageProvider? inactiveThumbImage;
|
||||
final ImageErrorListener? onInactiveThumbImageError;
|
||||
final WidgetStateProperty<Color?>? thumbColor;
|
||||
final WidgetStateProperty<Color?>? trackColor;
|
||||
final WidgetStateProperty<Color?>? trackOutlineColor;
|
||||
final WidgetStateProperty<Icon?>? thumbIcon;
|
||||
final MaterialTapTargetSize? materialTapTargetSize;
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
final MouseCursor? mouseCursor;
|
||||
final WidgetStateProperty<Color?>? overlayColor;
|
||||
final double? splashRadius;
|
||||
final FocusNode? focusNode;
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
final bool autofocus;
|
||||
final Color? tileColor;
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final Widget? secondary;
|
||||
final bool? isThreeLine;
|
||||
final bool? dense;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final bool selected;
|
||||
final ListTileControlAffinity? controlAffinity;
|
||||
final ShapeBorder? shape;
|
||||
final Color? selectedTileColor;
|
||||
final VisualDensity? visualDensity;
|
||||
final bool? enableFeedback;
|
||||
final Color? hoverColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var listTileTheme = ListTileTheme.of(context);
|
||||
var effectiveControlAffinity =
|
||||
controlAffinity ??
|
||||
listTileTheme.controlAffinity ??
|
||||
ListTileControlAffinity.platform;
|
||||
|
||||
var controlChildren = [
|
||||
const SizedBox(height: 32, child: VerticalDivider()),
|
||||
const SizedBox(width: 8),
|
||||
Switch(
|
||||
value: value,
|
||||
onChanged: onTap != null
|
||||
? onChanged != null
|
||||
? (value) {
|
||||
Feedback.forTap(context);
|
||||
onChanged!(value);
|
||||
}
|
||||
: null
|
||||
: null,
|
||||
activeThumbColor: activeThumbColor,
|
||||
activeThumbImage: activeThumbImage,
|
||||
inactiveThumbImage: inactiveThumbImage,
|
||||
materialTapTargetSize:
|
||||
materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
activeTrackColor: activeTrackColor,
|
||||
inactiveTrackColor: inactiveTrackColor,
|
||||
inactiveThumbColor: inactiveThumbColor,
|
||||
autofocus: autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
onActiveThumbImageError: onActiveThumbImageError,
|
||||
onInactiveThumbImageError: onInactiveThumbImageError,
|
||||
thumbColor: thumbColor,
|
||||
trackColor: trackColor,
|
||||
trackOutlineColor: trackOutlineColor,
|
||||
thumbIcon: thumbIcon,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
mouseCursor: mouseCursor,
|
||||
splashRadius: splashRadius,
|
||||
overlayColor: overlayColor,
|
||||
),
|
||||
];
|
||||
var control = ExcludeFocus(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: (effectiveControlAffinity == ListTileControlAffinity.leading)
|
||||
? controlChildren.reversed.toList()
|
||||
: controlChildren,
|
||||
),
|
||||
);
|
||||
|
||||
Widget? leading;
|
||||
Widget? trailing;
|
||||
(leading, trailing) = switch (effectiveControlAffinity) {
|
||||
ListTileControlAffinity.leading => (control, secondary),
|
||||
ListTileControlAffinity.trailing ||
|
||||
ListTileControlAffinity.platform => (secondary, control),
|
||||
};
|
||||
|
||||
var theme = Theme.of(context);
|
||||
var switchTheme = SwitchTheme.of(context);
|
||||
var states = <WidgetState>{if (selected) WidgetState.selected};
|
||||
var effectiveActiveColor =
|
||||
activeThumbColor ??
|
||||
switchTheme.thumbColor?.resolve(states) ??
|
||||
theme.colorScheme.secondary;
|
||||
|
||||
var effectiveContentPadding =
|
||||
contentPadding ??
|
||||
EdgeInsets.only(
|
||||
left: effectiveControlAffinity == ListTileControlAffinity.leading
|
||||
? 16
|
||||
: (listTileTheme.contentPadding?.horizontal ?? (16.0 * 2)) / 2,
|
||||
right: effectiveControlAffinity != ListTileControlAffinity.leading
|
||||
? 16
|
||||
: (listTileTheme.contentPadding?.horizontal ?? (24.0 * 2)) / 2,
|
||||
);
|
||||
|
||||
return MergeSemantics(
|
||||
child: ListTile(
|
||||
selectedColor: effectiveActiveColor,
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
isThreeLine: isThreeLine,
|
||||
dense: dense,
|
||||
contentPadding: effectiveContentPadding,
|
||||
enabled: onTap != null,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
selected: selected,
|
||||
selectedTileColor: selectedTileColor,
|
||||
autofocus: autofocus,
|
||||
shape: shape,
|
||||
tileColor: tileColor,
|
||||
visualDensity: visualDensity,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
enableFeedback: enableFeedback,
|
||||
hoverColor: hoverColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tis is better than [SwitchListTile] because [contentPadding] is actually
|
||||
/// set correctly
|
||||
class ListTileSwitch extends StatelessWidget {
|
||||
const ListTileSwitch({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.activeThumbColor,
|
||||
this.activeTrackColor,
|
||||
this.inactiveThumbColor,
|
||||
this.inactiveTrackColor,
|
||||
this.activeThumbImage,
|
||||
this.onActiveThumbImageError,
|
||||
this.inactiveThumbImage,
|
||||
this.onInactiveThumbImageError,
|
||||
this.thumbColor,
|
||||
this.trackColor,
|
||||
this.trackOutlineColor,
|
||||
this.thumbIcon,
|
||||
this.materialTapTargetSize,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.mouseCursor,
|
||||
this.overlayColor,
|
||||
this.splashRadius,
|
||||
this.focusNode,
|
||||
this.onFocusChange,
|
||||
this.autofocus = false,
|
||||
this.tileColor,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.isThreeLine,
|
||||
this.dense,
|
||||
this.contentPadding,
|
||||
this.secondary,
|
||||
this.selected = false,
|
||||
this.controlAffinity,
|
||||
this.shape,
|
||||
this.selectedTileColor,
|
||||
this.visualDensity,
|
||||
this.enableFeedback,
|
||||
this.hoverColor,
|
||||
});
|
||||
|
||||
final bool value;
|
||||
final ValueChanged<bool>? onChanged;
|
||||
final Color? activeThumbColor;
|
||||
final Color? activeTrackColor;
|
||||
final Color? inactiveThumbColor;
|
||||
final Color? inactiveTrackColor;
|
||||
final ImageProvider? activeThumbImage;
|
||||
final ImageErrorListener? onActiveThumbImageError;
|
||||
final ImageProvider? inactiveThumbImage;
|
||||
final ImageErrorListener? onInactiveThumbImageError;
|
||||
final WidgetStateProperty<Color?>? thumbColor;
|
||||
final WidgetStateProperty<Color?>? trackColor;
|
||||
final WidgetStateProperty<Color?>? trackOutlineColor;
|
||||
final WidgetStateProperty<Icon?>? thumbIcon;
|
||||
final MaterialTapTargetSize? materialTapTargetSize;
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
final MouseCursor? mouseCursor;
|
||||
final WidgetStateProperty<Color?>? overlayColor;
|
||||
final double? splashRadius;
|
||||
final FocusNode? focusNode;
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
final bool autofocus;
|
||||
final Color? tileColor;
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final Widget? secondary;
|
||||
final bool? isThreeLine;
|
||||
final bool? dense;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final bool selected;
|
||||
final ListTileControlAffinity? controlAffinity;
|
||||
final ShapeBorder? shape;
|
||||
final Color? selectedTileColor;
|
||||
final VisualDensity? visualDensity;
|
||||
final bool? enableFeedback;
|
||||
final Color? hoverColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var listTileTheme = ListTileTheme.of(context);
|
||||
var effectiveControlAffinity =
|
||||
controlAffinity ??
|
||||
listTileTheme.controlAffinity ??
|
||||
ListTileControlAffinity.platform;
|
||||
|
||||
var control = ExcludeFocus(
|
||||
child: IgnorePointer(
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: (_) {},
|
||||
activeThumbColor: activeThumbColor,
|
||||
activeThumbImage: activeThumbImage,
|
||||
inactiveThumbImage: inactiveThumbImage,
|
||||
materialTapTargetSize:
|
||||
materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
activeTrackColor: activeTrackColor,
|
||||
inactiveTrackColor: inactiveTrackColor,
|
||||
inactiveThumbColor: inactiveThumbColor,
|
||||
autofocus: autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
onActiveThumbImageError: onActiveThumbImageError,
|
||||
onInactiveThumbImageError: onInactiveThumbImageError,
|
||||
thumbColor: thumbColor,
|
||||
trackColor: trackColor,
|
||||
trackOutlineColor: trackOutlineColor,
|
||||
thumbIcon: thumbIcon,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
mouseCursor: mouseCursor,
|
||||
splashRadius: splashRadius,
|
||||
overlayColor: overlayColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget? leading;
|
||||
Widget? trailing;
|
||||
(leading, trailing) = switch (effectiveControlAffinity) {
|
||||
ListTileControlAffinity.leading => (control, secondary),
|
||||
ListTileControlAffinity.trailing ||
|
||||
ListTileControlAffinity.platform => (secondary, control),
|
||||
};
|
||||
|
||||
var theme = Theme.of(context);
|
||||
var switchTheme = SwitchTheme.of(context);
|
||||
var states = <WidgetState>{if (selected) WidgetState.selected};
|
||||
var effectiveActiveColor =
|
||||
activeThumbColor ??
|
||||
switchTheme.thumbColor?.resolve(states) ??
|
||||
theme.colorScheme.secondary;
|
||||
|
||||
var effectiveContentPadding =
|
||||
contentPadding ??
|
||||
EdgeInsets.only(
|
||||
left: effectiveControlAffinity == ListTileControlAffinity.leading
|
||||
? 16
|
||||
: (listTileTheme.contentPadding?.horizontal ?? (16.0 * 2)) / 2,
|
||||
right: effectiveControlAffinity != ListTileControlAffinity.leading
|
||||
? 16
|
||||
: (listTileTheme.contentPadding?.horizontal ?? (16.0 * 2)) / 2,
|
||||
);
|
||||
|
||||
return MergeSemantics(
|
||||
child: ListTile(
|
||||
selectedColor: effectiveActiveColor,
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
isThreeLine: isThreeLine,
|
||||
dense: dense,
|
||||
contentPadding: effectiveContentPadding,
|
||||
enabled: onChanged != null,
|
||||
onTap: onChanged != null ? () => onChanged!(!value) : null,
|
||||
selected: selected,
|
||||
selectedTileColor: selectedTileColor,
|
||||
autofocus: autofocus,
|
||||
shape: shape,
|
||||
tileColor: tileColor,
|
||||
visualDensity: visualDensity,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
enableFeedback: enableFeedback,
|
||||
hoverColor: hoverColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import 'package:uuid/uuid.dart';
|
|||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../main.dart';
|
||||
import 'clients.dart';
|
||||
import '../services/clients.dart';
|
||||
import 'haptic.dart';
|
||||
import 'setter.dart';
|
||||
// import 'package:scroll_to_index/scroll_to_index.dart';
|
|
@ -4,7 +4,6 @@ import 'dart:io';
|
|||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||
|
@ -12,7 +11,7 @@ import 'package:uuid/uuid.dart';
|
|||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../main.dart';
|
||||
import 'clients.dart';
|
||||
import '../services/clients.dart';
|
||||
import 'desktop.dart';
|
||||
import 'haptic.dart';
|
||||
import 'sender.dart';
|
||||
|
@ -44,9 +43,7 @@ void setModel(BuildContext context, Function setState) {
|
|||
}
|
||||
|
||||
addIndex = models.length;
|
||||
// ignore: use_build_context_synchronously
|
||||
models.add(AppLocalizations.of(context).modelDialogAddModel);
|
||||
// ignore: use_build_context_synchronously
|
||||
modelsReal.add(AppLocalizations.of(context).modelDialogAddModel);
|
||||
modal.add(false);
|
||||
|
||||
|
@ -671,74 +668,6 @@ void loadChat(String uuid, Function setState) {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
Future<bool> deleteChatDialog(BuildContext context, Function setState,
|
||||
{bool takeAction = true,
|
||||
bool? additionalCondition,
|
||||
String? uuid,
|
||||
bool popSidebar = false}) async {
|
||||
additionalCondition ??= true;
|
||||
uuid ??= chatUuid;
|
||||
|
||||
var returnValue = false;
|
||||
void delete(BuildContext context) {
|
||||
returnValue = true;
|
||||
if (takeAction) {
|
||||
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
|
||||
uuid) {
|
||||
var tmp = prefs!.getStringList("chats")!..removeAt(i);
|
||||
prefs!.setStringList("chats", tmp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (chatUuid == uuid) {
|
||||
messages = [];
|
||||
chatUuid = null;
|
||||
if (!desktopLayoutRequired(context) &&
|
||||
Navigator.of(context).canPop() &&
|
||||
popSidebar) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((prefs!.getBool("askBeforeDeletion") ?? false) && additionalCondition) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, setLocalState) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).deleteDialogTitle),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(AppLocalizations.of(context).deleteDialogDescription),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
selectionHaptic();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).deleteDialogCancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
selectionHaptic();
|
||||
Navigator.of(context).pop();
|
||||
delete(context);
|
||||
},
|
||||
child:
|
||||
Text(AppLocalizations.of(context).deleteDialogDelete))
|
||||
]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
delete(context);
|
||||
}
|
||||
setState(() {});
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
Future<String> prompt(BuildContext context,
|
||||
{String description = "",
|
||||
String value = "",
|
|
@ -10,7 +10,7 @@ import 'package:version/version.dart';
|
|||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../main.dart';
|
||||
import 'clients.dart';
|
||||
import '../services/clients.dart';
|
||||
import 'desktop.dart';
|
||||
import 'haptic.dart';
|
||||
|
187
pubspec.lock
187
pubspec.lock
|
@ -13,10 +13,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.13.0"
|
||||
bitsdojo_window:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -133,10 +133,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: datetime_loop
|
||||
sha256: "123784357129a92c85bdfc14a65d3e2f86e69860974eedb48bebc8f93b6f60cb"
|
||||
sha256: "2112dd246d786a7753ac510e2a6e9dfbe7f990090b5a75c66f67a0a23e6e3a4c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.3.1"
|
||||
diffutil_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -153,14 +153,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
dynamic_color:
|
||||
dynamic_system_colors:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
|
||||
name: dynamic_system_colors
|
||||
sha256: "43794e658fa88cbdec9f397dd1afd2eb69b6c9717e99b93b16ba37c3aa3b3a8c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
version: "1.8.0"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -173,10 +173,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -213,18 +213,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_android
|
||||
sha256: f3a3d48a36d1640b4dca22a086f26b426c246925a80eddc2953120775fbcf86a
|
||||
sha256: "3015702ab73987000e7ff2df5ddc99666d2bcd65cdb243f59da35729d3be6cff"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1+13"
|
||||
version: "0.5.1+15"
|
||||
file_selector_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_ios
|
||||
sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420"
|
||||
sha256: fe9f52123af16bba4ad65bd7e03defbbb4b172a38a8e6aaa2a869a0c56a5f5fb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3+1"
|
||||
version: "0.5.3+2"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -237,10 +237,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+2"
|
||||
version: "0.9.4+4"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -343,10 +343,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
|
||||
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6+2"
|
||||
version: "0.7.7+1"
|
||||
flutter_parsed_text:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -359,10 +359,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
|
||||
sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
version: "2.0.29"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -371,12 +371,11 @@ packages:
|
|||
flutter_tts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "607fd86d44592407f9df5c980614c7bf7141f4f8"
|
||||
resolved-ref: "607fd86d44592407f9df5c980614c7bf7141f4f8"
|
||||
url: "https://github.com/mumu-lhl/flutter_tts.git"
|
||||
source: git
|
||||
version: "4.2.2"
|
||||
name: flutter_tts
|
||||
sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.3"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -394,18 +393,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.5"
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.5.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -414,78 +413,86 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
hyphenatorx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hyphenatorx
|
||||
sha256: "9afe950d84e56fde91b9129bf3c95e86e70f19a20e24ba6458ee1d028a64816e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.11"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.2.0"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
|
||||
sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+22"
|
||||
version: "0.8.13"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.1.0"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+2"
|
||||
version: "0.8.13"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
|
||||
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
version: "0.2.2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.1"
|
||||
version: "2.11.0"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -506,26 +513,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "11.0.1"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
linkify:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -594,18 +601,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
version: "8.3.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -666,10 +673,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.6"
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -738,18 +745,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.2"
|
||||
version: "2.5.3"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
|
||||
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.8"
|
||||
version: "2.4.11"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -823,10 +830,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: speech_to_text
|
||||
sha256: "6cf8f284997490ebef1d68f8707bef57dcf083f43c0f915cc285428520bfe6be"
|
||||
sha256: c07557664974afa061f221d0d4186935bea4220728ea9446702825e8b988db04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
version: "7.3.0"
|
||||
speech_to_text_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -835,6 +842,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
speech_to_text_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: speech_to_text_windows
|
||||
sha256: "2c9846d18253c7bbe059a276297ef9f27e8a2745dead32192525beb208195072"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+beta.8"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -879,10 +894,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.6"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -927,26 +942,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
|
||||
sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.15"
|
||||
version: "6.3.17"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
version: "6.3.4"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -959,10 +974,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
version: "3.2.3"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -975,10 +990,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -999,10 +1014,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
version:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1023,10 +1038,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1039,10 +1054,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
|
||||
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.12.0"
|
||||
version: "5.14.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1052,5 +1067,5 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.0"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
|
16
pubspec.yaml
16
pubspec.yaml
|
@ -4,8 +4,8 @@ publish_to: 'none'
|
|||
version: 1.2.0+9
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.4 <4.0.0'
|
||||
flutter: 3.29.0
|
||||
sdk: '>=3.8.1 <4.0.0'
|
||||
flutter: 3.35.0
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
@ -36,18 +36,18 @@ dependencies:
|
|||
flutter_displaymode: ^0.6.0
|
||||
duration_picker: ^1.2.0
|
||||
speech_to_text: ^7.0.0
|
||||
flutter_tts:
|
||||
git:
|
||||
url: https://github.com/mumu-lhl/flutter_tts.git
|
||||
ref: 607fd86d44592407f9df5c980614c7bf7141f4f8
|
||||
flutter_tts: ^4.2.3
|
||||
permission_handler: ^11.3.1
|
||||
datetime_loop: ^1.2.0
|
||||
dynamic_color: ^1.7.0
|
||||
universal_html: ^2.2.4
|
||||
pwa_install: ^0.0.5
|
||||
|
||||
flutter_chat_types: any
|
||||
markdown: any
|
||||
hyphenatorx: ^1.5.11
|
||||
|
||||
# dynamic_color: ^1.7.0
|
||||
dynamic_system_colors: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// developer tool
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:convert';
|
|
@ -0,0 +1,39 @@
|
|||
// developer tool
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
void main() {
|
||||
var random = Random.secure();
|
||||
var logId = "";
|
||||
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const numbers = "0123456789";
|
||||
|
||||
var last1 = "";
|
||||
var last2 = "";
|
||||
|
||||
for (var i = 0; i < 8; i++) {
|
||||
var doLetter = random.nextBool();
|
||||
|
||||
if (i == 0) {
|
||||
doLetter = true; // first character is always a letter
|
||||
} else if (numbers.contains(last1) && numbers.contains(last2)) {
|
||||
doLetter = true;
|
||||
} else if (letters.contains(last1) && letters.contains(last2)) {
|
||||
doLetter = false;
|
||||
}
|
||||
|
||||
String nextChar;
|
||||
if (doLetter) {
|
||||
nextChar = letters[random.nextInt(letters.length)];
|
||||
} else {
|
||||
nextChar = numbers[random.nextInt(numbers.length)];
|
||||
}
|
||||
|
||||
logId += nextChar;
|
||||
(last2, last1) = (last1, nextChar);
|
||||
}
|
||||
|
||||
print(logId);
|
||||
}
|
|
@ -1 +1,41 @@
|
|||
{}
|
||||
{
|
||||
"de": [
|
||||
"errorGuardTitle",
|
||||
"errorGuardDetails",
|
||||
"errorGuardException",
|
||||
"errorGuardStackTrace",
|
||||
"errorGuardReport"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
"errorGuardTitle",
|
||||
"errorGuardDetails",
|
||||
"errorGuardException",
|
||||
"errorGuardStackTrace",
|
||||
"errorGuardReport"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"errorGuardTitle",
|
||||
"errorGuardDetails",
|
||||
"errorGuardException",
|
||||
"errorGuardStackTrace",
|
||||
"errorGuardReport"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"errorGuardTitle",
|
||||
"errorGuardDetails",
|
||||
"errorGuardException",
|
||||
"errorGuardStackTrace",
|
||||
"errorGuardReport"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"errorGuardTitle",
|
||||
"errorGuardDetails",
|
||||
"errorGuardException",
|
||||
"errorGuardStackTrace",
|
||||
"errorGuardReport"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||
#include <dynamic_system_colors/dynamic_color_plugin_c_api.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_tts/flutter_tts_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <speech_to_text_windows/speech_to_text_windows.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
|
@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("FlutterTtsPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
SpeechToTextWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SpeechToTextWindows"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_windows
|
||||
dynamic_color
|
||||
dynamic_system_colors
|
||||
file_selector_windows
|
||||
flutter_tts
|
||||
permission_handler_windows
|
||||
speech_to_text_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue