├── .gitignore ├── LICENSE ├── README.md ├── README_JP.md ├── README_KR.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── my_ollama │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── my_ollama_android.iml └── settings.gradle ├── assets ├── images │ ├── appstore.png │ ├── claude.png │ ├── ollama.png │ ├── openai.png │ └── pdfthumb.png └── translations │ ├── en-US.json │ ├── ja-JP.json │ └── ko-KR.json ├── changelog.md ├── devtools_options.yaml ├── image.jpg ├── image_en.jpg ├── image_jp.jpg ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Info.plist ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── 100.png │ │ │ ├── 1024.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 16.png │ │ │ ├── 167.png │ │ │ ├── 180.png │ │ │ ├── 20.png │ │ │ ├── 256.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 50.png │ │ │ ├── 512.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 64.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ └── Contents.json │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── ja.lproj │ │ └── InfoPlist.strings │ └── ko.lproj │ │ └── InfoPlist.strings └── RunnerTests │ └── RunnerTests.swift ├── ipad.png ├── lib ├── helpers │ └── event_bus.dart ├── main.dart ├── models │ ├── chat_session.dart │ └── model_manager.dart ├── provider │ └── main_provider.dart ├── services │ └── chat_service.dart ├── utils │ └── platform_utils.dart ├── views │ ├── chat_view.dart │ ├── desktop_view.dart │ ├── drawer.dart │ └── settings.dart └── widgets │ ├── app_title.dart │ ├── dialogs.dart │ ├── drop_menu.dart │ ├── list_header.dart │ ├── model_selector.dart │ ├── text_fields.dart │ └── title_list.dart ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 128.png │ │ │ ├── 16.png │ │ │ ├── 256.png │ │ │ ├── 32.png │ │ │ ├── 512.png │ │ │ ├── 64.png │ │ │ └── Contents.json │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── my_ollama.iml ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # dotenv environment variables file 15 | .env* 16 | 17 | # Avoid committing generated Javascript files: 18 | *.dart.js 19 | *.info.json # Produced by the --dump-info flag. 20 | *.js # When generated by dart2js. Don't specify *.js if your 21 | # project includes source files written in JavaScript. 22 | *.js_ 23 | *.js.deps 24 | *.js.map 25 | 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | 29 | .idea/ 30 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | # ✨ MyOllama ✨ 5 | 6 | _Ollama-based LLM mobile client_ 7 | 8 | [한국어](./README_KR.md) • 9 | [日本語](./README_JP.md) 10 | 11 |
12 | 13 | # MyOllama 14 | 15 | MyOllama is a mobile client app that allows you to connect to a computer with Ollama installed and interact with the Large Language Model (LLM). You can download and build the source code or download the MyOllama app from the [Apple App Store](https://apps.apple.com/us/app/my-ollama/id6738298481). 16 | 17 | ## Introduction 18 | 19 | Ollama is open source software that makes it easy to run Large Language Models (LLMs) on your local machine. 20 | You can use MyOllama to access Ollama and utilize various LLMs. MyOllama - Run LLMs on your own computer through the Ollama program, so you can talk to AI models without paying a fee. 21 | 22 | ![poster](./image_en.jpg) 23 | 24 | ## Key features 25 | 26 | - Remote LLM access: Connect to the Ollama host via IP address 27 | - Custom prompts: support for setting custom instructions 28 | - Supports various open source LLMs (Llama, Gemma, Qwen, Mistral, etc.) 29 | - Customizable instruction settings 30 | - Supports image recognition (only on models that support it) 31 | - Intuitive chat-like UI 32 | - Conversation history: save and manage chat sessions 33 | - Supports iPhone, iPad, Android, Mac Desktop 34 | - Supports Korean, English, and Japanese 35 | - Support Markdown format 36 | 37 | ![poster](./ipad.png) 38 | 39 | ## How to use 40 | 41 | 1. Install Ollama on your computer (macOS, Windows, Linux supported). You can see how to install Ollama on [Ollama GitHub](https://ollama.com/download). 42 | 2. Download the source and build it with Flutter, or download the MyOllama app from the [App Store](https://apps.apple.com/us/app/my-ollama/id6738298481). 43 | 3. Install the desired model in Ollama. [Download model](https://ollama.com/search) 44 | 4. Change the settings to make Ollama remotely accessible. See: [link](http://practical.kr/?p=809) 45 | 5. Launch the MyOllama app and enter the IP address of the computer where Ollama is installed. 46 | 6. Select the desired AI model and start a conversation. 47 | 48 | ## System requirements 49 | 50 | - Computer with Ollama installed 51 | - Network connection 52 | 53 | ## Advantages 54 | 55 | - This app is designed for developers and researchers who want to efficiently utilize open-source LLMs. It can be utilized for various technical experiments such as API calls, prompt engineering, model performance testing, etc. 56 | - Advanced AI features available for free 57 | - Supports a wide range of LLM models 58 | - Privacy-protected (runs on your local machine) 59 | - Versatile for programming, creative work, casual questions, etc. 60 | - Organized to keep conversations in context 61 | 62 | ## Notes 63 | 64 | - This app requires a computer with Ollama installed. 65 | - You are responsible for setting up and managing your Ollama host. Be aware of security settings. 66 | 67 | ## Download App 68 | 69 | - For those who have difficulty building, you can download the app from the link below. 70 | - [https://apps.apple.com/kr/app/my-ollama/id6738298481](https://apps.apple.com/kr/app/my-ollama/id6738298481) 71 | 72 | ## License 73 | 74 | MyOllama is licensed under the GNU license. For more information, please refer to the [LICENSE](LICENSE) file. 75 | 76 | ## Contact 77 | 78 | For questions or bug reports about MyOllama, please send an email to rtlink.park@gmail.com. 79 | 80 | -------------------------------------------------------------------------------- /README_JP.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # ✨ マイオラマ ✨ 4 | 5 | _OllamaベースのLLMモバイルクライアント_ 6 | 7 | [ENGLISH](./README.md) • 8 | [한국어](./README_KR.md) 9 | 10 |
11 | 12 | # MyOllama 13 | 14 | MyOllamaは、Ollamaがインストールされたコンピュータに接続し、大規模言語モデル(LLM)と対話できるモバイルクライアントアプリです。ソースコードをダウンロードしてビルドするか、[アップルアプリストア](https://apps.apple.com/us/app/my-ollama/id6738298481)からMyOllamaアプリをダウンロードすることができます。 15 | 16 | ## 紹介 17 | 18 | Ollamaは大規模言語モデル(Large Language Models, LLMs)をローカルコンピュータで簡単に実行できるようにするオープンソースソフトウェアです。 19 | MyOllamaを使用してOllamaに接続し、様々なLLMを活用することができます。MyOllama - Ollamaプログラムを通じて自分のコンピュータでLLMを実行するので、別途使用料なしでAIモデルと対話することができます。 20 | 21 | ![poster](./image_jp.jpg) 22 | 23 | ## 主な機能 24 | 25 | - リモートLLM接続:IPアドレスでOllamaホストに接続します。 26 | - カスタムプロンプト: ユーザー定義のInstruction設定サポート 27 | - 様々なオープンソースLLMをサポート(Llama, Gemma, Qwen, Mistralなど) 28 | - ユーザー定義のInstruction設定可能 29 | - 画像認識機能をサポート(該当機能をサポートするモデルに限る) 30 | - 直感的なチャット形式のUI 31 | - 会話記録:チャットセッションの保存と管理 32 | - Supports iPhone, iPad, Android, Mac Desktop 33 | - 韓国語、English、日本語をサポート 34 | 35 | ![poster](./ipad.png) 36 | 37 | ## 使い方 38 | 39 | 1. Ollamaをパソコンにインストールします(macOS、Windows、Linuxをサポート)。Ollamaのインストール方法は[Ollama GitHub](https://ollama.com/download)で確認することができます。 40 | 2. ソースをダウンロードしてFlutterを使ってビルドするか、[App Store](https://apps.apple.com/us/app/my-ollama/id6738298481)からMyOllamaアプリをダウンロードします。 41 | 3. Ollamaに好きなモデルをインストールします。[モデルダウンロード](https://ollama.com/search) 42 | 4. Ollamaをリモートアクセスができるように設定を変更します。参照ください: [リンク](http://practical.kr/?p=809) 43 | 5. MyOllamaアプリを実行し、OllamaがインストールされているコンピュータのIPアドレスを入力します。 44 | 6. 希望のAIモデルを選択して会話を開始します。 45 | 46 | ## システム要件 47 | 48 | - Ollamaがインストールされているコンピュータ 49 | - ネットワーク接続 50 | 51 | ## 利点 52 | 53 | - このアプリは、オープンソースのLLMを効率的に活用したい開発者や研究者向けに設計されています。API呼び出し、プロンプトエンジニアリング、モデルの性能テストなど、様々な技術的な実験に活用できます。 54 | - 無料で高度なAI機能を使用可能 55 | - 様々なLLMモデルをサポート 56 | - プライバシー保護 (ローカルコンピュータで実行) 57 | - プログラミング、クリエイティブな作業、日常的な質問など、様々な用途に活用できます。 58 | - 会話の文脈を継続できるように構成 59 | 60 | ## 注意事項 61 | 62 | - このアプリを使用するには、必ずOllamaがインストールされたコンピュータが必要です。 63 | - Ollamaホストの設定と管理はユーザーの責任で行ってください。セキュリティ設定にご注意ください。 64 | 65 | ## アプリのダウンロード 66 | 67 | - ビルドが難しい方は下記のリンクからアプリをダウンロードすることができます。 68 | - [https://apps.apple.com/kr/app/my-ollama/id6738298481](https://apps.apple.com/kr/app/my-ollama/id6738298481) 69 | 70 | ## ライセンス 71 | 72 | MyOllamaはGNUライセンスに従います。詳細は[LICENSE](LICENSE)ファイルを参照してください。 73 | 74 | ## お問い合わせ 75 | 76 | MyOllamaに関するお問い合わせやバグ報告は rtlink.park@gmail.com までメールを送ってください。 77 | 78 | DeepL.com(無料版)で翻訳しました。 79 | -------------------------------------------------------------------------------- /README_KR.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # ✨ 마이올라마 ✨ 4 | 5 | _Ollama 기반 LLM 모바일 클라이언트_ 6 | 7 | [ENGLISH](./README.md) • 8 | [日本語](./README_JP.md) 9 | 10 |
11 | 12 | # 마이올라마 13 | 14 | MyOllama는 Ollama가 설치된 컴퓨터에 접속하여 대규모 언어 모델(LLM)과 상호작용할 수 있는 모바일 클라이언트 앱입니다. 소스코드를 다운받아 빌드 하거나 [애플 앱스토어](https://apps.apple.com/us/app/my-ollama/id6738298481)에서 MyOllama 앱을 다운로드할 수 있습니다. 15 | 16 | ## 소개 17 | 18 | Ollama는 대규모 언어 모델(Large Language Models, LLMs)을 로컬 컴퓨터에서 쉽게 실행할 수 있게 해주는 오픈 소스 소프트웨어입니다. 19 | MyOllama를 사용하여 Ollama에 접속하여 다양한 LLM을 활용할 수 있습니다. MyOllama - Ollama 프로그램을 통해 자신의 컴퓨터에서 LLM을 실행하므로, 별도의 사용료 없이 AI 모델과 대화할 수 있습니다. 20 | 21 | ![poster](./image.jpg) 22 | 23 | ## 주요 기능 24 | 25 | - 원격 LLM 접속: IP 주소를 통해 Ollama 호스트에 연결 26 | - 커스텀 프롬프트: 사용자 정의 Instruction 설정 지원 27 | - 다양한 오픈 소스 LLM 지원 (Llama, Gemma, Qwen, Mistral 등) 28 | - 사용자 정의 Instruction 설정 가능 29 | - 이미지 인식 기능 지원 (해당 기능을 지원하는 모델에 한함) 30 | - 직관적인 채팅 형식의 UI 31 | - 대화 기록: 채팅 세션 저장 및 관리 32 | - iPhone, iPad, Android, Mac Desktop 지원 33 | - 한국어, English, 日本語를 지원 34 | 35 | ![poster](./ipad.png) 36 | 37 | ## 사용 방법 38 | 39 | 1. Ollama를 컴퓨터에 설치합니다 (macOS, Windows, Linux 지원). Ollama 설치 방법은 [Ollama GitHub](https://ollama.com/download)에서 확인할 수 있습니다. 40 | 2. 소스를 다운 받아서 Flutter를 이용하여 빌드하거나, [App Store](https://apps.apple.com/us/app/my-ollama/id6738298481)에서 MyOllama 앱을 다운로드합니다. 41 | 3. Ollama에 원하는 모델을 설치합니다. [모델다운로드](https://ollama.com/search) 42 | 4. Ollama를 원격 접속이 가능하도록 설정을 변경합니다. 참조: [링크](http://practical.kr/?p=809) 43 | 5. MyOllama 앱을 실행하고 Ollama가 설치된 컴퓨터의 IP 주소를 입력합니다. 44 | 6. 원하는 AI 모델을 선택하고 대화를 시작합니다. 45 | 46 | ## 시스템 요구사항 47 | 48 | - Ollama가 설치된 컴퓨터 49 | - 네트워크 연결 50 | 51 | ## 장점 52 | 53 | - 이 앱은 오픈소스 LLM을 효율적으로 활용하고자 하는 개발자 및 연구자를 위해 설계되었습니다. API 호출, 프롬프트 엔지니어링, 모델 성능 테스트 등 다양한 기술적 실험에 활용 가능합니다. 54 | - 무료로 고급 AI 기능 사용 가능 55 | - 다양한 LLM 모델 지원 56 | - 프라이버시 보호 (로컬 컴퓨터에서 실행) 57 | - 프로그래밍, 창의적 작업, 일상적인 질문 등 다양한 용도로 활용 가능 58 | - 대화의 맥락을 이어갈수 있도록 구성 59 | 60 | ## 주의사항 61 | 62 | - 이 앱을 사용하려면 반드시 Ollama가 설치된 컴퓨터가 필요합니다. 63 | - Ollama 호스트 설정 및 관리는 사용자 책임입니다. 보안 설정에 유의하세요. 64 | 65 | ## App 다운로드 66 | 67 | - 빌드가 어려운 분들은 아래 링크에서 앱을 다운 받을수 있습니다. 68 | - [https://apps.apple.com/kr/app/my-ollama/id6738298481](https://apps.apple.com/kr/app/my-ollama/id6738298481) 69 | 70 | ## 라이선스 71 | 72 | MyOllama는 GNU 라이선스를 따릅니다. 자세한 내용은 [LICENSE](LICENSE) 파일을 참조해 주세요. 73 | 74 | ## 연락처 75 | 76 | MyOllama에 대한 문의나 버그 신고는 rtlink.park@gmail.com 으로 메일을 보내주세요. 77 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "com.example.my_ollama" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.example.my_ollama" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/my_ollama/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.my_ollama 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip 6 | -------------------------------------------------------------------------------- /android/my_ollama_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version '8.7.2' apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /assets/images/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/assets/images/appstore.png -------------------------------------------------------------------------------- /assets/images/claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/assets/images/claude.png -------------------------------------------------------------------------------- /assets/images/ollama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/assets/images/ollama.png -------------------------------------------------------------------------------- /assets/images/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/assets/images/openai.png -------------------------------------------------------------------------------- /assets/images/pdfthumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/assets/images/pdfthumb.png -------------------------------------------------------------------------------- /assets/translations/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "l_settings": "Settings", 3 | "l_ollama_setting": "Olama settings", 4 | "l_server_address": "Server address", 5 | "l_instructions": "LLM instructions", 6 | "l_temperature": "Temperature", 7 | "l_download": "Download", 8 | "l_download_ollama": "Download Olama server to your computer from the link", 9 | "l_howto": "How to", 10 | "l_howto_1": "How to allow external access to the Olama server", 11 | "l_app_info": "App information", 12 | "l_open_source": "Open source", 13 | "l_open_comment": "MyOlama is an open source project. You can check the source code at the link", 14 | "l_delete": "Delete", 15 | "l_delete_question": "Do you want to delete the selected data?", 16 | "l_delete_all": "Delete all data", 17 | "l_myollama": "MyOllama", 18 | "l_version": "Version: ", 19 | "l_input_question" : "Enter a question", 20 | "l_no_conversation" : "Start a new conversation\nLLMs that support images can also attach images", 21 | "l_no_server": "No server is connected. \nPlease connect to a server in the settings and try again", 22 | "l_error_1": "An error occurred while communicating with the server, please check the server address in settings and try again", 23 | "l_confirm": "Confirm", 24 | "l_cancel": "Cancel", 25 | "l_no_model": "No model", 26 | "l_q1": "What is MyOlama?", 27 | "l_a1": "MyOlama is a client app that communicates with the Olama server. Users can ask questions to the LLM installed on the server through MyOlama", 28 | "l_q2": "How do I connect to the Olama server?", 29 | "l_a2": "Enter the address of the Olama server in the settings and save it, then you can connect to the server from the main screen", 30 | "l_q3": "How do I analyze images?", 31 | "l_a3": "The ollama server should have a pre-installed LLM that can analyze images, for example, llama3.2. \n\nThen you can select an image in the app, ask questions and get answers.", 32 | "l_success": "Connection success", 33 | "l_error": "Connection failure", 34 | "l_success_url": "You have been connected to the server", 35 | "l_error_url": "Unable to connect to the server", 36 | "l_invalid_url": "Invalid address", 37 | "l_copyed": "Copied", 38 | "l_server_check": "Server Check", 39 | "l_new_chat": "New chat", 40 | "l_share_all": "Share all", 41 | "l_search": "Search" 42 | } -------------------------------------------------------------------------------- /assets/translations/ja-JP.json: -------------------------------------------------------------------------------- 1 | { 2 | "l_settings": "設定", 3 | "l_ollama_setting": "オラマの設定", 4 | "l_server_address": "サーバーアドレス", 5 | "l_instructions": "LLMの指示", 6 | "l_temperature": "温度", 7 | "l_download": "ダウンロード", 8 | "l_download_ollama": "リンクからオラマサーバーをパソコンにダウンロードできます。", 9 | "l_howto": "使い方", 10 | "l_howto_1": "ollamaサーバーに外部アクセスを許可する方法", 11 | "l_app_info": "アプリ情報", 12 | "l_open_source": "オープンソース", 13 | "l_open_comment": "マイオラマはオープンソースプロジェクトです。リンクからソースコードを確認することができます。", 14 | "l_delete": "削除", 15 | "l_delete_question": "選択したデータを削除しますか?", 16 | "l_delete_all": "全データを削除します", 17 | "l_myollama": "マイオラマ", 18 | "l_version": "バージョン:", 19 | "l_input_question" : "質問を入力してください", 20 | "l_no_conversation" : "新しい会話を開始します\n画像をサポートするLLMは画像を添付することもできます", 21 | "l_no_server": "接続されているサーバーがありません。\n設定でサーバーに接続して再試行してください", 22 | "l_error_1": "サーバーとの通信中にエラーが発生しました。 設定でサーバーのアドレスを確認して再試行してください", 23 | "l_confirm": "確認", 24 | "l_cancel": "キャンセル", 25 | "l_no_model": "モデルがありません", 26 | "l_q1": "マイオラマとは何ですか?", 27 | "l_a1": "MyOlamaはOlamaサーバーと通信するクライアントアプリです。ユーザーはマイオラマを通じてサーバーにインストールされたLLMに質問をすることができます。", 28 | "l_q2": "Olamaサーバーに接続するにはどうすればいいですか?", 29 | "l_a2": "設定でOlamaサーバーのアドレスを入力して保存してください。 その後,メイン画面からサーバーに接続することができます。", 30 | "l_q3": "どのように画像を分析するのですか?", 31 | "l_a3": "llamaサーバーには,画像を分析できるLLMがプリインストールされている必要があります(例:llama3.2)。\n\nそして,アプリで画像を選択して質問して答えを得ることができます。", 32 | "l_success": "接続成功", 33 | "l_error": "接続失敗", 34 | "l_success_url": "サーバーに接続しました", 35 | "l_error_url": "サーバーに接続できません", 36 | "l_invalid_url": "無効なアドレスです。", 37 | "l_copyed": "コピー済み", 38 | "l_server_check": "サーバー接続状態の確認", 39 | "l_new_chat": "新しい会話", 40 | "l_share_all": "全体共有", 41 | "l_search": "検索" 42 | } -------------------------------------------------------------------------------- /assets/translations/ko-KR.json: -------------------------------------------------------------------------------- 1 | { 2 | "l_settings": "설정", 3 | "l_ollama_setting": "올라마 설정", 4 | "l_server_address": "서버 주소", 5 | "l_instructions": "LLM 지침", 6 | "l_temperture": "온도", 7 | "l_download": "다운로드", 8 | "l_download_ollama": "링크에서 올라마 서버를 컴퓨터에 다운로드 받을수 있습니다.", 9 | "l_howto": "사용법", 10 | "l_howto_1": "올라마 서버에 외부 접속을 허용하는 방법", 11 | "l_app_info": "앱 정보", 12 | "l_open_source": "오픈소스", 13 | "l_open_comment": "마이올라마는 오픈소스 프로젝트입니다. 링크에서 소스코드를 확인할 수 있습니다.", 14 | "l_delete": "삭제", 15 | "l_delete_question": "선택한 데이터를 삭제할까요?", 16 | "l_delete_all": "전체 데이터를 삭제합니다", 17 | "l_myollama": "마이올라마", 18 | "l_version": "버전 : ", 19 | "l_input_question" : "질문을 입력하세요", 20 | "l_no_conversation" : "새 대화를 시작합니다\n이미지를 지원하는 LLM은 이미지를 첨부 할수도 있습니다", 21 | "l_no_server": "연결된 서버가 없습니다. \n설정에서 서버에 연결하고 다시 시도하세요", 22 | "l_error_1": "서버와 통신하는 동안 오류가 발생했습니다. 설정에서 서버 주소를 확인한 후 다시 시도하세요.", 23 | "l_confirm": "확인", 24 | "l_cancel": "취소", 25 | "l_no_model": "모델이 없습니다", 26 | "l_q1": "마이올라마는 무엇인가요?", 27 | "l_a1": "마이올라마는 올라마 서버와 통신하는 클라이언트 앱입니다. 사용자는 마이올라마를 통해 서버에 설치된 LLM에 질문을 할 수 있습니다.", 28 | "l_q2": "올라마 서버에 연결하려면 어떻게 하나요?", 29 | "l_a2": "설정에서 올라마 서버의 주소를 입력하고 저장하세요. 그런 다음 메인 화면에서 서버에 연결할 수 있습니다.", 30 | "l_q3": "이미지를 어떻게 분석하나요?", 31 | "l_a3": "ollama 서버에는 이미지를 분석할 수 있는 LLM이 사전 설치되어 있어야 합니다(예: llama3.2.). \n\n그리고 앱에서 이미지를 선택하고 질문하고 답변을 얻을 수 있습니다.", 32 | "l_success": "연결성공", 33 | "l_error": "연결실패", 34 | "l_success_url": "서버에 연결되었습니다.", 35 | "l_error_url": "서버에 연결할 수 없습니다.", 36 | "l_invalid_url": "유효하지 않은 주소입니다.", 37 | "l_copyed": "복사되었습니다", 38 | "l_server_check": "서버연결상태확인", 39 | "l_new_chat": "새 대화", 40 | "l_share_all": "전체 공유", 41 | "l_search": "검색" 42 | } -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### 2024/11/18 - v1.0.0 4 | 5 | - v1.0.0 open 6 | 7 | ### 2024/11/28 - v1.0.5 8 | 9 | - Check server connection status 10 | - Add message copy, share, delete functions 11 | 12 | ### 2024/11/29 - v1.0.6 13 | 14 | - Support Markdown format 15 | 16 | ### 2024/11/30 - v1.0.7 17 | 18 | - Change item menu shape 19 | - Add Share All menu 20 | - Add Re question menu 21 | - Add Web link function 22 | - Release Android Version 23 | 24 | ### 2024/12/04 - v1.0.8 25 | 26 | - Change the chat view orientation 27 | - Add token usage 28 | - Add Search function 29 | 30 | ### 2024/12/16 - v1.1.0 31 | 32 | - Support iPad 33 | - Support Mac Desktop 34 | - fix some bugs 35 | 36 | ### 2025/03/03 - 1.2.0 37 | 38 | - Add Chat Session 39 | - fix some bugs -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/image.jpg -------------------------------------------------------------------------------- /image_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/image_en.jpg -------------------------------------------------------------------------------- /image_jp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/image_jp.jpg -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Crack2023 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | crack2023 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleTypeRole 29 | Editor 30 | CFBundleURLSchemes 31 | 32 | com.googleusercontent.apps.270642908804-35rke892o8frgd8j8pqbecn6f8hvqum4 33 | 34 | 35 | 36 | CFBundleVersion 37 | $(FLUTTER_BUILD_NUMBER) 38 | LSRequiresIPhoneOS 39 | 40 | NSCameraUsageDescription 41 | Use my camera for crack pictures 42 | NSLocationWhenInUseUsageDescription 43 | Use location information to store measurement locations 44 | NSLocationAlwaysAndWhenInUseUsageDescription 45 | Use location information to store measurement locations 46 | NSPhotoLibraryUsageDescription 47 | Use photo albums to save crack pictures 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | UILaunchStoryboardName 51 | LaunchScreen 52 | UIMainStoryboardFile 53 | Main 54 | UISupportedInterfaceOrientations 55 | 56 | UIInterfaceOrientationPortrait 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | UIInterfaceOrientationPortrait 63 | UIInterfaceOrientationPortraitUpsideDown 64 | 65 | UIViewControllerBasedStatusBarAppearance 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - app_settings (5.1.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - image_picker_ios (0.0.1): 6 | - Flutter 7 | - package_info_plus (0.4.5): 8 | - Flutter 9 | - path_provider_foundation (0.0.1): 10 | - Flutter 11 | - FlutterMacOS 12 | - share_plus (0.0.1): 13 | - Flutter 14 | - shared_preferences_foundation (0.0.1): 15 | - Flutter 16 | - FlutterMacOS 17 | - sqflite_darwin (0.0.4): 18 | - Flutter 19 | - FlutterMacOS 20 | - url_launcher_ios (0.0.1): 21 | - Flutter 22 | 23 | DEPENDENCIES: 24 | - app_settings (from `.symlinks/plugins/app_settings/ios`) 25 | - Flutter (from `Flutter`) 26 | - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) 27 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 28 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 29 | - share_plus (from `.symlinks/plugins/share_plus/ios`) 30 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 31 | - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) 32 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 33 | 34 | EXTERNAL SOURCES: 35 | app_settings: 36 | :path: ".symlinks/plugins/app_settings/ios" 37 | Flutter: 38 | :path: Flutter 39 | image_picker_ios: 40 | :path: ".symlinks/plugins/image_picker_ios/ios" 41 | package_info_plus: 42 | :path: ".symlinks/plugins/package_info_plus/ios" 43 | path_provider_foundation: 44 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 45 | share_plus: 46 | :path: ".symlinks/plugins/share_plus/ios" 47 | shared_preferences_foundation: 48 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 49 | sqflite_darwin: 50 | :path: ".symlinks/plugins/sqflite_darwin/darwin" 51 | url_launcher_ios: 52 | :path: ".symlinks/plugins/url_launcher_ios/ios" 53 | 54 | SPEC CHECKSUMS: 55 | app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2 56 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 57 | image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a 58 | package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 59 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 60 | share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a 61 | shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 62 | sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 63 | url_launcher_ios: 694010445543906933d732453a59da0a173ae33d 64 | 65 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 66 | 67 | COCOAPODS: 1.16.2 68 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | My Ollama 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | my_ollama 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UIApplicationSupportsIndirectInputEvents 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | NSAppTransportSecurity 49 | 50 | NSAllowsArbitraryLoads 51 | 52 | 53 | NSPhotoLibraryUsageDescription 54 | The app needs permission to use the photo album to analyze photos. 55 | 56 | 57 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | infoPlist.strings 3 | Runner 4 | 5 | Created by BillyPark on 11/16/24. 6 | 7 | */ 8 | CFBundleDisplayName = "My Ollama"; 9 | NSPhotoLibraryUsageDescription = "The app needs permission to use the photo album to analyze photos."; 10 | -------------------------------------------------------------------------------- /ios/Runner/ja.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | infoPlist.strings 3 | Runner 4 | 5 | Created by BillyPark on 11/16/24. 6 | 7 | */ 8 | CFBundleDisplayName = "マイ・オーラマ"; 9 | NSPhotoLibraryUsageDescription = "アプリが写真を分析するためにフォトアルバムの使用権限が必要です。"; 10 | -------------------------------------------------------------------------------- /ios/Runner/ko.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | infoPlist.strings 3 | Runner 4 | 5 | Created by BillyPark on 11/16/24. 6 | 7 | */ 8 | CFBundleDisplayName = "마이올라마"; 9 | NSPhotoLibraryUsageDescription = "앱이 사진을 분석하기 위해 포토앨범 사용 권한 필요합니다."; 10 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/ipad.png -------------------------------------------------------------------------------- /lib/helpers/event_bus.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:event_bus/event_bus.dart'; 3 | 4 | //----------------------------------------------------------------------------// 5 | class MyEventBus extends EventBus { 6 | static final MyEventBus _instance = MyEventBus._internal(); 7 | 8 | factory MyEventBus() { 9 | return _instance; 10 | } 11 | 12 | MyEventBus._internal() { 13 | EventBus _instance = EventBus(); 14 | } 15 | } 16 | 17 | class UserLogStatusChangeEvent { 18 | UserLogStatusChangeEvent(); 19 | } 20 | 21 | class ChangeTitleEvent { 22 | String title; 23 | List action; 24 | ChangeTitleEvent(this.title, this.action); 25 | } 26 | 27 | class RefreshMainListEvent { 28 | RefreshMainListEvent(); 29 | } 30 | 31 | class LoadHistoryGroupListEvent { 32 | LoadHistoryGroupListEvent(); 33 | } 34 | 35 | class CloseDrawerEvent { 36 | CloseDrawerEvent(); 37 | } 38 | 39 | class NewChatBeginEvent { 40 | NewChatBeginEvent(); 41 | } 42 | 43 | class ReloadModelEvent { 44 | ReloadModelEvent(); 45 | } 46 | 47 | class ChatDoneEvent { 48 | ChatDoneEvent(); 49 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:easy_localization/easy_localization.dart'; 5 | import 'package:sqflite_common_ffi/sqflite_ffi.dart'; 6 | 7 | import 'views/drawer.dart'; 8 | import 'views/desktop_view.dart'; 9 | import 'provider/main_provider.dart'; 10 | import 'utils/platform_utils.dart'; 11 | 12 | //--------------------------------------------------------------------------// 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await EasyLocalization.ensureInitialized(); 16 | 17 | if (Platform.isWindows) { 18 | sqfliteFfiInit(); 19 | databaseFactory = databaseFactoryFfi; 20 | } 21 | 22 | runApp(MultiProvider( 23 | providers: [ 24 | ChangeNotifierProvider(create: (_) => MainProvider()), 25 | ], 26 | child: EasyLocalization( 27 | supportedLocales: [ 28 | Locale('en', 'US'), 29 | Locale('ko', 'KR'), 30 | Locale('ja', 'JP') 31 | ], 32 | path: 'assets/translations', 33 | fallbackLocale: Locale('en', 'US'), 34 | child: MyOllama()), 35 | )); 36 | } 37 | 38 | //--------------------------------------------------------------------------// 39 | class MyOllama extends StatelessWidget { 40 | @override 41 | Widget build(BuildContext context) { 42 | return MaterialApp( 43 | debugShowCheckedModeBanner: false, 44 | supportedLocales: context.supportedLocales, 45 | locale: context.locale, 46 | localizationsDelegates: context.localizationDelegates, 47 | theme: ThemeData( 48 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), 49 | appBarTheme: AppBarTheme( 50 | backgroundColor: Colors.indigo, 51 | foregroundColor: Colors.white, 52 | ), 53 | ), 54 | home: InitializationWrapper(), 55 | ); 56 | } 57 | } 58 | 59 | //--------------------------------------------------------------------------// 60 | class InitializationWrapper extends StatefulWidget { 61 | @override 62 | _InitializationWrapperState createState() => _InitializationWrapperState(); 63 | } 64 | 65 | //--------------------------------------------------------------------------// 66 | class _InitializationWrapperState extends State { 67 | @override 68 | void initState() { 69 | super.initState(); 70 | Future.delayed(Duration.zero, () { 71 | setLocale(context); 72 | _initializeApp(); 73 | }); 74 | } 75 | 76 | Future setLocale(BuildContext context) async { 77 | String currentLocale = Intl.getCurrentLocale(); 78 | List parts = currentLocale.split('_'); 79 | 80 | switch (parts[0]) { 81 | case 'ko': 82 | context.setLocale(const Locale('ko', 'KR')); 83 | break; 84 | case 'ja': 85 | context.setLocale(const Locale('ja', 'JP')); 86 | break; 87 | default: 88 | context.setLocale(const Locale('en', 'US')); 89 | break; 90 | } 91 | } 92 | 93 | Future _initializeApp() async { 94 | final provider = Provider.of(context, listen: false); 95 | await provider.initialize(); 96 | } 97 | 98 | @override 99 | Widget build(BuildContext context) { 100 | return Consumer( 101 | builder: (context, provider, _) { 102 | if (!provider.isInitialized) { 103 | return Scaffold( 104 | body: Center( 105 | child: Column( 106 | mainAxisAlignment: MainAxisAlignment.center, 107 | children: [ 108 | CircularProgressIndicator(), 109 | SizedBox(height: 20), 110 | Text('Initializing...').tr(), 111 | ], 112 | ), 113 | ), 114 | ); 115 | } 116 | 117 | return isDesktopOrTablet() ? DesktopView() : MyDrawer(); 118 | }, 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/models/chat_session.dart: -------------------------------------------------------------------------------- 1 | import 'package:ollama_dart/ollama_dart.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'dart:async'; 4 | 5 | class ChatSession { 6 | final String chatId; 7 | final String modelName; 8 | final OllamaClient client; 9 | final StreamController messageController; 10 | bool isActive; 11 | bool hasError = false; 12 | String? errorMessage; 13 | 14 | ChatSession({ 15 | required this.chatId, 16 | required this.modelName, 17 | required this.client, 18 | }) : messageController = StreamController.broadcast(), 19 | isActive = true; 20 | 21 | void dispose() { 22 | if (!messageController.isClosed) { 23 | messageController.close(); 24 | } 25 | isActive = false; 26 | } 27 | 28 | void addMessage(String content, {bool isError = false}) { 29 | if (!isActive || messageController.isClosed) return; 30 | 31 | if (isError) { 32 | hasError = true; 33 | errorMessage = content; 34 | } 35 | 36 | messageController.add(ChatSessionUpdate( 37 | content: content, 38 | isError: isError, 39 | timestamp: DateTime.now(), 40 | )); 41 | } 42 | 43 | Stream get messageStream => messageController.stream; 44 | } 45 | 46 | class ChatSessionUpdate { 47 | final String content; 48 | final bool isError; 49 | final DateTime timestamp; 50 | 51 | ChatSessionUpdate({ 52 | required this.content, 53 | this.isError = false, 54 | required this.timestamp, 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /lib/models/model_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:easy_localization/easy_localization.dart'; 4 | import 'package:sqflite/sqflite.dart'; 5 | import 'package:intl/intl.dart'; 6 | import 'package:uuid/uuid.dart'; 7 | 8 | import 'package:path/path.dart'; 9 | import '../provider/main_provider.dart'; 10 | 11 | 12 | class QDatabase { 13 | static final QDatabase _instance = QDatabase._init(); 14 | static Database? _database; 15 | 16 | final sql_questions = ''' 17 | CREATE TABLE IF NOT EXISTS questions( 18 | id INTEGER PRIMARY KEY AUTOINCREMENT, 19 | groupid TEXT NOT NULL, 20 | instruction TEXT, 21 | question TEXT, 22 | answer TEXT, 23 | image TEXT, 24 | created TEXT, 25 | engine TEXT 26 | );'''; 27 | //--------------------------------------------------------------------------// 28 | QDatabase._init() { 29 | } 30 | 31 | //--------------------------------------------------------------------------// 32 | factory QDatabase() { 33 | return _instance; 34 | } 35 | 36 | //--------------------------------------------------------------------------// 37 | Future init() async { 38 | if (_database != null) return _database!; 39 | _database = await _initDB('my_ollama.db'); 40 | _makeFirstRow(); 41 | return _database!; 42 | } 43 | 44 | //--------------------------------------------------------------------------// 45 | Future _initDB(String filePath) async { 46 | 47 | final dbPath = await getDatabasesPath(); 48 | final path = dbPath + "/" + filePath; 49 | print("DB Path: $path"); 50 | 51 | final options = OpenDatabaseOptions( 52 | version: 1, 53 | onCreate: _createDB 54 | ); 55 | 56 | return await databaseFactory.openDatabase(path, options: options); 57 | } 58 | 59 | //--------------------------------------------------------------------------// 60 | Future _makeFirstRow() async { 61 | final db = await _instance.init(); 62 | 63 | final groupid = Uuid().v4(); 64 | final questions = await db.rawQuery("SELECT * FROM questions"); 65 | if (questions.length == 0) { 66 | insertQuestion(groupid, "", tr("l_q1"), tr("l_a1"), null, "ollama"); 67 | insertQuestion(groupid, "", tr("l_q2"), tr("l_a2"), null, "ollama"); 68 | insertQuestion(groupid, "", tr("l_q3"), tr("l_a3"), null, "ollama"); 69 | } 70 | } 71 | 72 | //--------------------------------------------------------------------------// 73 | Future _createDB(Database db, int version) async { 74 | try { 75 | // Master 76 | await db.execute(sql_questions); 77 | } catch (e) { 78 | print("Error Creating TABLES: $e"); 79 | } 80 | } 81 | 82 | //--------------------------------------------------------------------------// 83 | Future _onUpgrade(Database db, int oldVersion, int newVersion) async { 84 | try { 85 | } catch (e) { 86 | print("Error Upgrading TABLES: $e"); 87 | } 88 | } 89 | 90 | //--------------------------------------------------------------------------// 91 | Future insertQuestion(String groupid, String instruction, String question, String answer, String? image, String engine) async { 92 | try { 93 | String created = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()); 94 | final db = await _instance.init(); 95 | await db.rawInsert('INSERT INTO questions(groupid, instruction, question, answer, image, created, engine) VALUES(?, ?, ?, ?, ?, ?, ?)', 96 | [groupid, instruction, question, answer, image, created, engine]); 97 | } catch (e) { 98 | print("Error Inserting Master: $e"); 99 | } 100 | } 101 | 102 | //--------------------------------------------------------------------------// 103 | Future getTitles() async { 104 | final db = await _instance.init(); 105 | return await db.rawQuery("select * from questions group by groupid order by id desc"); 106 | } 107 | 108 | //--------------------------------------------------------------------------// 109 | Future getDetails(String groupid) async { 110 | final db = await _instance.init(); 111 | List details = await db.rawQuery("SELECT * FROM questions WHERE groupid = ? ORDER BY created DESC", [groupid]); 112 | return details; 113 | } 114 | 115 | //--------------------------------------------------------------------------// 116 | Future getDetailsById(int id) async { 117 | final db = await _instance.init(); 118 | List details = await db.rawQuery("SELECT * FROM questions WHERE id = ?", [id]); 119 | return details; 120 | } 121 | 122 | //--------------------------------------------------------------------------// 123 | Future searchKeywords(String keyword) async { 124 | final db = await _instance.init(); 125 | final sql = "SELECT * FROM questions m "+ 126 | "WHERE question LIKE ? OR answer LIKE ? "+ 127 | "ORDER BY created DESC"; 128 | return await db.rawQuery(sql, ["%$keyword%", "%$keyword%"]); 129 | } 130 | 131 | //--------------------------------------------------------------------------// 132 | Future deleteQuestions(String groupid) async { 133 | try { 134 | final db = await _instance.init(); 135 | await db.rawDelete('DELETE FROM questions WHERE groupid = ?', [groupid]); 136 | } catch (e) { 137 | print("Error Deleting Master: $e"); 138 | } 139 | } 140 | 141 | //--------------------------------------------------------------------------// 142 | Future deleteRecord(int id) async { 143 | try { 144 | final db = await _instance.init(); 145 | await db.rawDelete('DELETE FROM questions WHERE id = ?', [id]); 146 | } catch (e) { 147 | print("Error Deleting Master: $e"); 148 | } 149 | } 150 | 151 | //--------------------------------------------------------------------------// 152 | Future deleteAllRecords() async { 153 | try { 154 | final db = await _instance.init(); 155 | await db.rawDelete('DELETE FROM questions', []); 156 | } catch (e) { 157 | print("Error Deleting Master: $e"); 158 | } 159 | } 160 | 161 | } -------------------------------------------------------------------------------- /lib/provider/main_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ffi'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | import 'package:ollama_dart/ollama_dart.dart'; 6 | import 'package:uuid/uuid.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | import 'package:package_info_plus/package_info_plus.dart'; 9 | import 'package:http/http.dart' as http; 10 | 11 | import '../models/model_manager.dart'; 12 | import '../models/chat_session.dart'; 13 | import '../helpers/event_bus.dart'; 14 | 15 | final FEED_IMAGE_SIZE = 200.0; 16 | 17 | class MainProvider with ChangeNotifier { 18 | final _prefs = SharedPreferencesAsync(); 19 | PackageInfo? _packageInfo; 20 | QDatabase qdb = QDatabase(); 21 | 22 | bool isInitialized = false; 23 | bool serveConnected = false; 24 | String version = "1.0.0"; 25 | int buildNumber = 0; 26 | 27 | String baseUrl = "http://192.168.0.1:11434"; 28 | Map activeSessions = {}; 29 | List? modelList; 30 | String? selectedModel; 31 | 32 | String instruction = "You are a helpful assistant."; 33 | String curGroupId = Uuid().v4(); 34 | double temperature = 0.5; 35 | 36 | //--------------------------------------------------------------------------// 37 | Future initialize() async { 38 | await loadPreferences(); 39 | await checkServerConnection(); 40 | await _initPackageInfo(); 41 | await qdb.init(); 42 | 43 | isInitialized = true; 44 | notifyListeners(); 45 | } 46 | 47 | //--------------------------------------------------------------------------// 48 | Future loadPreferences() async { 49 | temperature = await _prefs.getDouble("temperature") ?? 0.5; 50 | baseUrl = await _prefs.getString("baseUrl") ?? baseUrl; 51 | instruction = await _prefs.getString("instruction") ?? instruction; 52 | notifyListeners(); 53 | } 54 | 55 | //--------------------------------------------------------------------------// 56 | Future checkServerConnection() async { 57 | if (await _isOllamaOpen()) { 58 | serveConnected = true; 59 | return await _loadModels(); 60 | } else { 61 | return false; 62 | } 63 | } 64 | 65 | //--------------------------------------------------------------------------// 66 | Future _loadModels() async { 67 | try { 68 | final client = OllamaClient(baseUrl: baseUrl + "/api"); 69 | final res = await client.listModels(); 70 | if (res.models != null) { 71 | modelList = res.models!; 72 | serveConnected = modelList!.isNotEmpty; 73 | } 74 | notifyListeners(); 75 | return true; 76 | } catch (e) { 77 | print(e); 78 | modelList = []; 79 | notifyListeners(); 80 | return false; 81 | } 82 | } 83 | 84 | //--------------------------------------------------------------------------// 85 | Future _isOllamaOpen() async { 86 | try { 87 | final response = 88 | await http.get(Uri.parse(baseUrl)).timeout(Duration(seconds: 1)); 89 | return response.statusCode == 200; 90 | } catch (e) { 91 | return false; 92 | } 93 | } 94 | 95 | //--------------------------------------------------------------------------// 96 | ChatSession createChatSession(String modelName) { 97 | Future.microtask(() { 98 | _cleanupInactiveSessions(); 99 | }); 100 | 101 | final chatId = Uuid().v4(); 102 | final client = OllamaClient(baseUrl: baseUrl + "/api"); 103 | 104 | final session = ChatSession( 105 | chatId: chatId, 106 | modelName: modelName, 107 | client: client, 108 | ); 109 | 110 | activeSessions[chatId] = session; 111 | 112 | Future.microtask(() { 113 | notifyListeners(); 114 | }); 115 | 116 | return session; 117 | } 118 | 119 | //--------------------------------------------------------------------------// 120 | void _cleanupInactiveSessions() { 121 | final inactiveSessions = activeSessions.entries 122 | .where((entry) => !entry.value.isActive) 123 | .map((entry) => entry.key) 124 | .toList(); 125 | 126 | for (final sessionId in inactiveSessions) { 127 | disposeChatSession(sessionId); 128 | } 129 | } 130 | 131 | //--------------------------------------------------------------------------// 132 | void disposeChatSession(String chatId) { 133 | final session = activeSessions[chatId]; 134 | if (session != null) { 135 | try { 136 | session.dispose(); 137 | } catch (e) { 138 | print("Error disposing session: $e"); 139 | } 140 | activeSessions.remove(chatId); 141 | 142 | Future.microtask(() { 143 | notifyListeners(); 144 | }); 145 | } 146 | } 147 | 148 | //--------------------------------------------------------------------------// 149 | void disposeAllSessions() { 150 | for (final session in activeSessions.values) { 151 | try { 152 | session.dispose(); 153 | } catch (e) { 154 | print("Error disposing session: $e"); 155 | } 156 | } 157 | activeSessions.clear(); 158 | 159 | Future.microtask(() { 160 | notifyListeners(); 161 | }); 162 | } 163 | 164 | //--------------------------------------------------------------------------// 165 | void setSelectedModel(String model) { 166 | selectedModel = model; 167 | _prefs.setString("selectedModel", model); 168 | 169 | Future.microtask(() { 170 | notifyListeners(); 171 | }); 172 | } 173 | 174 | //--------------------------------------------------------------------------// 175 | Future _initPackageInfo() async { 176 | _packageInfo = await PackageInfo.fromPlatform(); 177 | version = _packageInfo!.version; 178 | buildNumber = int.parse(_packageInfo!.buildNumber); 179 | } 180 | 181 | //--------------------------------------------------------------------------// 182 | void setTemperature(double temp) { 183 | temperature = temp; 184 | _prefs.setDouble("temperature", temp); 185 | notifyListeners(); 186 | } 187 | 188 | //--------------------------------------------------------------------------// 189 | Future setBaseUrl(String url) async { 190 | baseUrl = url; 191 | _prefs.setString("baseUrl", url); 192 | 193 | // Dispose all active sessions 194 | disposeAllSessions(); 195 | 196 | return await checkServerConnection(); 197 | } 198 | 199 | //--------------------------------------------------------------------------// 200 | void setInstruction(String instruction) { 201 | this.instruction = instruction; 202 | _prefs.setString("instruction", instruction); 203 | notifyListeners(); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/services/chat_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:uuid/uuid.dart'; 3 | import 'package:ollama_dart/ollama_dart.dart' as ollama; 4 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types; 5 | import 'package:easy_localization/easy_localization.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'dart:async'; 8 | 9 | import '../provider/main_provider.dart'; 10 | import '../models/chat_session.dart'; 11 | import '../helpers/event_bus.dart'; 12 | 13 | class ChatService { 14 | final BuildContext context; 15 | final List questions; 16 | final List messages; 17 | final types.User answerer; 18 | final types.User user; 19 | final Function setState; 20 | String? selectedImage; 21 | ChatSession? _chatSession; 22 | StreamSubscription? _sessionSubscription; 23 | 24 | Function(String)? onMessageUpdate; 25 | 26 | ChatService({ 27 | required this.context, 28 | required this.questions, 29 | required this.messages, 30 | required this.answerer, 31 | required this.user, 32 | required this.setState, 33 | this.selectedImage, 34 | this.onMessageUpdate, 35 | }); 36 | 37 | void dispose() { 38 | _cleanupCurrentSession(); 39 | } 40 | 41 | void _cleanupCurrentSession() { 42 | _sessionSubscription?.cancel(); 43 | _sessionSubscription = null; 44 | 45 | if (_chatSession != null) { 46 | final provider = context.read(); 47 | provider.disposeChatSession(_chatSession!.chatId); 48 | _chatSession = null; 49 | } 50 | } 51 | 52 | Future startGeneration({ 53 | required String question, 54 | required String modelName, 55 | required Function(String) onMessageUpdate, 56 | required Function(String) onError, 57 | required Function() onComplete, 58 | }) async { 59 | _cleanupCurrentSession(); 60 | final provider = context.read(); 61 | _chatSession = provider.createChatSession(modelName); 62 | 63 | List qmsg = []; 64 | 65 | questions.forEach((item) { 66 | qmsg.add(ollama.Message( 67 | role: ollama.MessageRole.system, 68 | content: item, 69 | )); 70 | }); 71 | 72 | qmsg.add(ollama.Message( 73 | role: ollama.MessageRole.system, 74 | content: provider.instruction, 75 | )); 76 | 77 | qmsg.add(ollama.Message( 78 | role: ollama.MessageRole.user, 79 | content: question, 80 | )); 81 | 82 | try { 83 | final stream = _chatSession!.client.generateChatCompletionStream( 84 | request: ollama.GenerateChatCompletionRequest( 85 | model: modelName, 86 | messages: qmsg, 87 | keepAlive: 5, 88 | options: ollama.RequestOptions( 89 | temperature: provider.temperature, 90 | ), 91 | ), 92 | ); 93 | 94 | bool receivedResponse = false; 95 | Timer? timeoutTimer = Timer(const Duration(seconds: 30), () { 96 | if (!receivedResponse) { 97 | onError(tr("l_timeout")); 98 | } 99 | }); 100 | 101 | String answer = ''; 102 | int tokens = 0; 103 | final stopwatch = Stopwatch()..start(); 104 | 105 | try { 106 | await for (final response in stream) { 107 | if (!receivedResponse) { 108 | receivedResponse = true; 109 | if (timeoutTimer != null && timeoutTimer.isActive) { 110 | timeoutTimer.cancel(); 111 | timeoutTimer = null; 112 | } 113 | 114 | onMessageUpdate("Processing..."); 115 | } 116 | 117 | final token = response.message.content ?? ''; 118 | 119 | if (token.isNotEmpty) { 120 | tokens++; 121 | answer += token; 122 | onMessageUpdate(answer); 123 | 124 | await Future.delayed(Duration(milliseconds: 1)); 125 | } 126 | } 127 | 128 | if (timeoutTimer != null && timeoutTimer.isActive) { 129 | timeoutTimer.cancel(); 130 | timeoutTimer = null; 131 | } 132 | 133 | stopwatch.stop(); 134 | final seconds = stopwatch.elapsedMilliseconds / 1000; 135 | final tokensPerSecond = tokens / seconds; 136 | 137 | answer += "\n\n _MyOllama - $modelName " + 138 | "\nToken/Sec: ${tokensPerSecond.toStringAsFixed(1)}" + 139 | "_"; 140 | 141 | onMessageUpdate(answer); 142 | 143 | await provider.qdb.insertQuestion(provider.curGroupId, 144 | provider.instruction, question, answer, selectedImage, modelName); 145 | 146 | MyEventBus().fire(ChatDoneEvent()); 147 | 148 | selectedImage = null; 149 | 150 | onComplete(); 151 | } catch (e) { 152 | onError(e.toString()); 153 | } 154 | } catch (e) { 155 | onError(e.toString()); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/utils/platform_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | bool isDesktopOrTablet() { 5 | if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { 6 | return true; 7 | } 8 | 9 | // Check if running on iPad 10 | if (Platform.isIOS) { 11 | final window = WidgetsBinding.instance.window; 12 | final size = window.physicalSize; 13 | final pixelRatio = window.devicePixelRatio; 14 | final width = size.width / pixelRatio; 15 | 16 | // iPad typically has a width greater than 768 points 17 | return width >= 768; 18 | } 19 | 20 | return false; 21 | } 22 | -------------------------------------------------------------------------------- /lib/views/chat_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter/scheduler.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:easy_localization/easy_localization.dart'; 9 | import 'package:flutter_styled_toast/flutter_styled_toast.dart'; 10 | import 'package:share_plus/share_plus.dart'; 11 | import 'package:flutter_markdown/flutter_markdown.dart'; 12 | import 'package:url_launcher/url_launcher.dart'; 13 | 14 | import 'package:uuid/uuid.dart'; 15 | import 'package:flutter_chat_ui/flutter_chat_ui.dart'; 16 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types; 17 | import 'package:image_picker/image_picker.dart'; 18 | 19 | import '../provider/main_provider.dart'; 20 | import '../helpers/event_bus.dart'; 21 | import '../widgets/dialogs.dart'; 22 | import '../widgets/drop_menu.dart'; 23 | import '../services/chat_service.dart'; 24 | 25 | import 'settings.dart'; 26 | import '../utils/platform_utils.dart'; 27 | 28 | class ChatView extends StatefulWidget { 29 | const ChatView({Key? key}) : super(key: key); 30 | 31 | @override 32 | createState() => _ChatViewState(); 33 | } 34 | 35 | class _ChatViewState extends State { 36 | final ImagePicker _picker = ImagePicker(); 37 | List _messages = []; 38 | List _questions = []; 39 | final _user = const types.User(id: '82091008-a484-4a89-ae75-a22bf8d6f3ac'); 40 | final _answerer = 41 | const types.User(id: '82091008-a484-4a89-ae75-a22bf8d6f3ad'); 42 | String? _selectedImage; 43 | TextEditingController _questionController = TextEditingController(); 44 | bool _showWait = false; 45 | bool _isProcessed = false; 46 | late final ChatService _chatService; 47 | String _currentGroupId = ''; 48 | 49 | //--------------------------------------------------------------------------// 50 | @override 51 | void initState() { 52 | super.initState(); 53 | _initEventConnector(); 54 | _init(); 55 | } 56 | 57 | @override 58 | void dispose() { 59 | _questionController.dispose(); 60 | _chatService.dispose(); 61 | super.dispose(); 62 | } 63 | 64 | //--------------------------------------------------------------------------// 65 | Future _init() async { 66 | _chatService = ChatService( 67 | context: context, 68 | questions: _questions, 69 | messages: _messages, 70 | answerer: _answerer, 71 | user: _user, 72 | setState: () => setState(() {}), 73 | onMessageUpdate: _handleMessageUpdate); 74 | 75 | final rightMenu = [ 76 | IconButton(onPressed: _newNote, icon: Icon(Icons.add)), 77 | DropMenu(_reloadServerModel, _newNote, _shareAll, _showSettings) 78 | ]; 79 | MyEventBus().fire(ChangeTitleEvent(tr("l_myollama"), rightMenu)); 80 | } 81 | 82 | //--------------------------------------------------------------------------// 83 | void _showSettings() { 84 | // 설정 열기 전 현재 서버 주소 저장 85 | final provider = context.read(); 86 | final oldServerUrl = provider.baseUrl; 87 | 88 | if (isDesktopOrTablet()) { 89 | showDialog( 90 | context: context, 91 | builder: (BuildContext context) { 92 | final size = MediaQuery.of(context).size; 93 | return Dialog( 94 | child: Container( 95 | width: size.width * 0.8, 96 | height: size.height * 0.8, 97 | padding: EdgeInsets.all(16), 98 | child: Column( 99 | crossAxisAlignment: CrossAxisAlignment.start, 100 | children: [ 101 | Row( 102 | children: [ 103 | Text(tr("l_settings"), 104 | style: TextStyle( 105 | fontSize: 20, fontWeight: FontWeight.bold)), 106 | Spacer(), 107 | IconButton( 108 | icon: Icon(Icons.close), 109 | onPressed: () { 110 | Navigator.of(context).pop(); 111 | 112 | // 설정 창이 닫힐 때 서버 주소 변경 확인 113 | final newServerUrl = provider.baseUrl; 114 | if (oldServerUrl != newServerUrl) { 115 | // 서버 주소가 변경되었으면 모델 리로드 116 | MyEventBus().fire(ReloadModelEvent()); 117 | } 118 | }, 119 | ), 120 | ], 121 | ), 122 | SizedBox(height: 16), 123 | Expanded( 124 | child: MySettings(), 125 | ), 126 | ], 127 | ), 128 | ), 129 | ); 130 | }, 131 | ); 132 | } else { 133 | Navigator.push( 134 | context, 135 | MaterialPageRoute(builder: (context) => const MySettings()), 136 | ).then((_) { 137 | final newServerUrl = provider.baseUrl; 138 | if (oldServerUrl != newServerUrl) { 139 | // 서버 주소가 변경되었으면 모델 리로드 140 | MyEventBus().fire(ReloadModelEvent()); 141 | } 142 | }); 143 | } 144 | } 145 | 146 | //--------------------------------------------------------------------------// 147 | void _initEventConnector() async { 148 | MyEventBus().on().listen((event) { 149 | _loadHistoryChats(); 150 | }); 151 | MyEventBus().on().listen((event) { 152 | _newNote(); 153 | }); 154 | MyEventBus().on().listen((event) { 155 | _loadHistoryChats(); 156 | _isProcessed = false; 157 | setState(() {}); 158 | }); 159 | } 160 | 161 | //--------------------------------------------------------------------------// 162 | void _reloadServerModel() async { 163 | final result = await context.read().checkServerConnection(); 164 | MyEventBus().fire(ReloadModelEvent()); 165 | setState(() {}); 166 | 167 | if (result) { 168 | showToast(tr("l_success"), 169 | context: context, position: StyledToastPosition.center); 170 | } else { 171 | showToast(tr("l_error_url"), 172 | context: context, position: StyledToastPosition.center); 173 | } 174 | } 175 | 176 | //--------------------------------------------------------------------------// 177 | void _loadHistoryChats() async { 178 | _showWait = true; 179 | setState(() {}); 180 | 181 | _messages = []; 182 | _questions = await context 183 | .read() 184 | .qdb 185 | .getDetails(context.read().curGroupId); 186 | _questions.reversed.forEach((item) { 187 | if (item["image"] != null) { 188 | _makeImageMessageAdd(item["image"]); 189 | } 190 | int qtime = DateTime.parse(item["created"]).millisecondsSinceEpoch; 191 | _makeMessageAdd(_user, item["question"], Uuid().v4(), qtime, item["id"]); 192 | 193 | String answer = item["answer"]; 194 | _makeMessageAdd(_answerer, answer, Uuid().v4(), qtime, item["id"]); 195 | }); 196 | 197 | _showWait = false; 198 | if (mounted) setState(() {}); 199 | } 200 | 201 | //--------------------------------------------------------------------------// 202 | void _makeMessageAdd(types.User user, String text, String unique, int time, 203 | [int? id = null]) async { 204 | final msg = types.TextMessage( 205 | author: user, 206 | createdAt: time, 207 | id: unique, 208 | text: text, 209 | metadata: {'id': id}, 210 | ); 211 | 212 | _messages.insert(0, msg); 213 | if (mounted) setState(() {}); 214 | } 215 | 216 | //--------------------------------------------------------------------------// 217 | void _shareAll() { 218 | String sharedData = ""; 219 | if (_questions.length == 0) return; 220 | 221 | _questions.forEach((item) { 222 | sharedData += item["question"] + 223 | "\n\n" + 224 | item["answer"] + 225 | "\n\n" + 226 | item["engine"] + 227 | "\n" + 228 | item["created"] + 229 | "\n\n"; 230 | }); 231 | Share.share(sharedData); 232 | } 233 | 234 | //--------------------------------------------------------------------------// 235 | void _newNote([String? question]) { 236 | final provider = context.read(); 237 | 238 | provider.curGroupId = Uuid().v4(); 239 | _messages = []; 240 | _questions = []; 241 | 242 | if (question != null) { 243 | _questionController.text = question; 244 | } else { 245 | _questionController.clear(); 246 | } 247 | 248 | setState(() {}); 249 | } 250 | 251 | //--------------------------------------------------------------------------// 252 | void _handleMessageUpdate(String content) { 253 | // 디버그 로그 254 | if (_messages.isEmpty) { 255 | return; 256 | } 257 | 258 | if (!mounted) { 259 | return; 260 | } 261 | 262 | // 첫 번째 메시지가 응답자(AI)의 메시지인지 확인 263 | if (_messages[0].author.id != _answerer.id) { 264 | return; 265 | } 266 | 267 | // 현재 메시지 텍스트 확인 268 | final currentText = (_messages[0] as types.TextMessage).text; 269 | if (currentText == content) { 270 | return; 271 | } 272 | 273 | setState(() { 274 | _messages[0] = 275 | (_messages[0] as types.TextMessage).copyWith(text: content); 276 | }); 277 | } 278 | 279 | //--------------------------------------------------------------------------// 280 | void _beginAsking(types.PartialText message) async { 281 | // 질문이 입력되면 키보드 감추기 282 | FocusScope.of(context).unfocus(); 283 | 284 | final provider = context.read(); 285 | String question = message.text; 286 | 287 | if (question.isEmpty) return; 288 | 289 | // 이미 처리 중인 경우 중복 요청 방지 290 | if (_isProcessed) { 291 | return; 292 | } 293 | 294 | // 사용자 메시지 추가 295 | types.TextMessage ask = types.TextMessage( 296 | author: _user, 297 | createdAt: DateTime.now().millisecondsSinceEpoch, 298 | id: const Uuid().v4(), 299 | text: question, 300 | ); 301 | 302 | setState(() { 303 | _messages.insert(0, ask); 304 | _isProcessed = true; // 처리 중 상태 설정 305 | }); 306 | 307 | // 선택된 모델 확인 308 | String? modelToUse = provider.selectedModel; 309 | 310 | // 선택된 모델이 없으면 첫 번째 모델 사용 311 | if (modelToUse == null && provider.modelList!.isNotEmpty) { 312 | modelToUse = provider.modelList!.first.model; 313 | if (modelToUse != null) { 314 | provider.setSelectedModel(modelToUse); 315 | } else { 316 | setState(() { 317 | _isProcessed = false; 318 | }); 319 | showToast(tr("l_error_no_models"), 320 | context: context, position: StyledToastPosition.center); 321 | return; 322 | } 323 | } 324 | 325 | if (mounted) { 326 | _chatService.selectedImage = _selectedImage; 327 | 328 | try { 329 | // 응답 메시지 미리 생성 330 | types.TextMessage ansMessage = types.TextMessage( 331 | author: _answerer, 332 | createdAt: DateTime.now().millisecondsSinceEpoch, 333 | id: const Uuid().v4(), 334 | text: "...", 335 | ); 336 | 337 | setState(() { 338 | _messages.insert(0, ansMessage); 339 | }); 340 | 341 | // 채팅 서비스를 통해 응답 생성 시작 342 | await _chatService.startGeneration( 343 | question: question, 344 | modelName: modelToUse!, 345 | onMessageUpdate: (content) { 346 | // 디버그 로그 추가 347 | _handleMessageUpdate(content); 348 | }, 349 | onError: (error) { 350 | // 오류 메시지로 UI 업데이트 351 | if (_messages.isNotEmpty && 352 | _messages[0].author.id == _answerer.id) { 353 | setState(() { 354 | _messages[0] = (_messages[0] as types.TextMessage).copyWith( 355 | text: "오류: $error", 356 | ); 357 | }); 358 | } 359 | 360 | ScaffoldMessenger.of(context).showSnackBar( 361 | SnackBar(content: Text(error)), 362 | ); 363 | setState(() { 364 | _isProcessed = false; 365 | }); 366 | }, 367 | onComplete: () { 368 | setState(() { 369 | _isProcessed = false; 370 | }); 371 | }, 372 | ); 373 | } catch (e) { 374 | // 오류 메시지로 UI 업데이트 375 | if (_messages.isNotEmpty && _messages[0].author.id == _answerer.id) { 376 | setState(() { 377 | _messages[0] = (_messages[0] as types.TextMessage).copyWith( 378 | text: "오류: $e", 379 | ); 380 | }); 381 | } 382 | 383 | setState(() { 384 | _isProcessed = false; 385 | }); 386 | ScaffoldMessenger.of(context).showSnackBar( 387 | SnackBar(content: Text("오류: $e")), 388 | ); 389 | } 390 | } 391 | } 392 | 393 | //--------------------------------------------------------------------------// 394 | void _handleAttachmentPressed() async { 395 | XFile? image = await _picker.pickImage( 396 | source: ImageSource.gallery, 397 | imageQuality: 80, 398 | maxWidth: 500, 399 | maxHeight: 500); 400 | if (image != null) { 401 | File file = File(image.path); 402 | List imageBytes = file.readAsBytesSync(); 403 | _selectedImage = base64Encode(imageBytes); 404 | _makeImageMessageAdd(_selectedImage!); 405 | } 406 | } 407 | 408 | //--------------------------------------------------------------------------// 409 | void _handlePreviewDataFetched( 410 | types.TextMessage message, types.PreviewData previewData) { 411 | final index = _messages.indexWhere((element) => element.id == message.id); 412 | final updatedMessage = (_messages[index] as types.TextMessage).copyWith( 413 | previewData: previewData, 414 | ); 415 | 416 | setState(() { 417 | _messages[index] = updatedMessage; 418 | }); 419 | } 420 | 421 | //--------------------------------------------------------------------------// 422 | void _makeImageMessageAdd(String base64image) { 423 | final msg = types.CustomMessage( 424 | author: _answerer, 425 | id: Uuid().v4(), 426 | metadata: {'data': base64image, 'type': 'image'}, 427 | ); 428 | 429 | _messages.insert(0, msg); 430 | if (mounted) setState(() {}); 431 | } 432 | 433 | //--------------------------------------------------------------------------// 434 | Widget _customMessageBuilder(types.CustomMessage message, 435 | {required int messageWidth}) { 436 | if (message.metadata!['type'] == 'image') { 437 | return RepaintBoundary( 438 | child: Image.memory( 439 | base64Decode(message.metadata!['data']), 440 | fit: BoxFit.contain, 441 | ), 442 | ); 443 | } 444 | return const SizedBox(); 445 | } 446 | 447 | //--------------------------------------------------------------------------// 448 | void _menuRunner(int number, types.TextMessage message) async { 449 | final provider = context.read(); 450 | 451 | final id = message.metadata!['id']; 452 | final record = await provider.qdb.getDetailsById(id); 453 | if (record.length > 0) { 454 | final question = record[0]["question"]; 455 | final answer = record[0]["answer"]; 456 | final created = record[0]["created"]; 457 | final model = record[0]["engine"]; 458 | final sharedData = "$question\n\n$answer\n\n$model\n$created"; 459 | 460 | if (number == 0) { 461 | _newNote(); 462 | } else if (number == 1) { 463 | Clipboard.setData(ClipboardData(text: sharedData)); 464 | showToast(tr("l_copyed"), 465 | context: context, position: StyledToastPosition.center); 466 | } else if (number == 2) { 467 | Share.share(sharedData); 468 | } else if (number == 3) { 469 | final result = await AskDialog.show(context, 470 | title: tr("l_delete"), message: tr("l_delete_question")); 471 | if (result == true) { 472 | await provider.qdb.deleteRecord(id); 473 | // check is last data? 474 | provider.qdb 475 | .getDetails(context.read().curGroupId) 476 | .then((value) { 477 | if (value.length == 0) { 478 | MyEventBus().fire(RefreshMainListEvent()); 479 | _newNote(); 480 | } else { 481 | _loadHistoryChats(); 482 | } 483 | }); 484 | } 485 | } 486 | } 487 | } 488 | 489 | //--------------------------------------------------------------------------// 490 | Widget _messageMenu(types.TextMessage message) { 491 | return Row( 492 | mainAxisAlignment: MainAxisAlignment.end, 493 | children: [ 494 | IconButton( 495 | onPressed: () { 496 | _menuRunner(0, message); 497 | }, 498 | icon: Icon( 499 | Icons.open_in_new, 500 | color: Colors.white, 501 | )), 502 | IconButton( 503 | onPressed: () { 504 | _menuRunner(1, message); 505 | }, 506 | icon: Icon( 507 | Icons.copy, 508 | color: Colors.white, 509 | )), 510 | IconButton( 511 | onPressed: () { 512 | _menuRunner(2, message); 513 | }, 514 | icon: Icon( 515 | Icons.share, 516 | color: Colors.white, 517 | )), 518 | IconButton( 519 | onPressed: () { 520 | _menuRunner(3, message); 521 | }, 522 | icon: Icon( 523 | Icons.delete_outline, 524 | color: Colors.white, 525 | )), 526 | ], 527 | ); 528 | } 529 | 530 | //--------------------------------------------------------------------------// 531 | Widget _textMessageBuilder(types.TextMessage message, 532 | {required int messageWidth, bool? showName}) { 533 | final isAnswer = message.author.id == _answerer.id; 534 | 535 | return Container( 536 | margin: EdgeInsets.all(16), 537 | child: Column( 538 | children: [ 539 | MarkdownBody( 540 | data: message.text, 541 | selectable: true, 542 | onTapLink: (text, href, title) { 543 | launchUrl(Uri.parse(href!)); 544 | }, 545 | ), 546 | if (isAnswer && !_isProcessed) 547 | Column( 548 | children: [ 549 | Divider( 550 | color: Colors.black12, 551 | ), 552 | _messageMenu(message) 553 | ], 554 | ) 555 | ], 556 | )); 557 | } 558 | 559 | //--------------------------------------------------------------------------// 560 | Widget _chatUI() { 561 | final provider = context.read(); 562 | 563 | return Chat( 564 | theme: DefaultChatTheme( 565 | primaryColor: Colors.white, 566 | secondaryColor: Colors.orangeAccent, 567 | inputBackgroundColor: Colors.indigo, 568 | inputTextCursorColor: Colors.white, 569 | backgroundColor: Colors.grey.shade200, 570 | messageInsetsHorizontal: 16, 571 | messageInsetsVertical: 10, 572 | ), 573 | inputOptions: InputOptions( 574 | textEditingController: _questionController, 575 | ), 576 | messages: _messages, 577 | onAttachmentPressed: _handleAttachmentPressed, 578 | onPreviewDataFetched: _handlePreviewDataFetched, 579 | onSendPressed: _beginAsking, 580 | user: _user, 581 | l10n: ChatL10nEn( 582 | inputPlaceholder: tr("l_input_question"), 583 | emptyChatPlaceholder: provider.serveConnected 584 | ? tr("l_no_conversation") 585 | : tr("l_no_server")), 586 | customMessageBuilder: _customMessageBuilder, 587 | textMessageBuilder: (message, 588 | {required messageWidth, required showName}) => 589 | _textMessageBuilder(message, 590 | messageWidth: messageWidth, showName: showName), 591 | ); 592 | } 593 | 594 | //--------------------------------------------------------------------------// 595 | @override 596 | Widget build(BuildContext context) { 597 | return Scaffold( 598 | body: Stack( 599 | children: [ 600 | GestureDetector( 601 | onTap: () { 602 | // 화면의 빈 공간을 터치하면 키보드 숨기기 603 | FocusScope.of(context).unfocus(); 604 | }, 605 | child: _chatUI(), 606 | ), 607 | _showWait ? Center(child: CircularProgressIndicator()) : SizedBox() 608 | ], 609 | ), 610 | ); 611 | } 612 | } 613 | -------------------------------------------------------------------------------- /lib/views/desktop_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:split_view/split_view.dart'; 3 | import 'package:easy_localization/easy_localization.dart'; 4 | 5 | import 'chat_view.dart'; 6 | import '../widgets/title_list.dart'; 7 | import '../widgets/model_selector.dart'; 8 | import '../widgets/list_header.dart'; 9 | import '../widgets/app_title.dart'; 10 | import '../helpers/event_bus.dart'; 11 | 12 | class DesktopView extends StatefulWidget { 13 | const DesktopView({Key? key}) : super(key: key); 14 | 15 | @override 16 | createState() => _DesktopViewState(); 17 | } 18 | 19 | class _DesktopViewState extends State { 20 | final double _initialWeight = 0.35; 21 | List? _action; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | _initEventConnector(); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | super.dispose(); 32 | } 33 | 34 | //--------------------------------------------------------------------------// 35 | void _initEventConnector() async { 36 | MyEventBus().on().listen((event) { 37 | if (mounted) { 38 | _action = event.action; 39 | setState(() {}); 40 | } 41 | }); 42 | } 43 | 44 | //--------------------------------------------------------------------------// 45 | PreferredSizeWidget _buildToolbar() { 46 | return AppBar( 47 | title: Row( 48 | children: [ 49 | AppTitle(textColor: Colors.white), 50 | SizedBox(width: 20), 51 | ModelSelector(), 52 | Spacer(), 53 | ], 54 | ), 55 | backgroundColor: Theme.of(context).primaryColor, 56 | elevation: 1, 57 | actions: _action, 58 | ); 59 | } 60 | 61 | //--------------------------------------------------------------------------// 62 | @override 63 | Widget build(BuildContext context) { 64 | return Scaffold( 65 | appBar: _buildToolbar(), 66 | body: SplitView( 67 | viewMode: SplitViewMode.Horizontal, 68 | indicator: SplitIndicator( 69 | viewMode: SplitViewMode.Horizontal, 70 | color: Colors.grey, 71 | ), 72 | activeIndicator: SplitIndicator( 73 | viewMode: SplitViewMode.Horizontal, 74 | color: Colors.blue, 75 | ), 76 | controller: 77 | SplitViewController(weights: [_initialWeight, 1 - _initialWeight]), 78 | children: [ 79 | // Left panel (1 part) 80 | Container( 81 | color: Theme.of(context).colorScheme.background, 82 | child: TitleList(), 83 | ), 84 | // Right panel (4 parts) 85 | Container( 86 | color: Theme.of(context).colorScheme.background, 87 | child: ChatView(), 88 | ), 89 | ], 90 | ), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/views/drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:flutter_advanced_drawer/flutter_advanced_drawer.dart'; 4 | import 'package:ollama_dart/ollama_dart.dart'; 5 | import 'package:easy_localization/easy_localization.dart'; 6 | 7 | import '../provider/main_provider.dart'; 8 | import '../helpers/event_bus.dart'; 9 | import '../widgets/title_list.dart'; 10 | import '../widgets/model_selector.dart'; 11 | import '../widgets/list_header.dart'; 12 | 13 | import 'chat_view.dart'; 14 | 15 | class MyDrawer extends StatefulWidget { 16 | const MyDrawer({super.key}); 17 | 18 | @override 19 | State createState() => _MyDrawerState(); 20 | } 21 | 22 | class _MyDrawerState extends State { 23 | final _drawer = AdvancedDrawerController(); 24 | List? _action; 25 | Widget? _currentWidget; 26 | 27 | //--------------------------------------------------------------------------// 28 | @override 29 | void initState() { 30 | super.initState(); 31 | _initEventConnector(); 32 | _currentWidget = const ChatView(); 33 | } 34 | 35 | //--------------------------------------------------------------------------// 36 | void _initEventConnector() async { 37 | MyEventBus().on().listen((event) { 38 | if (mounted) { 39 | _action = event.action; 40 | setState(() {}); 41 | } 42 | }); 43 | 44 | MyEventBus().on().listen((event) { 45 | _drawer.hideDrawer(); 46 | }); 47 | 48 | MyEventBus().on().listen((event) { 49 | setState(() {}); // Just refresh the state to rebuild ModelSelector 50 | }); 51 | } 52 | 53 | //--------------------------------------------------------------------------// 54 | void _handleMenuButtonPressed() { 55 | _drawer.showDrawer(); 56 | } 57 | 58 | //--------------------------------------------------------------------------// 59 | PreferredSizeWidget _appbar() { 60 | return AppBar( 61 | title: const ModelSelector(), 62 | leading: IconButton( 63 | onPressed: _handleMenuButtonPressed, 64 | icon: ValueListenableBuilder( 65 | valueListenable: _drawer, 66 | builder: (_, value, __) { 67 | return AnimatedSwitcher( 68 | duration: const Duration(milliseconds: 250), 69 | child: Icon( 70 | value.visible ? Icons.clear : Icons.menu, 71 | key: ValueKey(value.visible), 72 | ), 73 | ); 74 | }, 75 | ), 76 | ), 77 | actions: _action, 78 | ); 79 | } 80 | 81 | //--------------------------------------------------------------------------// 82 | Widget _listContainer() { 83 | return Container( 84 | color: Colors.white, 85 | child: const Column( 86 | crossAxisAlignment: CrossAxisAlignment.center, 87 | mainAxisAlignment: MainAxisAlignment.start, 88 | children: [ 89 | ListHeader(), 90 | Expanded(child: TitleList()), 91 | ], 92 | ), 93 | ); 94 | } 95 | 96 | //--------------------------------------------------------------------------// 97 | @override 98 | Widget build(BuildContext context) { 99 | return AdvancedDrawer( 100 | backdropColor: Colors.indigo, 101 | controller: _drawer, 102 | animateChildDecoration: true, 103 | rtlOpening: false, 104 | openScale: 1.0, 105 | openRatio: 0.8, 106 | disabledGestures: true, 107 | child: Scaffold( 108 | appBar: _appbar(), 109 | body: Container(child: _currentWidget), 110 | ), 111 | drawer: SafeArea( 112 | child: _listContainer(), 113 | ), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/views/settings.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:easy_localization/easy_localization.dart'; 4 | import 'package:app_settings/app_settings.dart'; 5 | import 'package:url_launcher/url_launcher.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'package:flutter_styled_toast/flutter_styled_toast.dart'; 8 | import '../utils/platform_utils.dart'; 9 | 10 | import '../widgets/text_fields.dart'; 11 | import '../provider/main_provider.dart'; 12 | import '../widgets/dialogs.dart'; 13 | import '../helpers/event_bus.dart'; 14 | 15 | final title_style = TextStyle(fontSize: 16, color: Colors.grey[500], fontWeight: FontWeight.bold); 16 | final linkStyle = TextStyle(fontSize: 15, color: Colors.blueAccent, fontWeight: FontWeight.bold); 17 | final TextStyle midSizeStyle = TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black54); 18 | final TextStyle dvcSubStyle = TextStyle(fontSize: 13, fontWeight: FontWeight.w400, color: Colors.black38); 19 | 20 | class MySettings extends StatefulWidget { 21 | const MySettings({Key? key}) : super(key: key); 22 | 23 | @override 24 | createState()=>_MySettingsState(); 25 | } 26 | 27 | class _MySettingsState extends State { 28 | final server_address = TextEditingController(); 29 | final instruction = TextEditingController(); 30 | final temperature = TextEditingController(); 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | _loadPreferences(); 36 | } 37 | 38 | @override 39 | void dispose() { 40 | super.dispose(); 41 | } 42 | 43 | //--------------------------------------------------------------------------// 44 | void _loadPreferences() { 45 | final provider = context.read(); 46 | provider.loadPreferences(); 47 | 48 | server_address.text = provider.baseUrl; 49 | instruction.text = provider.instruction; 50 | temperature.text = provider.temperature.toString(); 51 | } 52 | 53 | //--------------------------------------------------------------------------// 54 | void _deleteAllRecords() async { 55 | final provider = context.read(); 56 | final result = await AskDialog.show(context, title: 'Delete all data', message: 'Are you sure?'); 57 | if (result == true) { 58 | await provider.qdb.deleteAllRecords(); 59 | MyEventBus().fire(NewChatBeginEvent()); 60 | MyEventBus().fire(RefreshMainListEvent()); 61 | } 62 | } 63 | 64 | 65 | //--------------------------------------------------------------------------// 66 | Widget ActionCardPanel(IconData leadIcon, String title, String? subtitle, IconData trailIcon, Function action) { 67 | return Card( 68 | child: ListTile( 69 | leading: Icon(leadIcon), 70 | title: Text(title, style: midSizeStyle), 71 | subtitle: subtitle != null ? Text(subtitle, style: dvcSubStyle) : null, 72 | trailing: Icon(trailIcon), 73 | onTap: action as void Function()?, 74 | ), 75 | ); 76 | } 77 | 78 | //--------------------------------------------------------------------------// 79 | bool _isValidUrl(String url) { 80 | final urlPattern = RegExp( 81 | r'^(http|https):\/\/([\w-]+\.)+[\w-]+(:\d+)?(\/[\w- .\/?%&=]*)?$', 82 | caseSensitive: false, 83 | multiLine: false, 84 | ); 85 | return urlPattern.hasMatch(url); 86 | } 87 | 88 | //--------------------------------------------------------------------------// 89 | @override 90 | Widget build(BuildContext context) { 91 | final provider = context.read(); 92 | final version = provider.version + ' (' + provider.buildNumber.toString() + ')'; 93 | final isDesktop = isDesktopOrTablet(); // Use the new function 94 | 95 | final widgets = [ 96 | ListTile(title: Text(tr("l_ollama_setting"), style: title_style)), 97 | Row( 98 | children: [ 99 | Expanded( 100 | child:QTextField(tr("l_server_address"), server_address, (_){}) 101 | ), 102 | IconButton( 103 | icon: Icon(Icons.network_check, size: 30,), 104 | onPressed: () async { 105 | if (_isValidUrl(server_address.text)) { 106 | final reached = await provider.setBaseUrl(server_address.text); 107 | if (reached) { 108 | MyEventBus().fire(ReloadModelEvent()); 109 | showToast(tr("l_success"), context: context, position: StyledToastPosition.center); 110 | } else { 111 | showToast(tr("l_error_url"), context: context, position: StyledToastPosition.center); 112 | } 113 | } else { 114 | AskDialog.show(context, title: tr("l_error"), message: tr("l_invalid_url")); 115 | } 116 | }, 117 | ) 118 | ], 119 | ), 120 | QTextField(tr("l_instructions"), instruction, (String value){ 121 | provider.setInstruction(value); 122 | }, maxLines: 5), 123 | QTextField(tr("l_temp"), temperature, (String value){ 124 | provider.setTemperature(double.parse(value)); 125 | }), 126 | ActionCardPanel(Icons.download_for_offline_outlined, tr("l_download"), tr("l_download_ollama"), Icons.arrow_forward_ios, () { 127 | launchUrl(Uri.parse("https://ollama.com/download")); 128 | }), 129 | ActionCardPanel(Icons.help_outline, tr("l_howto"), tr("l_howto_1"), Icons.arrow_forward_ios, () { 130 | launchUrl(Uri.parse("http://practical.kr/?p=809")); 131 | }), 132 | ListTile(title: Text(tr("l_app_info"), style: title_style)), 133 | ActionCardPanel(Icons.memory, tr("l_open_source"), tr("l_open_comment"), Icons.arrow_forward_ios, () { 134 | launchUrl(Uri.parse("https://github.com/bipark/my_ollama_app")); 135 | }), 136 | ActionCardPanel(Icons.delete_forever_outlined, tr("l_delete"), tr("l_delete_all"), Icons.arrow_forward_ios, () { 137 | _deleteAllRecords(); 138 | }), 139 | ActionCardPanel(Icons.settings_applications_outlined, tr("l_app_info"), null, Icons.arrow_forward_ios, (){ 140 | AppSettings.openAppSettings(); 141 | }), 142 | ActionCardPanel(Icons.info, tr("l_myollama"), tr("l_version") + version, Icons.arrow_forward_ios, () { 143 | launchUrl(Uri.parse("http://practical.kr")); 144 | }), 145 | ]; 146 | 147 | final content = Container( 148 | padding: const EdgeInsets.all(10.0), 149 | child: Material( 150 | child: CustomScrollView( 151 | slivers: [ 152 | SliverList( 153 | delegate: SliverChildBuilderDelegate( 154 | (context, index) => widgets[index], 155 | childCount: widgets.length, 156 | ), 157 | ), 158 | ], 159 | ), 160 | ), 161 | ); 162 | 163 | if (isDesktop) { 164 | return content; 165 | } 166 | 167 | return Scaffold( 168 | appBar: AppBar( 169 | title: Row( 170 | children: [ 171 | Text(tr("l_settings"), style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold)) 172 | ], 173 | ), 174 | actions: [ 175 | IconButton( 176 | onPressed: () { 177 | _loadPreferences(); 178 | }, 179 | icon: Icon(Icons.refresh, color: Colors.white) 180 | ), 181 | ], 182 | ), 183 | body: GestureDetector( 184 | onTap: () { 185 | FocusScope.of(context).requestFocus(FocusNode()); 186 | }, 187 | child: content, 188 | ), 189 | ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /lib/widgets/app_title.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_localization/easy_localization.dart'; 3 | 4 | class AppTitle extends StatelessWidget { 5 | final double? fontSize; 6 | final Color? textColor; 7 | 8 | const AppTitle({ 9 | super.key, 10 | this.fontSize = 17, 11 | this.textColor = Colors.white, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Row( 17 | children: [ 18 | SizedBox(width: 10), 19 | Text(tr("l_myollama"), 20 | style: TextStyle( 21 | fontSize: fontSize, 22 | fontWeight: FontWeight.bold, 23 | color: textColor, 24 | )), 25 | ], 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/widgets/dialogs.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AskDialog extends StatelessWidget { 4 | final String title; 5 | final String message; 6 | final String confirmLabel; 7 | final String cancelLabel; 8 | final VoidCallback? onConfirm; 9 | final VoidCallback? onCancel; 10 | final bool barrierDismissible; 11 | 12 | const AskDialog({ 13 | super.key, 14 | this.title = 'Confirm', 15 | this.message = 'Are you sure?', 16 | this.confirmLabel = 'OK', 17 | this.cancelLabel = 'Cancel', 18 | this.onConfirm, 19 | this.onCancel, 20 | this.barrierDismissible = true, 21 | }); 22 | 23 | static Future show( 24 | BuildContext context, { 25 | String title = 'Confirm', 26 | String message = 'Are you sure?', 27 | String confirmLabel = 'OK', 28 | String cancelLabel = 'Cancel', 29 | VoidCallback? onConfirm, 30 | VoidCallback? onCancel, 31 | bool barrierDismissible = true, 32 | }) { 33 | return showDialog( 34 | context: context, 35 | barrierDismissible: barrierDismissible, 36 | builder: (context) => AskDialog( 37 | title: title, 38 | message: message, 39 | confirmLabel: confirmLabel, 40 | cancelLabel: cancelLabel, 41 | onConfirm: onConfirm, 42 | onCancel: onCancel, 43 | barrierDismissible: barrierDismissible, 44 | ), 45 | ); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return AlertDialog( 51 | shape: RoundedRectangleBorder( 52 | borderRadius: BorderRadius.circular(10.0), 53 | ), 54 | title: Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), 55 | content: Text(message), 56 | actions: [ 57 | TextButton( 58 | onPressed: () { 59 | onCancel?.call(); 60 | Navigator.of(context).pop(false); 61 | }, 62 | child: Text(cancelLabel), 63 | ), 64 | FilledButton( 65 | onPressed: () { 66 | onConfirm?.call(); 67 | Navigator.of(context).pop(true); 68 | }, 69 | child: Text(confirmLabel), 70 | ), 71 | ], 72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /lib/widgets/drop_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_localization/easy_localization.dart'; 3 | 4 | class DropMenu extends StatefulWidget { 5 | final Function netCheck; 6 | final Function newNote; 7 | final Function shareAll; 8 | final Function showSettings; 9 | const DropMenu(this.netCheck, this.newNote, this.shareAll, this.showSettings, {super.key}); 10 | 11 | @override 12 | State createState() => _DropMenuState(); 13 | } 14 | 15 | class _DropMenuState extends State { 16 | final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'DropMenu'); 17 | 18 | @override 19 | void dispose() { 20 | _buttonFocusNode.dispose(); 21 | super.dispose(); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return MenuAnchor( 27 | childFocusNode: _buttonFocusNode, 28 | menuChildren: [ 29 | MenuItemButton( 30 | child: Row( 31 | children: [ 32 | const Icon(Icons.network_check), 33 | const SizedBox(width: 8), 34 | Text(tr("l_server_check")), 35 | ], 36 | ), 37 | onPressed: (){ 38 | widget.netCheck(); 39 | }, 40 | ), 41 | MenuItemButton( 42 | child: Row( 43 | children: [ 44 | const Icon(Icons.add), 45 | const SizedBox(width: 8), 46 | Text(tr("l_new_chat")), 47 | ], 48 | ), 49 | onPressed: () { 50 | widget.newNote(); 51 | }, 52 | ), 53 | MenuItemButton( 54 | child: Row( 55 | children: [ 56 | const Icon(Icons.share), 57 | const SizedBox(width: 8), 58 | Text(tr("l_share_all")), 59 | ], 60 | ), 61 | onPressed: () { 62 | widget.shareAll(); 63 | }, 64 | ), 65 | MenuItemButton( 66 | child: Row( 67 | children: [ 68 | const Icon(Icons.settings), 69 | const SizedBox(width: 8), 70 | Text(tr("l_settings")), 71 | ], 72 | ), 73 | onPressed: () { 74 | widget.showSettings(); 75 | }, 76 | ), 77 | ], 78 | builder: (_, MenuController controller, Widget? child) { 79 | return IconButton( 80 | focusNode: _buttonFocusNode, 81 | onPressed: () { 82 | if (controller.isOpen) { 83 | controller.close(); 84 | } else { 85 | controller.open(); 86 | } 87 | }, 88 | icon: const Icon(Icons.more_vert), 89 | ); 90 | }, 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/widgets/list_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_localization/easy_localization.dart'; 3 | 4 | import '../helpers/event_bus.dart'; 5 | import 'app_title.dart'; 6 | 7 | class ListHeader extends StatelessWidget { 8 | const ListHeader({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Container( 13 | decoration: BoxDecoration( 14 | color: Colors.indigo, 15 | border: Border(bottom: BorderSide(color: Colors.grey.shade200)), 16 | ), 17 | height: 56, 18 | child: Row( 19 | children: [ 20 | AppTitle(), 21 | Spacer(), 22 | IconButton( 23 | onPressed: () { 24 | MyEventBus().fire(RefreshMainListEvent()); 25 | }, 26 | icon: Icon(Icons.refresh, color: Colors.white) 27 | ), 28 | ], 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/widgets/model_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:ollama_dart/ollama_dart.dart'; 4 | import 'package:easy_localization/easy_localization.dart'; 5 | 6 | import '../provider/main_provider.dart'; 7 | import '../helpers/event_bus.dart'; 8 | 9 | class ModelSelector extends StatefulWidget { 10 | const ModelSelector({super.key}); 11 | 12 | @override 13 | State createState() => _ModelSelectorState(); 14 | } 15 | 16 | class _ModelSelectorState extends State { 17 | final MenuController _menuController = MenuController(); 18 | List _models = []; 19 | bool _isLoading = true; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | _initEventConnector(); 25 | WidgetsBinding.instance.addPostFrameCallback((_) { 26 | _loadModels(); 27 | }); 28 | } 29 | 30 | //--------------------------------------------------------------------------// 31 | void _initEventConnector() async { 32 | MyEventBus().on().listen((event) { 33 | _loadModels(); 34 | }); 35 | } 36 | 37 | //--------------------------------------------------------------------------// 38 | void _loadModels() { 39 | setState(() => _isLoading = true); 40 | 41 | final provider = context.read(); 42 | if (provider.modelList != null && provider.modelList!.isNotEmpty) { 43 | _models = provider.modelList! 44 | .map((Model e) => e.model) 45 | .where((model) => model != null) 46 | .map((model) => model!) 47 | .toList(); 48 | 49 | if (provider.selectedModel == null && _models.isNotEmpty) { 50 | Future.microtask(() { 51 | provider.setSelectedModel(_models.first); 52 | }); 53 | } 54 | } else { 55 | _models = []; 56 | } 57 | 58 | setState(() => _isLoading = false); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return Consumer( 64 | builder: (context, provider, child) { 65 | if (_isLoading) { 66 | return SizedBox( 67 | width: 24, 68 | height: 24, 69 | child: CircularProgressIndicator( 70 | strokeWidth: 2, 71 | valueColor: AlwaysStoppedAnimation(Colors.white), 72 | ), 73 | ); 74 | } 75 | 76 | if (_models.isEmpty) { 77 | return TextButton.icon( 78 | onPressed: null, 79 | icon: Text(tr("l_no_models"), 80 | style: TextStyle( 81 | color: Colors.red[300], fontWeight: FontWeight.bold)), 82 | label: Icon(Icons.error_outline, color: Colors.red[300]), 83 | ); 84 | } 85 | 86 | final displayModel = provider.selectedModel ?? 87 | (_models.isNotEmpty ? _models.first : tr("l_no_model")); 88 | 89 | return MenuAnchor( 90 | alignmentOffset: Offset(0, 8), 91 | controller: _menuController, 92 | menuChildren: [ 93 | for (final String option in _models) 94 | MenuItemButton( 95 | onPressed: () { 96 | provider.setSelectedModel(option); 97 | _menuController.close(); 98 | }, 99 | child: Text(option, style: TextStyle(color: Colors.black)), 100 | ), 101 | ], 102 | builder: (context, controller, child) { 103 | return TextButton.icon( 104 | onPressed: () { 105 | if (controller.isOpen) { 106 | controller.close(); 107 | } else { 108 | controller.open(); 109 | } 110 | }, 111 | icon: Text(displayModel, 112 | style: TextStyle( 113 | color: Colors.yellowAccent, fontWeight: FontWeight.bold)), 114 | label: Icon(Icons.arrow_drop_down, color: Colors.white), 115 | ); 116 | }, 117 | ); 118 | }, 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/widgets/text_fields.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | 4 | class QTextField extends StatefulWidget { 5 | final String label; 6 | final Function(String) onChanged; 7 | final TextEditingController controller; 8 | final maxLines; 9 | 10 | QTextField(this.label, this.controller, this.onChanged, {this.maxLines = 1}); 11 | @override 12 | _QTextFieldState createState() => _QTextFieldState(); 13 | } 14 | 15 | class _QTextFieldState extends State { 16 | late FocusNode _focusNode; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | _focusNode = FocusNode(); 22 | _focusNode.addListener(() { 23 | if (!_focusNode.hasFocus) { 24 | widget.onChanged(widget.controller.text); 25 | } 26 | }); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | _focusNode.dispose(); 32 | super.dispose(); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Container( 38 | padding: EdgeInsets.only(bottom: 20), 39 | child: TextField( 40 | keyboardType: TextInputType.multiline, 41 | controller: widget.controller, 42 | focusNode: _focusNode, 43 | maxLines: widget.maxLines, 44 | decoration: InputDecoration( 45 | isDense: true, 46 | labelText: widget.label, 47 | floatingLabelStyle: TextStyle(color: Colors.grey, fontSize: 16), 48 | floatingLabelBehavior: FloatingLabelBehavior.always, 49 | border: OutlineInputBorder( 50 | borderRadius: BorderRadius.circular(5), 51 | borderSide: BorderSide(color: Colors.black12) 52 | ), 53 | ), 54 | onChanged: widget.onChanged, 55 | ) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/widgets/title_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:easy_localization/easy_localization.dart'; 4 | 5 | import '../provider/main_provider.dart'; 6 | import '../helpers/event_bus.dart'; 7 | 8 | import 'dialogs.dart'; 9 | 10 | class TitleList extends StatefulWidget { 11 | const TitleList({Key? key}) : super(key: key); 12 | 13 | @override 14 | createState()=>_TitleListState(); 15 | } 16 | 17 | class _TitleListState extends State { 18 | bool _showWait = false; 19 | List _titles = []; 20 | int _selectedIndex = 0; 21 | TextEditingController _search = TextEditingController(); 22 | 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _initEventConnector(); 28 | _loadData(refresh: true); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | super.dispose(); 34 | } 35 | 36 | //--------------------------------------------------------------------------// 37 | void _initEventConnector() async { 38 | MyEventBus().on().listen((event) { 39 | _loadData(); 40 | }); 41 | } 42 | 43 | 44 | //--------------------------------------------------------------------------// 45 | Future _loadData({bool refresh = false}) async { 46 | if (mounted) { 47 | _showWait = true; 48 | setState(() {}); 49 | 50 | _titles = await context.read().qdb.getTitles(); 51 | if (refresh) { 52 | if (_titles.length > 0) _selectTitle(0); 53 | } 54 | 55 | _showWait = false; 56 | setState(() {}); 57 | } 58 | } 59 | 60 | //--------------------------------------------------------------------------// 61 | void _deleteQuestion(String groupid) async { 62 | final provider = context.read(); 63 | final result = await AskDialog.show(context, title: tr("l_delete"), message: tr("l_delete_question")); 64 | if (result == true) { 65 | await provider.qdb.deleteQuestions(groupid); 66 | _loadData(); 67 | _selectedIndex = 0; 68 | setState(() {}); 69 | } 70 | } 71 | 72 | //--------------------------------------------------------------------------// 73 | Widget _titlePanel(int index) { 74 | String title = _titles[index]["question"]; 75 | if (title.length > 70) { 76 | title = title.substring(0, 70) + "..."; 77 | title = title.replaceAll("\n", " "); 78 | title = title.trimLeft(); 79 | } 80 | 81 | return Container( 82 | color : _selectedIndex == index ? Colors.grey.shade200 : Colors.transparent, 83 | padding: EdgeInsets.fromLTRB(14, 6, 10, 6), 84 | child: Row( 85 | children: [ 86 | Expanded( 87 | child : Column( 88 | crossAxisAlignment: CrossAxisAlignment.start, 89 | children: [ 90 | Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w400)), 91 | Text(_titles[index]["created"].toString(), style: TextStyle(fontSize: 12, color: Colors.grey)) 92 | ] 93 | ) 94 | ), 95 | Row( 96 | children: [ 97 | IconButton(onPressed: () { 98 | _deleteQuestion(_titles[index]["groupid"]); 99 | }, icon: Icon(Icons.delete_outline, size: 20, color: Colors.black54,)) 100 | ], 101 | ) 102 | ], 103 | ), 104 | ); 105 | } 106 | 107 | //--------------------------------------------------------------------------// 108 | void _selectTitle(int index) { 109 | final provider = context.read(); 110 | 111 | _selectedIndex = index; 112 | provider.curGroupId = _titles[index]["groupid"]; 113 | MyEventBus().fire(CloseDrawerEvent()); 114 | MyEventBus().fire(LoadHistoryGroupListEvent()); 115 | setState(() {}); 116 | } 117 | 118 | //--------------------------------------------------------------------------// 119 | void _startSearch() async { 120 | _showWait = true; 121 | setState(() {}); 122 | 123 | final provider = context.read(); 124 | if (_search.text != "") { 125 | _titles = await provider.qdb.searchKeywords(_search.text); 126 | } 127 | _showWait = false; 128 | setState(() {}); 129 | } 130 | 131 | //--------------------------------------------------------------------------// 132 | Widget _searchPanel() { 133 | return Container( 134 | margin: EdgeInsets.all(10), 135 | decoration: BoxDecoration( 136 | border: Border.all(width: 1, color: Colors.indigo), 137 | ), 138 | child: Row( 139 | children: [ 140 | Expanded( 141 | child: TextField( 142 | controller: _search, 143 | decoration: InputDecoration( 144 | hintText: tr("l_search"), 145 | border: InputBorder.none, 146 | contentPadding: EdgeInsets.fromLTRB(10, 0, 10, 0), 147 | ), 148 | onSubmitted: (String value) { 149 | _startSearch(); 150 | }, 151 | ), 152 | ), 153 | IconButton( 154 | icon: Icon(Icons.search), 155 | onPressed: _startSearch, 156 | ), 157 | IconButton( 158 | icon: Icon(Icons.clear), 159 | onPressed: () { 160 | FocusScope.of(context).requestFocus(FocusNode()); 161 | _search.text = ""; 162 | _loadData(); 163 | }, 164 | ) 165 | ], 166 | ), 167 | ); 168 | } 169 | 170 | //--------------------------------------------------------------------------// 171 | Future _onRefresh() async { 172 | return _loadData(refresh: true); 173 | } 174 | 175 | //--------------------------------------------------------------------------// 176 | Widget _buildList() { 177 | if (_showWait) { 178 | return Center(child: CircularProgressIndicator()); 179 | } 180 | 181 | if (_titles.isEmpty) { 182 | return RefreshIndicator( 183 | onRefresh: _onRefresh, 184 | child: ListView( 185 | children: [ 186 | Container( 187 | height: 100, 188 | alignment: Alignment.center, 189 | child: Text(tr("l_no_items")), 190 | ), 191 | ], 192 | ), 193 | ); 194 | } 195 | 196 | return RefreshIndicator( 197 | onRefresh: _onRefresh, 198 | child: ListView.builder( 199 | itemCount: _titles.length, 200 | itemBuilder: (context, index) { 201 | return Material( 202 | color: Colors.transparent, 203 | child: InkWell( 204 | onTap: () { 205 | _selectTitle(index); 206 | }, 207 | child: _titlePanel(index), 208 | ), 209 | ); 210 | }, 211 | ), 212 | ); 213 | } 214 | 215 | //--------------------------------------------------------------------------// 216 | @override 217 | Widget build(BuildContext context) { 218 | return Container( 219 | color: Colors.white, 220 | child: Column( 221 | children: [ 222 | _searchPanel(), 223 | Expanded( 224 | child: _buildList(), 225 | ), 226 | ], 227 | ), 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import file_selector_macos 9 | import package_info_plus 10 | import path_provider_foundation 11 | import share_plus 12 | import shared_preferences_foundation 13 | import sqflite_darwin 14 | import url_launcher_macos 15 | 16 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 17 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 18 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 19 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 20 | SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 21 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 22 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 23 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 24 | } 25 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - file_selector_macos (0.0.1): 3 | - FlutterMacOS 4 | - FlutterMacOS (1.0.0) 5 | - package_info_plus (0.0.1): 6 | - FlutterMacOS 7 | - path_provider_foundation (0.0.1): 8 | - Flutter 9 | - FlutterMacOS 10 | - share_plus (0.0.1): 11 | - FlutterMacOS 12 | - shared_preferences_foundation (0.0.1): 13 | - Flutter 14 | - FlutterMacOS 15 | - sqflite_darwin (0.0.4): 16 | - Flutter 17 | - FlutterMacOS 18 | - url_launcher_macos (0.0.1): 19 | - FlutterMacOS 20 | 21 | DEPENDENCIES: 22 | - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) 23 | - FlutterMacOS (from `Flutter/ephemeral`) 24 | - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) 25 | - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) 26 | - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) 27 | - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) 28 | - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) 29 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 30 | 31 | EXTERNAL SOURCES: 32 | file_selector_macos: 33 | :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos 34 | FlutterMacOS: 35 | :path: Flutter/ephemeral 36 | package_info_plus: 37 | :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos 38 | path_provider_foundation: 39 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin 40 | share_plus: 41 | :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos 42 | shared_preferences_foundation: 43 | :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin 44 | sqflite_darwin: 45 | :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin 46 | url_launcher_macos: 47 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 48 | 49 | SPEC CHECKSUMS: 50 | file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d 51 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 52 | package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b 53 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 54 | share_plus: 1fa619de8392a4398bfaf176d441853922614e89 55 | shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 56 | sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d 57 | url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 58 | 59 | PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 60 | 61 | COCOAPODS: 1.16.2 62 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/macos/Runner/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/macos/Runner/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/macos/Runner/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/macos/Runner/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/macos/Runner/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bipark/my_ollama_app/95a479cd7ab5513fd491247f95b51054d4a6a288/macos/Runner/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} -------------------------------------------------------------------------------- /macos/Runner/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = my_ollama 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.rtlink.myOllama 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.rtlink. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.network.server 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | NSHumanReadableCopyright 31 | $(PRODUCT_COPYRIGHT) 32 | NSMainNibFile 33 | MainMenu 34 | NSPhotoLibraryUsageDescription 35 | The app needs permission to use the photo album to analyze photos. 36 | NSPrincipalClass 37 | NSApplication 38 | 39 | 40 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /my_ollama.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: my_ollama 2 | description: "MyOllama" 3 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 4 | 5 | version: 1.0.8+41204 6 | 7 | environment: 8 | sdk: ^3.5.4 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | cupertino_icons: ^1.0.8 15 | provider: ^6.1.2 16 | flutter_advanced_drawer: ^1.3.7 17 | sqflite: ^2.4.1 18 | intl: ^0.19.0 19 | event_bus: ^2.0.0 20 | flutter_chat_ui: ^1.6.15 21 | uuid: ^3.0.7 22 | ollama_dart: ^0.2.2 23 | image_picker: ^1.1.2 24 | shared_preferences: ^2.3.3 25 | package_info_plus: ^8.1.1 26 | dart_ping: ^9.0.1 27 | url_launcher: ^6.3.1 28 | easy_localization: ^3.0.7 29 | app_settings: ^5.1.1 30 | flutter_styled_toast: ^2.2.1 31 | share_plus: ^10.1.2 32 | flutter_markdown: ^0.7.4+3 33 | split_view: ^3.2.1 34 | sqflite_common_ffi: ^2.3.0 35 | 36 | dev_dependencies: 37 | flutter_test: 38 | sdk: flutter 39 | 40 | flutter_lints: ^4.0.0 41 | 42 | flutter: 43 | uses-material-design: true 44 | assets: 45 | - assets/images/ 46 | - assets/translations/ 47 | 48 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:my_ollama/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------