├── .fvmrc ├── .github ├── art │ ├── 0.gif │ ├── 1.gif │ ├── screenshot.png │ ├── screenshot_0.png │ ├── screenshot_1.png │ ├── screenshot_2.png │ ├── theme_dark.png │ └── theme_light.png └── workflows │ ├── CD.yml │ └── CI.yml ├── .gitignore ├── .metadata ├── .run ├── build_apk.run.xml ├── build_macos.run.xml ├── build_runner.run.xml ├── dev.run.xml └── prod.run.xml ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── shinhyo │ │ │ │ └── ollama │ │ │ │ └── talk │ │ │ │ └── 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 ├── debug.keystore ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── icons │ ├── chat-outline.svg │ ├── database-outline.svg │ ├── dots-horizontal.svg │ ├── forum-outline.svg │ ├── github.svg │ ├── head-snowflake-outline.svg │ ├── history.svg │ ├── keyboard-outline.svg │ ├── menu-close.svg │ ├── menu-open.svg │ ├── message-text-outline.svg │ ├── ollama.svg │ ├── robot-outline-dark.svg │ ├── robot-outline-light.svg │ ├── robot-outline.svg │ └── tune.svg └── images │ ├── app_icon_dark.png │ └── app_icon_light.png ├── build.yaml ├── coverage ├── filtered_lcov.info ├── lcov.info └── run_coverage.sh ├── devtools_options.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ └── Package.resolved ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── 100.png │ │ │ ├── 102.png │ │ │ ├── 1024.png │ │ │ ├── 108.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 16.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 20.png │ │ │ ├── 216.png │ │ │ ├── 234.png │ │ │ ├── 256.png │ │ │ ├── 258.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 48.png │ │ │ ├── 50.png │ │ │ ├── 512.png │ │ │ ├── 55.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 64.png │ │ │ ├── 66.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 88.png │ │ │ ├── 92.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 └── RunnerTests │ └── RunnerTests.swift ├── lib ├── config │ ├── build_config.dart │ └── dependencies.dart ├── data │ ├── config │ │ └── network_config_impl.dart │ ├── repository │ │ ├── database_repository_impl.dart │ │ ├── device_info_repository_impl.dart │ │ ├── ollama_repository_impl.dart │ │ └── shared_preferences_impl.dart │ ├── source │ │ ├── local │ │ │ ├── database │ │ │ │ ├── chat_room_dao.dart │ │ │ │ ├── message_dao.dart │ │ │ │ └── table │ │ │ │ │ ├── auto_incrementing_primary_key.dart │ │ │ │ │ ├── chat_room_table.dart │ │ │ │ │ └── message_table.dart │ │ │ └── drift.dart │ │ └── network │ │ │ └── dio.dart │ └── utils │ │ └── stream_parser.dart ├── domain │ ├── config │ │ └── network_config.dart │ ├── models │ │ ├── chat_mode.dart │ │ ├── chat_room_entity.dart │ │ ├── message_entity.dart │ │ ├── ollama_chat_entity.dart │ │ ├── ollama_entity.dart │ │ ├── ollama_generate_entity.dart │ │ └── role.dart │ ├── repository │ │ ├── database_repository.dart │ │ ├── device_info_repository.dart │ │ ├── ollama_repository.dart │ │ └── preferences_repository.dart │ └── use_cases │ │ ├── create_chat_room_name_use_case.dart │ │ ├── create_chat_room_use_cases.dart │ │ ├── ollama_generate_use_case.dart │ │ ├── send_chat_use_case.dart │ │ ├── update_host_use_case.dart │ │ └── watch_selected_model_use_case.dart ├── i18n │ └── strings.i18n.json ├── main.dart ├── main_viewmodel.dart ├── ui │ ├── chat │ │ ├── chat_screen.dart │ │ ├── chat_viewmodel.dart │ │ └── widget │ │ │ ├── chat_bubble.dart │ │ │ ├── chat_input_text.dart │ │ │ ├── chat_list.dart │ │ │ ├── chat_toolbar.dart │ │ │ └── drawer_history.dart │ ├── core │ │ ├── base │ │ │ ├── base_command.dart │ │ │ ├── base_cubit.dart │ │ │ ├── base_screen.dart │ │ │ └── max_width_container.dart │ │ ├── themes │ │ │ ├── colors.dart │ │ │ ├── icons.dart │ │ │ ├── theme.dart │ │ │ ├── theme_ext.dart │ │ │ └── theme_text.dart │ │ └── widget │ │ │ ├── chip_menu_anchor.dart │ │ │ ├── dialog.dart │ │ │ ├── dragging_appbar.dart │ │ │ ├── dragging_widget.dart │ │ │ └── overay_boundary.dart │ ├── root │ │ ├── root_screen.dart │ │ ├── root_viewmodel.dart │ │ └── widget │ │ │ ├── root_navigation_bar.dart │ │ │ ├── root_navigation_rail.dart │ │ │ └── root_tab.dart │ ├── routing │ │ ├── route │ │ │ ├── chat_history_shell_branch.dart │ │ │ ├── shell_branch_home.dart │ │ │ ├── shell_branch_setting.dart │ │ │ └── shell_route_root.dart │ │ └── router.dart │ └── tab │ │ ├── history │ │ ├── chat_history_screen.dart │ │ ├── chat_history_viewmodel.dart │ │ └── widget │ │ │ ├── chat_room_header.dart │ │ │ └── chat_room_list.dart │ │ ├── home │ │ ├── home_screen.dart │ │ ├── home_viewmodel.dart │ │ └── widget │ │ │ ├── home_input_text.dart │ │ │ ├── home_tool_bar.dart │ │ │ └── home_toolbar_chips.dart │ │ └── setting │ │ ├── setting_screen.dart │ │ └── setting_viewmodel.dart └── utils │ ├── keyboard_util.dart │ ├── logger.dart │ ├── platform_util.dart │ ├── size_ext.dart │ └── string_ext.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ └── generated_plugins.cmake └── runner │ ├── CMakeLists.txt │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ └── Flutter-Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── 100.png │ │ │ ├── 102.png │ │ │ ├── 1024.png │ │ │ ├── 108.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 16.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 20.png │ │ │ ├── 216.png │ │ │ ├── 234.png │ │ │ ├── 256.png │ │ │ ├── 258.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 48.png │ │ │ ├── 50.png │ │ │ ├── 512.png │ │ │ ├── 55.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 64.png │ │ │ ├── 66.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 88.png │ │ │ ├── 92.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 ├── pubspec.lock ├── pubspec.yaml ├── test ├── setting_viewmodel_test.dart ├── setting_viewmodel_test.mocks.dart └── widget_test.dart └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "beta" 3 | } -------------------------------------------------------------------------------- /.github/art/0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/.github/art/0.gif -------------------------------------------------------------------------------- /.github/art/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/.github/art/1.gif -------------------------------------------------------------------------------- /.github/art/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/.github/art/screenshot.png -------------------------------------------------------------------------------- /.github/art/screenshot_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/.github/art/screenshot_0.png -------------------------------------------------------------------------------- /.github/art/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/.github/art/screenshot_1.png -------------------------------------------------------------------------------- /.github/art/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/.github/art/screenshot_2.png -------------------------------------------------------------------------------- /.github/art/theme_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/.github/art/theme_dark.png -------------------------------------------------------------------------------- /.github/art/theme_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/.github/art/theme_light.png -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: Test Coverage Report 2 | on: [ pull_request ] 3 | 4 | jobs: 5 | test_coverage: 6 | runs-on: ubuntu-latest 7 | # runs-on: self-hosted 8 | 9 | permissions: 10 | contents: read 11 | pull-requests: write 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Flutter 17 | uses: subosito/flutter-action@v2 18 | with: 19 | channel: stable 20 | 21 | - name: Setup LCOV 22 | uses: hrishikesh-kadam/setup-lcov@v1 23 | 24 | - name: Get Flutter dependencies 25 | run: | 26 | flutter pub get 27 | flutter pub add --dev test_cov_console 28 | 29 | - name: Run build_runner 30 | run: dart run build_runner build --delete-conflicting-outputs --verbose 31 | 32 | - name: Run tests with coverage 33 | run: flutter test --coverage 34 | 35 | - name: Filter ViewModel files 36 | run: | 37 | lcov --extract coverage/lcov.info "lib/**/*_viewmodel.dart" -o coverage/filtered_lcov.info 38 | 39 | - name: Report code coverage 40 | uses: zgosalvez/github-actions-report-lcov@v4 41 | with: 42 | coverage-files: coverage/filtered_lcov.info 43 | # minimum-coverage: 90 44 | artifact-name: code-coverage-report 45 | github-token: ${{ secrets.GITHUB_TOKEN }} 46 | update-comment: true 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # VS Code 22 | # The .vscode folder contains launch configuration and tasks you configure in 23 | # VS Code which you may wish to be included in version control, so this line 24 | # is commented out by default. 25 | #.vscode/ 26 | 27 | # Flutter/Dart/Pub related 28 | **/doc/api/ 29 | **/ios/Flutter/.last_build_id 30 | .dart_tool/ 31 | .flutter-plugins 32 | .flutter-plugins-dependencies 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | 48 | # FVM Version Cache 49 | .fvm/ 50 | 51 | # Code generation related files 52 | *.g.dart 53 | *.freezed.dart 54 | 55 | # Native platform specific auto-generated plugin files 56 | # Linux 57 | linux/flutter/generated_plugin_registrant.cc 58 | linux/flutter/generated_plugin_registrant.h 59 | linux/generated_plugins.cmake 60 | 61 | # macOS 62 | macos/Flutter/GeneratedPluginRegistrant.swift 63 | 64 | # Windows 65 | windows/flutter/generated_plugin_registrant.cc 66 | windows/flutter/generated_plugin_registrant.h 67 | windows/generated_plugins.cmake 68 | 69 | # iOS Flutter auto-generated 70 | ios/Flutter/Flutter.framework 71 | ios/Flutter/App.framework 72 | ios/Flutter/Generated.xcconfig 73 | ios/Flutter/flutter_export_environment.sh 74 | ios/Podfile.lock 75 | ios/Pods/ 76 | 77 | # Android 78 | android/.gradle 79 | android/app/build/ 80 | android/local.properties 81 | 82 | # OS-specific files 83 | .DS_Store 84 | .localized 85 | Thumbs.db 86 | desktop.ini 87 | 88 | # Temporary files 89 | *.tmp 90 | 91 | /coverage/html/* 92 | /android/app/.cxx/* -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 17 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 18 | - platform: android 19 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 20 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 21 | - platform: ios 22 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 23 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 24 | - platform: linux 25 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 26 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 27 | - platform: macos 28 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 29 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 30 | - platform: web 31 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 32 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 33 | - platform: windows 34 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 35 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /.run/build_apk.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | -------------------------------------------------------------------------------- /.run/build_macos.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | -------------------------------------------------------------------------------- /.run/build_runner.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.run/dev.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /.run/prod.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Luke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

OllamaTalk

4 |

5 | 6 | OllamaTalk is a fully **local, cross-platform AI chat application** that runs seamlessly on macOS, 7 | Windows, Linux, Android, and iOS. All AI processing happens entirely **on your device**, ensuring a 8 | secure and private chat experience without relying on external servers or cloud services. This 9 | design guarantees complete control over your data while delivering a unified experience across all 10 | major platforms. 11 | 12 |

13 | 14 | 15 |

16 | 17 |

18 | 19 |

20 | 21 | ## Cross-Platform Support 22 | 23 | - macOS 24 | - Windows 25 | - Linux 26 | - Web 27 | - Android 28 | - iOS 29 | 30 | ## Installation 31 | 32 | ### Step 1: Install Ollama Server 33 | 34 | - Download and install Ollama from the [official download page](https://ollama.com/download). 35 | 36 | ### Step 2: Download AI Models 37 | 38 | - Browse and download your preferred models from 39 | the [Ollama Model Hub](https://ollama.com/search). 40 | Examples: deepseek-r1, llama, mistral, qwen, gemma2, llava, and more. 41 | 42 | ### Step 3: Start Ollama Server 43 | 44 | #### Default Local Setup 45 | 46 | ```bash 47 | ollama serve # Defaults to http://localhost:11434 48 | ``` 49 | 50 | #### Cross-Device Access Setup 51 | 52 | ```bash 53 | OLLAMA_HOST=0.0.0.0:11434 ollama serve # Enables access from mobile devices 54 | ``` 55 | 56 | **Mobile Device Configuration**: 57 | 58 | - When using OllamaTalk on mobile devices (Android/iOS): 59 | 1. Use the cross-device access configuration on your server 60 | 2. In the OllamaTalk mobile app settings: 61 | 62 | - Navigate to settings 63 | - Enter server IP as: `http://:11434` 64 | - Replace `` with your server's local network IP 65 | 66 | **Network Requirements**: 67 | 68 | - Server and mobile device must be on the same local network 69 | 70 | ### Step 4: Run the Application 71 | 72 | 1. Visit the [OllamaTalk Releases](https://github.com/shinhyo/OllamaTalk/releases) page. 73 | 2. Download the latest version for your platform 74 | 75 | ### Step 5: Launch OllamaTalk 76 | 77 | 1. Open the installed application. 78 | 2. Connect to your local Ollama server. 79 | 3. Start chatting with AI. 80 | 81 | > **Note:** Ensure that the Ollama server is running before launching the application. 82 | 83 | ## License 84 | 85 | This project is licensed under the MIT License. 86 | 87 | ## Support 88 | 89 | - Report bugs and suggest features through GitHub Issues. 90 | - Contributions via Pull Requests are welcome! -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - "**/*.g.dart" 6 | - "**/*.freezed.dart" 7 | errors: 8 | invalid_annotation_target: ignore 9 | plugins: 10 | - custom_lint 11 | linter: 12 | rules: 13 | - avoid_unnecessary_containers 14 | - prefer_const_constructors 15 | - prefer_const_declarations 16 | - prefer_const_literals_to_create_immutables 17 | - sized_box_for_whitespace 18 | - avoid_redundant_argument_values 19 | 20 | - prefer_final_fields 21 | - prefer_final_locals 22 | - prefer_relative_imports 23 | - directives_ordering 24 | - require_trailing_commas 25 | - use_key_in_widget_constructors 26 | - use_full_hex_values_for_flutter_colors 27 | - await_only_futures 28 | - unawaited_futures 29 | - sort_child_properties_last 30 | - unnecessary_parenthesis 31 | - unnecessary_string_interpolations 32 | - unnecessary_this 33 | - curly_braces_in_flow_control_structures 34 | - avoid_print 35 | - 36 | -------------------------------------------------------------------------------- /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 = "io.github.shinhyo.ollama.talk" 10 | // compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | compileSdk 35 13 | ndkVersion "27.2.12479018" 14 | 15 | compileOptions { 16 | sourceCompatibility = JavaVersion.VERSION_17 17 | targetCompatibility = JavaVersion.VERSION_17 18 | } 19 | 20 | kotlinOptions { 21 | jvmTarget = JavaVersion.VERSION_17 22 | } 23 | 24 | defaultConfig { 25 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 26 | applicationId = "io.github.shinhyo.ollama.talk" 27 | // You can update the following values to match your application needs. 28 | // For more information, see: https://flutter.dev/to/review-gradle-config. 29 | minSdk = flutter.minSdkVersion 30 | targetSdk = flutter.targetSdkVersion 31 | versionCode = flutter.versionCode 32 | versionName = flutter.versionName 33 | } 34 | 35 | signingConfigs { 36 | debug { 37 | storeFile rootProject.file('debug.keystore') 38 | keyAlias 'androiddebugkey' 39 | keyPassword 'android' 40 | storePassword 'android' 41 | } 42 | } 43 | 44 | buildTypes { 45 | debug { 46 | signingConfig = signingConfigs.debug 47 | } 48 | release { 49 | // TODO: Add your own signing config for the release build. 50 | // Signing with the debug keys for now, so `flutter run --release` works. 51 | signingConfig = signingConfigs.debug 52 | } 53 | } 54 | } 55 | 56 | flutter { 57 | source = "../.." 58 | } 59 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 17 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/io/github/shinhyo/ollama/talk/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.shinhyo.ollama.talk 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/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/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/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/android/debug.keystore -------------------------------------------------------------------------------- /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.10.2-bin.zip 6 | -------------------------------------------------------------------------------- /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.3" apply false 22 | id "org.jetbrains.kotlin.android" version "2.0.20" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /assets/icons/chat-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/database-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/dots-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/forum-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/head-snowflake-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/keyboard-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/menu-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/menu-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/message-text-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/ollama.svg: -------------------------------------------------------------------------------- 1 | 3 | Ollama 4 | 6 | -------------------------------------------------------------------------------- /assets/icons/robot-outline-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icons/robot-outline-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icons/robot-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/icons/tune.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /assets/images/app_icon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/assets/images/app_icon_dark.png -------------------------------------------------------------------------------- /assets/images/app_icon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/assets/images/app_icon_light.png -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | drift_dev: 5 | options: 6 | store_date_time_values_as_text: true # ISO-8601 7 | -------------------------------------------------------------------------------- /coverage/run_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # coverage/run_coverage.sh 3 | 4 | cd "$(dirname "$0")/.." || exit 5 | 6 | # Run flutter test with coverage 7 | flutter test --coverage 8 | 9 | # Filter only viewmodel files 10 | lcov --extract coverage/lcov.info "lib/**/*_viewmodel.dart" -o coverage/filtered_lcov.info 11 | 12 | # Generate HTML report 13 | genhtml coverage/filtered_lcov.info -o coverage/html 14 | -------------------------------------------------------------------------------- /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 | - drift: true -------------------------------------------------------------------------------- /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 "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /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/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "3fc40d5c0c204302bc754915a56338ad37462a757e370001fe7fbb56acbd8324", 3 | "pins" : [ 4 | { 5 | "identity" : "csqlite", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/simolus3/CSQLite.git", 8 | "state" : { 9 | "revision" : "f13dc216059e85d60beed2bd9900f74be241e14d" 10 | } 11 | } 12 | ], 13 | "version" : 3 14 | } 15 | -------------------------------------------------------------------------------- /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.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "3fc40d5c0c204302bc754915a56338ad37462a757e370001fe7fbb56acbd8324", 3 | "pins" : [ 4 | { 5 | "identity" : "csqlite", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/simolus3/CSQLite.git", 8 | "state" : { 9 | "revision" : "f13dc216059e85d60beed2bd9900f74be241e14d" 10 | } 11 | } 12 | ], 13 | "version" : 3 14 | } 15 | -------------------------------------------------------------------------------- /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/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/108.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/234.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/234.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/258.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/258.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png -------------------------------------------------------------------------------- /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/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/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 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | OllamaTalk 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | OllamaTalk 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | LSApplicationQueriesSchemes 49 | 50 | https 51 | http 52 | 53 | NSAppTransportSecurity 54 | 55 | NSAllowsArbitraryLoads 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/config/build_config.dart: -------------------------------------------------------------------------------- 1 | enum BuildType { dev, preview, prod } 2 | 3 | class BuildConfig { 4 | static const mode = String.fromEnvironment( 5 | 'BUILD_MODE', 6 | defaultValue: 'dev', 7 | ); 8 | 9 | static bool get isDev => mode == 'dev'; 10 | 11 | static bool get isPreview => mode == 'preview'; 12 | 13 | static bool get isProd => mode == 'prod'; 14 | 15 | static BuildType get buildType { 16 | switch (mode) { 17 | case 'prod': 18 | return BuildType.prod; 19 | case 'preview': 20 | return BuildType.preview; 21 | default: 22 | return BuildType.dev; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/data/config/network_config_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | import '../../domain/config/network_config.dart'; 4 | 5 | class NetworkConfigImpl implements NetworkConfig { 6 | final Dio _dio; 7 | 8 | NetworkConfigImpl(this._dio); 9 | 10 | @override 11 | void updateBaseUrl(String url) { 12 | _dio.options.baseUrl = url; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/data/repository/device_info_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:package_info_plus/package_info_plus.dart'; 2 | 3 | import '../../domain/repository/device_info_repository.dart'; 4 | 5 | class DeviceInfoRepositoryImpl implements DeviceInfoRepository { 6 | @override 7 | Future getAppVersion() async { 8 | final packageInfo = await PackageInfo.fromPlatform(); 9 | return packageInfo.version; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/data/repository/ollama_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | import '../../domain/models/ollama_chat_entity.dart'; 6 | import '../../domain/models/ollama_entity.dart'; 7 | import '../../domain/models/ollama_generate_entity.dart'; 8 | import '../../domain/repository/ollama_repository.dart'; 9 | import '../utils/stream_parser.dart'; 10 | 11 | class OllamaRepositoryImpl extends OllamaRepository { 12 | final Dio _dio; 13 | CancelToken? _cancelToken; 14 | 15 | OllamaRepositoryImpl(this._dio); 16 | 17 | @override 18 | Future> getModels() async { 19 | try { 20 | final response = await _dio.get( 21 | '/api/tags', 22 | ); 23 | final List data = response.data['models'] as List; 24 | return data 25 | .map( 26 | (json) => OllamaModelEntity.fromJson(json as Map), 27 | ) 28 | .toList(); 29 | } catch (e) { 30 | throw Exception('Failed to fetch models: $e'); 31 | } 32 | } 33 | 34 | @override 35 | void cancelCurrentChat() { 36 | if (_cancelToken == null) return; 37 | _cancelToken?.cancel('User cancelled the chat'); 38 | _cancelToken = null; 39 | } 40 | 41 | @override 42 | Stream generateResponse({ 43 | required String model, 44 | required String prompt, 45 | }) async* { 46 | try { 47 | _cancelToken = CancelToken(); 48 | final response = await _dio.post( 49 | '/api/generate', 50 | data: { 51 | 'model': model, 52 | 'prompt': prompt, 53 | 'stream': true, 54 | }, 55 | options: Options(responseType: ResponseType.stream), 56 | cancelToken: _cancelToken, 57 | ); 58 | 59 | yield* parseJsonStream( 60 | response.data!.stream.cast>(), 61 | OllamaGenerateEntity.fromJson, 62 | ); 63 | } catch (e) { 64 | yield* Stream.error(e); 65 | } finally { 66 | _cancelToken = null; 67 | } 68 | } 69 | 70 | @override 71 | Stream chat({ 72 | required String model, 73 | required List messages, 74 | }) async* { 75 | try { 76 | _cancelToken = CancelToken(); 77 | final response = await _dio.post( 78 | '/api/chat', 79 | data: { 80 | 'model': model, 81 | 'messages': messages.map((msg) => msg.toJson()).toList(), 82 | 'stream': true, 83 | }, 84 | options: Options(responseType: ResponseType.stream), 85 | cancelToken: _cancelToken, 86 | ); 87 | 88 | yield* parseJsonStream( 89 | response.data!.stream.cast>(), 90 | OllamaChatEntity.fromJson, 91 | ); 92 | } catch (e) { 93 | yield* Stream.error(e); 94 | } finally { 95 | _cancelToken = null; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/data/source/local/database/chat_room_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | 3 | import '../../../../utils/logger.dart'; 4 | import '../drift.dart'; 5 | import 'table/chat_room_table.dart'; 6 | import 'table/message_table.dart'; 7 | 8 | part 'chat_room_dao.g.dart'; 9 | 10 | @DriftAccessor(tables: [ChatRoomTable, MessageTable]) 11 | class ChatRoomDao extends DatabaseAccessor 12 | with _$ChatRoomDaoMixin { 13 | ChatRoomDao(super.db); 14 | 15 | Future createRoom(ChatRoomTableCompanion room) { 16 | logInfo('createRoom: $room'); 17 | return into(chatRoomTable).insert(room); 18 | } 19 | 20 | Stream watchRoom(int roomId) { 21 | return (select(chatRoomTable)..where((tbl) => tbl.id.equals(roomId))) 22 | .watchSingleOrNull(); 23 | } 24 | 25 | Stream> watchAllRooms() { 26 | return (select(chatRoomTable) 27 | ..orderBy([ 28 | (tbl) => OrderingTerm( 29 | expression: tbl.updatedAt, 30 | mode: OrderingMode.desc, 31 | ), 32 | ])) 33 | .watch(); 34 | } 35 | 36 | Stream> watchChatRoomsWithLastMessage() { 37 | final chatRooms = alias(chatRoomTable, 'cr'); 38 | final messages = alias(messageTable, 'm'); 39 | 40 | final query = select(chatRooms).join([ 41 | leftOuterJoin( 42 | messages, 43 | messages.roomId.equalsExp(chatRooms.id), 44 | ), 45 | ]) 46 | ..addColumns([messages.id, messages.content, messages.createdAt]) 47 | ..where(messages.id.isNotNull()) 48 | ..orderBy([ 49 | OrderingTerm(expression: messages.createdAt, mode: OrderingMode.desc), 50 | ]); 51 | 52 | return query.watch().map((rows) { 53 | final grouped = {}; 54 | 55 | for (final row in rows) { 56 | final chatRoom = row.readTable(chatRooms); 57 | final message = row.readTableOrNull(messages); 58 | 59 | if (!grouped.containsKey(chatRoom.id)) { 60 | grouped[chatRoom.id] = (chatRoom, message); 61 | } 62 | } 63 | 64 | return grouped.values.toList(); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/data/source/local/database/message_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | 3 | import '../drift.dart'; 4 | import 'table/chat_room_table.dart'; 5 | import 'table/message_table.dart'; 6 | 7 | part 'message_dao.g.dart'; 8 | 9 | @DriftAccessor(tables: [MessageTable, ChatRoomTable]) 10 | class MessageDao extends DatabaseAccessor with _$MessageDaoMixin { 11 | MessageDao(super.db); 12 | 13 | Future> getRecentMessages(int roomId, {int limit = 10}) { 14 | return (select(messageTable) 15 | ..where((tbl) => tbl.roomId.equals(roomId)) 16 | ..orderBy([ 17 | (tbl) => OrderingTerm( 18 | expression: tbl.id, 19 | mode: OrderingMode.desc, 20 | ), 21 | ]) 22 | ..limit(limit)) 23 | .get(); 24 | } 25 | 26 | Future insertMessage(MessageTableCompanion message) { 27 | return into(messageTable).insert(message); 28 | } 29 | 30 | Stream> watchMessagesInRoom(int roomId) { 31 | return (select(messageTable) 32 | ..where((tbl) => tbl.roomId.equals(roomId)) 33 | ..orderBy([ 34 | (tbl) => OrderingTerm( 35 | expression: tbl.createdAt, 36 | ), 37 | ])) 38 | .watch(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/data/source/local/database/table/auto_incrementing_primary_key.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | 3 | mixin AutoIncrementingPrimaryKey on Table { 4 | IntColumn get id => integer().autoIncrement()(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/data/source/local/database/table/chat_room_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | 3 | import '../../../../../domain/models/chat_room_entity.dart'; 4 | import '../../drift.dart'; 5 | import 'auto_incrementing_primary_key.dart'; 6 | 7 | @DataClassName('ChatRoom') 8 | class ChatRoomTable extends Table with AutoIncrementingPrimaryKey { 9 | TextColumn get name => text()(); 10 | 11 | TextColumn get chatMode => text()(); 12 | 13 | TextColumn get prompt => text()(); 14 | 15 | DateTimeColumn get createdAt => 16 | dateTime().named('created_at').withDefault(currentDateAndTime)(); 17 | 18 | DateTimeColumn get updatedAt => 19 | dateTime().named('updated_at').withDefault(currentDateAndTime)(); 20 | } 21 | 22 | extension ChatRoomDataMapper on ChatRoom { 23 | ChatRoomEntity toDomain() { 24 | return ChatRoomEntity( 25 | id: id, 26 | name: name, 27 | prompt: prompt, 28 | chatMode: chatMode, 29 | createdAt: createdAt, 30 | updatedAt: updatedAt, 31 | ); 32 | } 33 | } 34 | 35 | extension ChatRoomDomainMapper on ChatRoomEntity { 36 | ChatRoomTableCompanion toData() { 37 | return ChatRoomTableCompanion.insert( 38 | name: name, 39 | chatMode: chatMode, 40 | prompt: prompt, 41 | createdAt: Value(createdAt), 42 | updatedAt: Value(updatedAt), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/data/source/local/database/table/message_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | 3 | import '../../../../../domain/models/message_entity.dart'; 4 | import '../../../../../domain/models/role.dart'; 5 | import '../../drift.dart'; 6 | import 'auto_incrementing_primary_key.dart'; 7 | import 'chat_room_table.dart'; 8 | 9 | @DataClassName('Message') 10 | class MessageTable extends Table with AutoIncrementingPrimaryKey { 11 | IntColumn get roomId => integer().named('room_id').references( 12 | ChatRoomTable, 13 | #id, 14 | onDelete: KeyAction.cascade, 15 | onUpdate: KeyAction.cascade, 16 | )(); 17 | 18 | TextColumn get role => textEnum()(); 19 | 20 | TextColumn get content => text().nullable()(); 21 | 22 | TextColumn get model => text().nullable()(); 23 | 24 | IntColumn get totalDuration => integer().nullable()(); 25 | 26 | IntColumn get loadDuration => integer().nullable()(); 27 | 28 | IntColumn get promptEvalCount => integer().nullable()(); 29 | 30 | IntColumn get promptEvalDuration => integer().nullable()(); 31 | 32 | IntColumn get evalCount => integer().nullable()(); 33 | 34 | IntColumn get evalDuration => integer().nullable()(); 35 | 36 | DateTimeColumn get createdAt => 37 | dateTime().named('created_at').withDefault(currentDateAndTime)(); 38 | 39 | DateTimeColumn get updatedAt => 40 | dateTime().named('updated_at').withDefault(currentDateAndTime)(); 41 | } 42 | 43 | extension MessageDataMapper on Message { 44 | MessageEntity toDomain() { 45 | return MessageEntity( 46 | id: id, 47 | roomId: roomId, 48 | role: role, 49 | content: content ?? '', 50 | model: model, 51 | totalDuration: totalDuration, 52 | loadDuration: loadDuration, 53 | promptEvalCount: promptEvalCount, 54 | promptEvalDuration: promptEvalDuration, 55 | evalCount: evalCount, 56 | evalDuration: evalDuration, 57 | createdAt: createdAt, 58 | updatedAt: updatedAt, 59 | ); 60 | } 61 | } 62 | 63 | extension MessageDomainMapper on MessageEntity { 64 | MessageTableCompanion toData() { 65 | return MessageTableCompanion.insert( 66 | roomId: roomId, 67 | role: role, 68 | content: Value(content), 69 | model: Value(model), 70 | totalDuration: Value(totalDuration), 71 | loadDuration: Value(loadDuration), 72 | promptEvalCount: Value(promptEvalCount), 73 | promptEvalDuration: Value(promptEvalDuration), 74 | evalCount: Value(evalCount), 75 | evalDuration: Value(evalDuration), 76 | createdAt: Value(createdAt), 77 | updatedAt: Value(updatedAt), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/data/source/local/drift.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | import 'package:drift_flutter/drift_flutter.dart'; 3 | 4 | import '../../../domain/models/role.dart'; 5 | import '../../../utils/logger.dart'; 6 | import 'database/chat_room_dao.dart'; 7 | import 'database/message_dao.dart'; 8 | import 'database/table/chat_room_table.dart'; 9 | import 'database/table/message_table.dart'; 10 | 11 | part 'drift.g.dart'; 12 | 13 | @DriftDatabase( 14 | tables: [ 15 | ChatRoomTable, 16 | MessageTable, 17 | ], 18 | daos: [ 19 | ChatRoomDao, 20 | MessageDao, 21 | ], 22 | ) 23 | class AppDatabase extends _$AppDatabase { 24 | @override 25 | int get schemaVersion => 1; 26 | 27 | AppDatabase() 28 | : super( 29 | driftDatabase( 30 | name: 'talk', 31 | web: DriftWebOptions( 32 | sqlite3Wasm: Uri.parse('sqlite3.wasm'), 33 | driftWorker: Uri.parse('drift_worker.js'), 34 | onResult: (result) { 35 | if (result.missingFeatures.isNotEmpty) { 36 | logDebug( 37 | 'Using ${result.chosenImplementation} due to unsupported ' 38 | 'browser features: ${result.missingFeatures}'); 39 | } 40 | }, 41 | ), 42 | ), 43 | ); 44 | 45 | @override 46 | MigrationStrategy get migration => MigrationStrategy( 47 | beforeOpen: (details) async { 48 | logInfo('Database beforeOpen wasCreated: ${details.wasCreated}'); 49 | await customStatement('PRAGMA foreign_keys = ON'); 50 | }, 51 | onUpgrade: (Migrator m, int from, int to) async { 52 | logInfo('Database onUpgrade $from -> $to'); 53 | }, 54 | ); 55 | 56 | AppDatabase.forTesting(DatabaseConnection super.connection); 57 | 58 | Future deleteAllData() { 59 | return transaction(() async { 60 | for (final table in allTables) { 61 | await delete(table).go(); 62 | } 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/data/source/network/dio.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:talker_dio_logger/talker_dio_logger.dart'; 4 | 5 | import '../../../domain/repository/preferences_repository.dart'; 6 | 7 | final Dio dio = Dio( 8 | BaseOptions(), 9 | )..interceptors.addAll([ 10 | if (kDebugMode) 11 | TalkerDioLogger( 12 | settings: const TalkerDioLoggerSettings( 13 | printRequestHeaders: true, 14 | printResponseHeaders: true, 15 | ), 16 | ), 17 | ]); 18 | 19 | class ApiClient { 20 | static Dio createDio(PreferencesRepository prefs) { 21 | final String baseUrl = prefs.get(PreferenceKeys.apiHost)!; 22 | final baseOptions = BaseOptions( 23 | baseUrl: baseUrl, 24 | connectTimeout: const Duration(seconds: 15), 25 | receiveTimeout: const Duration(seconds: 15), 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | }, 29 | validateStatus: (status) => true, 30 | ); 31 | return Dio(baseOptions) 32 | ..interceptors.addAll([ 33 | if (kDebugMode) 34 | TalkerDioLogger( 35 | settings: const TalkerDioLoggerSettings( 36 | printRequestHeaders: true, 37 | printResponseHeaders: true, 38 | ), 39 | ), 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/data/utils/stream_parser.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../../utils/logger.dart'; 4 | 5 | Stream parseJsonStream( 6 | Stream> inputStream, 7 | T Function(Map) fromJson, 8 | ) async* { 9 | final buffer = StringBuffer(); 10 | const decoder = Utf8Decoder(); 11 | bool isJsonStarted = false; 12 | 13 | await for (final chunk in inputStream) { 14 | final decodedChunk = decoder.convert(chunk); 15 | buffer.write(decodedChunk); 16 | 17 | if (!isJsonStarted && buffer.toString().trim().startsWith('{')) { 18 | isJsonStarted = true; 19 | } 20 | 21 | if (isJsonStarted && buffer.toString().trim().endsWith('}')) { 22 | try { 23 | final jsonMap = jsonDecode(buffer.toString()) as Map; 24 | yield fromJson(jsonMap); 25 | buffer.clear(); 26 | isJsonStarted = false; 27 | } catch (e, stackTrace) { 28 | logError('Error parsing JSON chunk: ${buffer.toString()}', stackTrace); 29 | continue; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/domain/config/network_config.dart: -------------------------------------------------------------------------------- 1 | abstract class NetworkConfig { 2 | void updateBaseUrl(String url); 3 | } 4 | -------------------------------------------------------------------------------- /lib/domain/models/chat_mode.dart: -------------------------------------------------------------------------------- 1 | enum ChatMode { 2 | general('General Assistant', { 3 | 'temperature': 0.7, 4 | 'top_k': 50, 5 | 'top_p': 0.9, 6 | 'repeat_penalty': 1.1, 7 | 'max_tokens': 2000, 8 | }), 9 | coding('Code Expert', { 10 | 'temperature': 0.2, 11 | 'top_k': 40, 12 | 'top_p': 0.95, 13 | 'repeat_penalty': 1.2, 14 | 'max_tokens': 4000, 15 | 'frequency_penalty': 1.1, 16 | }), 17 | creative('Creative Writer', { 18 | 'temperature': 0.9, 19 | 'top_k': 60, 20 | 'top_p': 0.99, 21 | 'repeat_penalty': 1.0, 22 | 'max_tokens': 3000, 23 | 'frequency_penalty': 0.7, 24 | }), 25 | technical('Technical Writer', { 26 | 'temperature': 0.4, 27 | 'top_k': 45, 28 | 'top_p': 0.85, 29 | 'repeat_penalty': 1.15, 30 | 'max_tokens': 2500, 31 | }), 32 | ; 33 | 34 | final String label; 35 | final Map options; 36 | 37 | const ChatMode(this.label, this.options); 38 | 39 | ChatMode? findByLabel(String label) { 40 | try { 41 | return ChatMode.values.firstWhere((element) => element.label == label); 42 | } catch (e) { 43 | return null; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/domain/models/chat_room_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'chat_room_entity.freezed.dart'; 4 | 5 | part 'chat_room_entity.g.dart'; 6 | 7 | @freezed 8 | class ChatRoomEntity with _$ChatRoomEntity { 9 | const factory ChatRoomEntity({ 10 | @Default(null) int? id, 11 | required String name, 12 | required String prompt, 13 | required String chatMode, 14 | required DateTime createdAt, 15 | required DateTime updatedAt, 16 | }) = _ChatRoomEntity; 17 | 18 | factory ChatRoomEntity.fromJson(Map json) => 19 | _$ChatRoomEntityFromJson(json); 20 | } 21 | -------------------------------------------------------------------------------- /lib/domain/models/message_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'role.dart'; 4 | 5 | part 'message_entity.freezed.dart'; 6 | 7 | part 'message_entity.g.dart'; 8 | 9 | @freezed 10 | class MessageEntity with _$MessageEntity { 11 | const factory MessageEntity({ 12 | @Default(null) int? id, 13 | required int roomId, 14 | required Role role, 15 | required String content, 16 | String? model, 17 | int? totalDuration, 18 | int? loadDuration, 19 | int? promptEvalCount, 20 | int? promptEvalDuration, 21 | int? evalCount, 22 | int? evalDuration, 23 | required DateTime createdAt, 24 | required DateTime updatedAt, 25 | }) = _MessageEntity; 26 | 27 | factory MessageEntity.fromJson(Map json) => 28 | _$MessageEntityFromJson(json); 29 | } 30 | -------------------------------------------------------------------------------- /lib/domain/models/ollama_chat_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'role.dart'; 4 | 5 | part 'ollama_chat_entity.freezed.dart'; 6 | 7 | part 'ollama_chat_entity.g.dart'; 8 | 9 | @freezed 10 | class OllamaChatEntity with _$OllamaChatEntity { 11 | const factory OllamaChatEntity({ 12 | required String model, 13 | @JsonKey(name: 'created_at') required DateTime createdAt, 14 | required OllamaMessageEntity message, 15 | required bool done, 16 | @JsonKey(name: 'done_reason') String? doneReason, 17 | List? context, 18 | @JsonKey(name: 'total_duration') int? totalDuration, 19 | @JsonKey(name: 'load_duration') int? loadDuration, 20 | @JsonKey(name: 'prompt_eval_count') int? promptEvalCount, 21 | @JsonKey(name: 'prompt_eval_duration') int? promptEvalDuration, 22 | @JsonKey(name: 'eval_count') int? evalCount, 23 | @JsonKey(name: 'eval_duration') int? evalDuration, 24 | }) = _OllamaChatEntity; 25 | 26 | factory OllamaChatEntity.fromJson(Map json) => 27 | _$OllamaChatEntityFromJson(json); 28 | } 29 | 30 | @freezed 31 | class OllamaMessageEntity with _$OllamaMessageEntity { 32 | const factory OllamaMessageEntity({ 33 | required Role role, 34 | @Default('') String content, 35 | }) = _OllamaMessageEntity; 36 | 37 | factory OllamaMessageEntity.fromJson(Map json) => 38 | _$OllamaMessageEntityFromJson(json); 39 | } 40 | -------------------------------------------------------------------------------- /lib/domain/models/ollama_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'ollama_entity.freezed.dart'; 4 | 5 | part 'ollama_entity.g.dart'; 6 | 7 | @freezed 8 | class OllamaModelEntity with _$OllamaModelEntity { 9 | const factory OllamaModelEntity({ 10 | required String name, 11 | required String model, 12 | @JsonKey(name: 'modified_at') required DateTime modifiedAt, 13 | required int size, 14 | required String digest, 15 | required OllamaDetailsModelEntity details, 16 | }) = _OllamaModelEntity; 17 | 18 | factory OllamaModelEntity.fromJson(Map json) => 19 | _$OllamaModelEntityFromJson(json); 20 | } 21 | 22 | @freezed 23 | class OllamaDetailsModelEntity with _$OllamaDetailsModelEntity { 24 | const factory OllamaDetailsModelEntity({ 25 | @JsonKey(name: 'parent_model') required String parentModel, 26 | required String format, 27 | required String family, 28 | required List families, 29 | @JsonKey(name: 'parameter_size') required String parameterSize, 30 | @JsonKey(name: 'quantization_level') required String quantizationLevel, 31 | }) = _OllamaDetailsModelEntity; 32 | 33 | factory OllamaDetailsModelEntity.fromJson(Map json) => 34 | _$OllamaDetailsModelEntityFromJson(json); 35 | } 36 | -------------------------------------------------------------------------------- /lib/domain/models/ollama_generate_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'ollama_generate_entity.freezed.dart'; 4 | 5 | part 'ollama_generate_entity.g.dart'; 6 | 7 | @freezed 8 | class OllamaGenerateEntity with _$OllamaGenerateEntity { 9 | const factory OllamaGenerateEntity({ 10 | required String model, 11 | @JsonKey(name: 'created_at') required DateTime createdAt, 12 | required String response, 13 | required bool done, 14 | @JsonKey(name: 'done_reason') String? doneReason, 15 | List? context, 16 | @JsonKey(name: 'total_duration') int? totalDuration, 17 | @JsonKey(name: 'load_duration') int? loadDuration, 18 | @JsonKey(name: 'prompt_eval_count') int? promptEvalCount, 19 | @JsonKey(name: 'prompt_eval_duration') int? promptEvalDuration, 20 | @JsonKey(name: 'eval_count') int? evalCount, 21 | @JsonKey(name: 'eval_duration') int? evalDuration, 22 | }) = _OllamaGenerateEntity; 23 | 24 | factory OllamaGenerateEntity.fromJson(Map json) => 25 | _$OllamaGenerateEntityFromJson(json); 26 | } 27 | -------------------------------------------------------------------------------- /lib/domain/models/role.dart: -------------------------------------------------------------------------------- 1 | enum Role { 2 | system, 3 | user, 4 | assistant, 5 | } 6 | -------------------------------------------------------------------------------- /lib/domain/repository/database_repository.dart: -------------------------------------------------------------------------------- 1 | import '../models/chat_room_entity.dart'; 2 | import '../models/message_entity.dart'; 3 | 4 | abstract class DatabaseRepository { 5 | // chatRoom 6 | Future createChatRoom(ChatRoomEntity room); 7 | 8 | Future getChatRoom(int roomId); 9 | 10 | Stream watchChatRoom(int roomId); 11 | 12 | Future> getAllChatRooms(); 13 | 14 | Stream> watchAllChatRooms(); 15 | 16 | Stream> 17 | watchAllRoomsWithLastMessage(); 18 | 19 | Future updateChatRoom(ChatRoomEntity room); 20 | 21 | Future updateChatRoomName(int roomId, String name); 22 | 23 | Future deleteChatRoom(int roomId); 24 | 25 | // message 26 | Future createMessage(int roomId, MessageEntity message); 27 | 28 | Future> getRecentMessages(int roomId, {int limit = 10}); 29 | 30 | Future> getMessagesInRoom(int roomId); 31 | 32 | Stream> watchMessagesInRoom(int roomId); 33 | 34 | Future deleteMessage(int messageId); 35 | } 36 | -------------------------------------------------------------------------------- /lib/domain/repository/device_info_repository.dart: -------------------------------------------------------------------------------- 1 | abstract class DeviceInfoRepository { 2 | Future getAppVersion(); 3 | } 4 | -------------------------------------------------------------------------------- /lib/domain/repository/ollama_repository.dart: -------------------------------------------------------------------------------- 1 | import '../models/ollama_chat_entity.dart'; 2 | import '../models/ollama_entity.dart'; 3 | import '../models/ollama_generate_entity.dart'; 4 | 5 | abstract class OllamaRepository { 6 | Future> getModels(); 7 | 8 | void cancelCurrentChat(); 9 | 10 | Stream generateResponse({ 11 | required String model, 12 | required String prompt, 13 | }); 14 | 15 | Stream chat({ 16 | required String model, 17 | required List messages, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /lib/domain/repository/preferences_repository.dart: -------------------------------------------------------------------------------- 1 | enum PreferenceKeys { 2 | apiHost(defaultValue: 'http://localhost:11434'), 3 | ollamaModel(defaultValue: null), 4 | chatMode(defaultValue: null), 5 | prompt(defaultValue: 'You are a helpful assistant.'), 6 | themeMode(defaultValue: 'dark'), 7 | ; 8 | 9 | final T? defaultValue; 10 | 11 | const PreferenceKeys({ 12 | required this.defaultValue, 13 | }); 14 | 15 | T? getDefaultValue() => defaultValue; 16 | } 17 | 18 | abstract class PreferencesRepository { 19 | T? get(PreferenceKeys key); 20 | 21 | Future set(PreferenceKeys key, T value); 22 | 23 | Future clear(); 24 | 25 | Stream watch(PreferenceKeys key); 26 | 27 | void dispose(); 28 | } 29 | -------------------------------------------------------------------------------- /lib/domain/use_cases/create_chat_room_name_use_case.dart: -------------------------------------------------------------------------------- 1 | import '../../utils/logger.dart'; 2 | import '../models/ollama_generate_entity.dart'; 3 | import '../repository/ollama_repository.dart'; 4 | 5 | class ChatRoomNameUseCase { 6 | final OllamaRepository _ollamaRepository; 7 | 8 | ChatRoomNameUseCase( 9 | this._ollamaRepository, 10 | ); 11 | 12 | Future execute({ 13 | required int chatRoomId, 14 | required String model, 15 | required List lastMessages, 16 | }) async { 17 | logInfo('Generating chat room name for model: $model'); 18 | 19 | final prompt = ''' 20 | You are a title generator AI. Generate a short title for this conversation. 21 | 22 | CRITICAL RULES: 23 | 1. MUST be UNDER 20 characters (HIGHEST PRIORITY) 24 | 2. Generate title in EXACTLY SAME language as last message 25 | 3. Format rules: 26 | - Single line only 27 | - NO spaces at start/end 28 | - NO special chars 29 | - NO numbers 30 | - NO quotes 31 | - Basic letters only 32 | 33 | CRITICAL: If title > 20 chars, shorten it 34 | Return ONLY the final title. 35 | 36 | Input conversation: 37 | ${lastMessages.join('\n')} 38 | '''; 39 | 40 | try { 41 | String accumulatedResponse = ''; 42 | 43 | await for (final OllamaGenerateEntity generated 44 | in _ollamaRepository.generateResponse( 45 | model: model, 46 | prompt: prompt, 47 | )) { 48 | if (generated.response.isNotEmpty) { 49 | accumulatedResponse += generated.response; 50 | } 51 | } 52 | logInfo('Generated chat room name: ${accumulatedResponse.trim()}'); 53 | return accumulatedResponse.trim(); 54 | } catch (e, stackTrace) { 55 | logError('Failed to generate chat room name', stackTrace); 56 | throw Exception('Failed to generate chat room name: $e'); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/domain/use_cases/create_chat_room_use_cases.dart: -------------------------------------------------------------------------------- 1 | import '../models/chat_room_entity.dart'; 2 | import '../repository/database_repository.dart'; 3 | 4 | class CreateChatRoomUseCase { 5 | final DatabaseRepository _databaseRepository; 6 | 7 | CreateChatRoomUseCase(this._databaseRepository); 8 | 9 | Future execute({ 10 | required String? name, 11 | required String chatMode, 12 | required String prompt, 13 | }) async { 14 | final now = DateTime.now(); 15 | final newChatRoom = ChatRoomEntity( 16 | name: name ?? 'New Chat', 17 | prompt: prompt, 18 | chatMode: chatMode, 19 | createdAt: now, 20 | updatedAt: now, 21 | ); 22 | final id = await _databaseRepository.createChatRoom(newChatRoom); 23 | return newChatRoom.copyWith(id: id); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/domain/use_cases/ollama_generate_use_case.dart: -------------------------------------------------------------------------------- 1 | import '../models/ollama_generate_entity.dart'; 2 | import '../repository/ollama_repository.dart'; 3 | 4 | class OllamaGenerateUseCase { 5 | final OllamaRepository _ollamaRepository; 6 | 7 | OllamaGenerateUseCase( 8 | this._ollamaRepository, 9 | ); 10 | 11 | Stream execute({ 12 | required String model, 13 | required String prompt, 14 | }) async* { 15 | try { 16 | await for (final OllamaGenerateEntity generated 17 | in _ollamaRepository.generateResponse( 18 | model: model, 19 | prompt: prompt, 20 | )) { 21 | if (generated.response.isNotEmpty) { 22 | yield generated.response; 23 | } 24 | } 25 | } catch (e) { 26 | yield* Stream.error(e); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/domain/use_cases/send_chat_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import '../../utils/logger.dart'; 4 | import '../models/message_entity.dart'; 5 | import '../models/ollama_chat_entity.dart'; 6 | import '../models/role.dart'; 7 | import '../repository/database_repository.dart'; 8 | import '../repository/ollama_repository.dart'; 9 | 10 | class OllamaChatUseCase { 11 | final OllamaRepository _ollamaRepository; 12 | final DatabaseRepository _databaseRepository; 13 | 14 | static const int maxMessages = 15; 15 | 16 | OllamaChatUseCase( 17 | this._ollamaRepository, 18 | this._databaseRepository, 19 | ); 20 | 21 | Stream execute({ 22 | required int roomId, 23 | required String model, 24 | required String prompt, 25 | required List messages, 26 | }) async* { 27 | final buffer = StringBuffer(); 28 | 29 | try { 30 | final userMessage = messages[1]; 31 | await _databaseRepository.createMessage(roomId, userMessage); 32 | 33 | final ollamaMessages = _prepareOllamaMessages(messages, prompt); 34 | await for (final OllamaChatEntity chat in _ollamaRepository.chat( 35 | model: model, 36 | messages: ollamaMessages, 37 | )) { 38 | final content = chat.message.content; 39 | buffer.write(content); 40 | yield content; 41 | 42 | if (chat.done) { 43 | await _databaseRepository.createMessage( 44 | roomId, 45 | MessageEntity( 46 | roomId: roomId, 47 | role: Role.assistant, 48 | content: buffer.toString().trim(), 49 | model: model, 50 | totalDuration: chat.totalDuration, 51 | loadDuration: chat.loadDuration, 52 | promptEvalCount: chat.promptEvalCount, 53 | promptEvalDuration: chat.promptEvalDuration, 54 | evalCount: chat.evalCount, 55 | evalDuration: chat.evalDuration, 56 | createdAt: DateTime.now(), 57 | updatedAt: DateTime.now(), 58 | ), 59 | ); 60 | } 61 | } 62 | 63 | logInfo('Chat completion successful'); 64 | } catch (e, stackTrace) { 65 | logError('Chat error: $e', stackTrace); 66 | yield* Stream.error(e); 67 | } finally { 68 | buffer.clear(); 69 | } 70 | } 71 | 72 | List _prepareOllamaMessages( 73 | List messages, 74 | String prompt, 75 | ) { 76 | final startIdx = max(0, messages.length - (maxMessages + 1)); 77 | final endIdx = max(0, messages.length - 1); 78 | 79 | final ollamaMessages = messages.reversed 80 | .map( 81 | (message) => OllamaMessageEntity( 82 | role: message.role, 83 | content: message.content, 84 | ), 85 | ) 86 | .toList() 87 | .sublist(startIdx, endIdx); 88 | 89 | // system prompt 90 | ollamaMessages.insert( 91 | 0, 92 | OllamaMessageEntity(role: Role.system, content: prompt), 93 | ); 94 | return ollamaMessages; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/domain/use_cases/update_host_use_case.dart: -------------------------------------------------------------------------------- 1 | import '../config/network_config.dart'; 2 | import '../repository/preferences_repository.dart'; 3 | 4 | class UpdateHostUseCase { 5 | final PreferencesRepository _preferencesRepository; 6 | final NetworkConfig _networkConfig; 7 | 8 | UpdateHostUseCase(this._preferencesRepository, this._networkConfig); 9 | 10 | void execute(String newHost) { 11 | _preferencesRepository.set(PreferenceKeys.apiHost, newHost); 12 | _networkConfig.updateBaseUrl(newHost); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/use_cases/watch_selected_model_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:rxdart/rxdart.dart'; 4 | 5 | import '../../utils/logger.dart'; 6 | import '../models/ollama_entity.dart'; 7 | import '../repository/ollama_repository.dart'; 8 | import '../repository/preferences_repository.dart'; 9 | 10 | class WatchOllamaModelsUseCase { 11 | final OllamaRepository _ollamaRepository; 12 | final PreferencesRepository _preferencesRepository; 13 | final _key = PreferenceKeys.ollamaModel; 14 | 15 | WatchOllamaModelsUseCase( 16 | this._ollamaRepository, 17 | this._preferencesRepository, 18 | ); 19 | 20 | Stream<({List models, String? selectedModel})> get stream { 21 | return _preferencesRepository 22 | .watch(PreferenceKeys.apiHost) 23 | .switchMap( 24 | (_) { 25 | logDebug('switchMap'); 26 | return Rx.combineLatest2( 27 | Stream.fromFuture(_ollamaRepository.getModels()), 28 | _preferencesRepository.watch(_key), 29 | (List models, String? selectedModel) { 30 | if (selectedModel == null && models.isNotEmpty) { 31 | selectedModel = models.first.name; 32 | _preferencesRepository.set(_key, selectedModel); 33 | } 34 | return (models: models, selectedModel: selectedModel); 35 | }, 36 | ); 37 | }, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/i18n/strings.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "appName": "OllamaTalk", 4 | "ok": "OK", 5 | "cancel": "Cancel", 6 | "delete": "Delete", 7 | "deleteTitle": "Confirm Delete", 8 | "deleteContent": "Are you sure you want to delete?" 9 | }, 10 | "home": { 11 | "know": "What do you want to know?", 12 | "ask": "Ask anything...", 13 | "unavailable": "Ollama service is unavailable. Please go to Settings and verify the service status", 14 | "loading": "Loading...", 15 | "promptTitle": "System Prompt Settings", 16 | "promptHint": "Enter your default prompt here...", 17 | "promptHelper": "This system prompt defines AI assistant's behavior and personality for all conversations", 18 | "backExit": "Press back again to exit the app." 19 | }, 20 | "chat": { 21 | "hintText": "Enter the message." 22 | }, 23 | "history": { 24 | "emptyTitle": "No chat history available." 25 | }, 26 | "settings": { 27 | "invalidUrl": "Invalid URL", 28 | "noModels": "No installed Ollama models. Run \"ollama pull [model]\" to install.", 29 | "connectionFailed": "Connection failed. Please check Ollama server address.", 30 | "screenTitle": "Setting", 31 | "headerOllama": "Ollama", 32 | "headerApp": "App", 33 | "serverUrlLabel": "Server URL", 34 | "serverUrlHint": "Enter Ollama server URL", 35 | "connectionStatusLabel": "Connection Status", 36 | "connectionStatusConnected": "Connected", 37 | "connectionStatusUnavailable": "Unavailable", 38 | "themeLabel": "Theme", 39 | "themeDialogTitle": "Select Theme", 40 | "versionLabel": "Version" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:bot_toast/bot_toast.dart'; 5 | import 'package:device_preview/device_preview.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_bloc/flutter_bloc.dart'; 8 | import 'package:talker_bloc_logger/talker_bloc_logger_observer.dart'; 9 | import 'package:window_manager/window_manager.dart'; 10 | 11 | import 'config/build_config.dart'; 12 | import 'config/dependencies.dart'; 13 | import 'i18n/strings.g.dart'; 14 | import 'main_viewmodel.dart'; 15 | import 'ui/core/themes/theme.dart'; 16 | import 'ui/routing/router.dart'; 17 | import 'utils/logger.dart'; 18 | import 'utils/platform_util.dart'; 19 | 20 | void main() async { 21 | WidgetsFlutterBinding.ensureInitialized(); 22 | 23 | await initDataInject(); 24 | await setupGlobalSettings(); 25 | 26 | runApp( 27 | DevicePreview( 28 | enabled: BuildConfig.isPreview, 29 | builder: (context) => const MainApp(), 30 | ), 31 | ); 32 | } 33 | 34 | Future setupGlobalSettings() async { 35 | await LocaleSettings.useDeviceLocale(); 36 | 37 | Bloc.observer = TalkerBlocObserver( 38 | talker: getIt().talker, 39 | ); 40 | 41 | await _initWindowOption(); 42 | } 43 | 44 | Future _initWindowOption() async { 45 | if (!PlatformUtils.isDesktop) return; 46 | 47 | await windowManager.ensureInitialized(); 48 | unawaited( 49 | windowManager.waitUntilReadyToShow( 50 | WindowOptions( 51 | size: const Size(1024, 768), 52 | minimumSize: const Size(420, 400), 53 | titleBarStyle: Platform.isWindows || Platform.isLinux 54 | ? TitleBarStyle.normal 55 | : TitleBarStyle.hidden, 56 | backgroundColor: Colors.transparent, 57 | windowButtonVisibility: true, 58 | title: t.common.appName, 59 | ), 60 | () async { 61 | await windowManager.show(); 62 | await windowManager.focus(); 63 | }, 64 | ), 65 | ); 66 | } 67 | 68 | class MainApp extends StatelessWidget { 69 | const MainApp({super.key}); 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | return BlocProvider( 74 | create: (context) => getIt(), 75 | child: BlocBuilder( 76 | buildWhen: (previous, current) => 77 | previous.themeMode != current.themeMode, 78 | builder: (context, state) { 79 | return MaterialApp.router( 80 | debugShowCheckedModeBanner: false, 81 | theme: AppTheme.light, 82 | darkTheme: AppTheme.dark, 83 | builder: BotToastInit(), 84 | themeMode: context.read().state.themeMode, 85 | routerConfig: router, 86 | ); 87 | }, 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/main_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:freezed_annotation/freezed_annotation.dart'; 6 | import 'package:rxdart/rxdart.dart'; 7 | 8 | import '../../domain/repository/preferences_repository.dart'; 9 | 10 | part 'main_viewmodel.freezed.dart'; 11 | 12 | @freezed 13 | class UiState with _$UiState { 14 | const factory UiState({ 15 | ThemeMode? themeMode, 16 | }) = _UiState; 17 | } 18 | 19 | class AppViewModel extends Cubit { 20 | final PreferencesRepository _preferencesRepository; 21 | final _compositeSubscription = CompositeSubscription(); 22 | 23 | AppViewModel( 24 | this._preferencesRepository, 25 | ) : super(const UiState()) { 26 | _init(); 27 | } 28 | 29 | _init() { 30 | final savedTheme = _preferencesRepository.get(PreferenceKeys.themeMode); 31 | emit(state.copyWith(themeMode: getThemeMode(savedTheme))); 32 | 33 | _preferencesRepository.watch(PreferenceKeys.themeMode).listen( 34 | (String? event) { 35 | emit(state.copyWith(themeMode: getThemeMode(event))); 36 | }, 37 | ).addTo(_compositeSubscription); 38 | } 39 | 40 | ThemeMode getThemeMode(String? savedTheme) => 41 | ThemeMode.values.byName(savedTheme?.toLowerCase() ?? 'system'); 42 | 43 | @override 44 | Future close() async { 45 | await _compositeSubscription.dispose(); 46 | await super.close(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/ui/chat/widget/chat_input_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../i18n/strings.g.dart'; 5 | import '../../../utils/keyboard_util.dart'; 6 | import '../../../utils/platform_util.dart'; 7 | import '../../core/themes/theme_ext.dart'; 8 | import '../chat_viewmodel.dart'; 9 | 10 | class ChatInputField extends StatelessWidget { 11 | const ChatInputField({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final viewModel = context.read(); 16 | final isMobile = PlatformUtils.isMobile; 17 | 18 | return BlocBuilder( 19 | buildWhen: (previous, current) { 20 | return previous.isGeneratingChat != current.isGeneratingChat; 21 | }, 22 | builder: (context, state) { 23 | return Container( 24 | padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), 25 | color: context.color.surface, 26 | child: _buildMessageInput(viewModel, state, isMobile), 27 | ); 28 | }, 29 | ); 30 | } 31 | 32 | Widget _buildMessageInput( 33 | ChatViewModel viewModel, 34 | ChatUiState state, 35 | bool isMobile, 36 | ) { 37 | return Row( 38 | crossAxisAlignment: CrossAxisAlignment.end, 39 | children: [ 40 | Expanded( 41 | child: KeyboardListener( 42 | focusNode: FocusNode(), 43 | onKeyEvent: (KeyEvent event) { 44 | KeyboardUtils.handleEnterKeyEvent( 45 | isMobile: isMobile, 46 | event: event, 47 | controller: viewModel.messageController, 48 | onSubmit: () => viewModel.sendMessage(), 49 | ); 50 | }, 51 | child: TextField( 52 | controller: viewModel.messageController, 53 | focusNode: viewModel.messageFocusNode, 54 | maxLines: null, 55 | minLines: 1, 56 | textInputAction: 57 | isMobile ? TextInputAction.newline : TextInputAction.none, 58 | keyboardType: TextInputType.multiline, 59 | decoration: InputDecoration( 60 | hintText: t.chat.hintText, 61 | border: InputBorder.none, 62 | enabledBorder: InputBorder.none, 63 | focusedBorder: InputBorder.none, 64 | contentPadding: const EdgeInsets.symmetric( 65 | horizontal: 12, 66 | vertical: 8, 67 | ), 68 | ), 69 | ), 70 | ), 71 | ), 72 | _buildSendButton(viewModel, state), 73 | ], 74 | ); 75 | } 76 | 77 | Widget _buildSendButton(ChatViewModel viewModel, ChatUiState state) { 78 | return IconButton( 79 | icon: state.isGeneratingChat 80 | ? const Icon(Icons.stop) 81 | : const Icon(Icons.send), 82 | onPressed: 83 | state.isGeneratingChat ? viewModel.cancelChat : viewModel.sendMessage, 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/ui/chat/widget/chat_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../chat_viewmodel.dart'; 5 | import 'chat_bubble.dart'; 6 | 7 | class ChatList extends StatelessWidget { 8 | const ChatList({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return BlocSelector messages, bool isGeneratingChat})>( 14 | selector: (ChatUiState state) => 15 | (messages: state.messages, isGeneratingChat: state.isGeneratingChat), 16 | builder: (context, selectedState) { 17 | return ListView.builder( 18 | padding: const EdgeInsets.all(16), 19 | reverse: true, 20 | itemCount: selectedState.messages.length, 21 | itemBuilder: (context, index) { 22 | final Message message = selectedState.messages[index]; 23 | return ChatBubble( 24 | message: message, 25 | isGeneratingChat: selectedState.isGeneratingChat, 26 | isLast: index == 0, 27 | onTabHeader: () {}, 28 | ); 29 | }, 30 | ); 31 | }, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/chat/widget/chat_toolbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../core/themes/theme_ext.dart'; 5 | import '../../tab/home/widget/home_toolbar_chips.dart'; 6 | import '../chat_viewmodel.dart'; 7 | 8 | class ChatToolbar extends StatelessWidget { 9 | const ChatToolbar({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return BlocBuilder( 14 | builder: (context, state) { 15 | if (state.uiError != null) { 16 | return const SizedBox.shrink(); 17 | } 18 | final viewModel = context.read(); 19 | final optionState = state.option; 20 | 21 | final modelList = optionState.modelList; 22 | final chatModeList = optionState.chatModeList; 23 | 24 | final indexSelectModel = optionState.indexSelectModel; 25 | final indexSelectChatMode = optionState.indexSelectChatMode; 26 | 27 | return Container( 28 | padding: const EdgeInsets.symmetric(horizontal: 8), 29 | color: context.color.surface, 30 | width: double.infinity, 31 | child: ChatCommonToolbar( 32 | isMini: true, 33 | isLoading: state.isInitLoading, 34 | models: modelList, 35 | modelIdx: indexSelectModel, 36 | onModel: (value) { 37 | viewModel.updateSelectedModel(value); 38 | }, 39 | chatModes: chatModeList, 40 | chatModeIdx: indexSelectChatMode, 41 | onChatMode: (value) { 42 | viewModel.updateChatMode(value); 43 | }, 44 | prompt: optionState.prompt, 45 | onPrompt: (value) { 46 | viewModel.updatePrompt(value); 47 | }, 48 | ), 49 | ); 50 | }, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/ui/core/base/base_command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:freezed_annotation/freezed_annotation.dart'; 5 | 6 | part 'base_command.freezed.dart'; 7 | 8 | @freezed 9 | sealed class UICommand with _$UICommand { 10 | const factory UICommand.showToast({ 11 | required String message, 12 | @Default(Duration(seconds: 2)) Duration duration, 13 | }) = ShowToastCommand; 14 | 15 | const factory UICommand.showSnackBar({ 16 | required String message, 17 | @Default(Duration(seconds: 2)) Duration duration, 18 | SnackBarAction? action, 19 | }) = ShowSnackBarCommand; 20 | } 21 | 22 | class UICommandController { 23 | final _controller = StreamController.broadcast(); 24 | 25 | Stream get stream => _controller.stream; 26 | 27 | bool get isClosed => _controller.isClosed; 28 | 29 | void showSnackBar({ 30 | required String message, 31 | required Duration duration, 32 | SnackBarAction? action, 33 | }) { 34 | if (isClosed) return; 35 | _controller.add( 36 | ShowSnackBarCommand( 37 | message: message, 38 | duration: duration, 39 | action: action, 40 | ), 41 | ); 42 | } 43 | 44 | void showToast({ 45 | required String message, 46 | required Duration duration, 47 | }) { 48 | if (isClosed) return; 49 | _controller.add( 50 | ShowToastCommand(message: message, duration: duration), 51 | ); 52 | } 53 | 54 | Future dispose() async { 55 | if (isClosed) return; 56 | await _controller.close(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/ui/core/base/base_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../config/dependencies.dart'; 5 | import 'base_command.dart'; 6 | 7 | abstract class BaseCubit extends Cubit { 8 | @protected 9 | final UICommandController commandController; 10 | 11 | BaseCubit(super.state, {UICommandController? commandController}) 12 | : commandController = commandController ?? getIt(); 13 | 14 | void showToast( 15 | String message, { 16 | Duration duration = const Duration(seconds: 2), 17 | }) { 18 | if (commandController.isClosed) return; 19 | commandController.showToast( 20 | message: message, 21 | duration: duration, 22 | ); 23 | } 24 | 25 | void showSnackBar( 26 | String message, { 27 | Duration duration = const Duration(seconds: 2), 28 | SnackBarAction? action, 29 | }) { 30 | if (commandController.isClosed) return; 31 | commandController.showSnackBar( 32 | message: message, 33 | duration: duration, 34 | action: action, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/ui/core/base/base_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import 'base_cubit.dart'; 5 | 6 | abstract class BaseScreen extends StatelessWidget { 7 | const BaseScreen({super.key}); 8 | 9 | @protected 10 | T createViewModel(BuildContext context); 11 | 12 | @protected 13 | Widget buildScaffold(BuildContext context); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return BlocProvider( 18 | create: createViewModel, 19 | child: GestureDetector( 20 | onTap: () => FocusScope.of(context).unfocus(), 21 | child: buildScaffold(context), 22 | ), 23 | ); 24 | } 25 | } 26 | 27 | abstract class BaseStatefulScreen extends StatefulWidget { 28 | const BaseStatefulScreen({super.key}); 29 | 30 | @protected 31 | T createViewModel(BuildContext context); 32 | 33 | @protected 34 | Widget buildScaffold(BuildContext context); 35 | 36 | @override 37 | State> createState() => _BaseStatefulScreenState(); 38 | } 39 | 40 | class _BaseStatefulScreenState 41 | extends State> { 42 | late T _viewModel; 43 | 44 | @override 45 | void initState() { 46 | super.initState(); 47 | _viewModel = widget.createViewModel(context); 48 | } 49 | 50 | @override 51 | void dispose() { 52 | _viewModel.close(); 53 | super.dispose(); 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return BlocProvider( 59 | create: (context) => _viewModel, 60 | child: GestureDetector( 61 | onTap: () => FocusScope.of(context).unfocus(), 62 | child: widget.buildScaffold(context), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/ui/core/base/max_width_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MaxWidthContainer extends StatelessWidget { 4 | static const double _maxWidth = 832; 5 | static const double _horizontalPadding = 16; 6 | static const double _verticalPadding = 16; 7 | 8 | final _boxConstraints = const BoxConstraints(maxWidth: _maxWidth); 9 | 10 | final _defaultPadding = const EdgeInsets.symmetric( 11 | horizontal: _horizontalPadding, 12 | vertical: _verticalPadding, 13 | ); 14 | 15 | final Widget child; 16 | final EdgeInsetsGeometry? padding; 17 | 18 | const MaxWidthContainer({ 19 | super.key, 20 | required this.child, 21 | this.padding, 22 | }); 23 | 24 | @override 25 | Widget build(BuildContext context) => Center( 26 | child: Container( 27 | constraints: _boxConstraints, 28 | padding: padding ?? _defaultPadding, 29 | child: child, 30 | ), 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/core/themes/colors.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | abstract final class AppColors { 4 | static const grey = Color(0xff59636e); 5 | 6 | static const textDark = Color(0xff1f2328); 7 | static const textLight = Color(0xffd1d7e1); 8 | 9 | static const backgroundDark = Color(0xff222831); 10 | static const surfaceDark = Color(0xff151b24); 11 | static const surfaceLight = Color(0xfff7f8fa); 12 | 13 | static const green = Color(0xff34ae47); 14 | static const red = Color(0xffff5370); 15 | static const purple = Color(0xff6c71ff); 16 | } 17 | -------------------------------------------------------------------------------- /lib/ui/core/themes/icons.dart: -------------------------------------------------------------------------------- 1 | enum AppIcons { 2 | robot('robot-outline.svg'), 3 | chat('chat-outline.svg'), 4 | menuOpen('menu-open.svg'), 5 | menuClose('menu-close.svg'), 6 | dotsHorizontal('dots-horizontal.svg'), 7 | history('history.svg'), 8 | messageText('message-text-outline.svg'), 9 | database('database-outline.svg'), 10 | forum('forum-outline.svg'), 11 | github('github.svg'), 12 | ollama('ollama.svg'), 13 | tune('tune.svg'), 14 | headSnowflake('head-snowflake-outline.svg'), 15 | keyboard('keyboard-outline.svg'), 16 | ; 17 | 18 | static const String _basePath = 'assets/icons/'; 19 | 20 | final String _path; 21 | 22 | const AppIcons(this._path); 23 | 24 | String get path => '$_basePath$_path'; 25 | } 26 | -------------------------------------------------------------------------------- /lib/ui/core/themes/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | 5 | import 'colors.dart'; 6 | import 'theme_text.dart'; 7 | 8 | abstract final class AppTheme { 9 | static const _seedColor = Colors.lightBlue; 10 | 11 | static final _darkTextTheme = createTextTheme(isDark: true); 12 | static final TextTheme _lightTextTheme = createTextTheme(isDark: false); 13 | 14 | static final ThemeData light = _createThemeData(Brightness.light); 15 | static final dark = _createThemeData(Brightness.dark); 16 | 17 | static var lightSystemStyle = SystemUiOverlayStyle( 18 | systemNavigationBarColor: light.colorScheme.surface, 19 | systemNavigationBarIconBrightness: Brightness.dark, 20 | statusBarIconBrightness: Brightness.light, 21 | statusBarBrightness: Brightness.dark, 22 | ); 23 | 24 | static var darkSystemStyle = SystemUiOverlayStyle( 25 | systemNavigationBarColor: dark.colorScheme.surface, 26 | systemNavigationBarIconBrightness: Brightness.light, 27 | statusBarIconBrightness: Brightness.dark, 28 | statusBarBrightness: Brightness.light, 29 | ); 30 | 31 | static ThemeData _createThemeData(Brightness brightness) { 32 | final isDark = brightness == Brightness.dark; 33 | final surfaceColor = 34 | isDark ? AppColors.surfaceDark : AppColors.surfaceLight; 35 | final textTheme = isDark ? _darkTextTheme : _lightTextTheme; 36 | 37 | final colorScheme = ColorScheme.fromSeed( 38 | seedColor: _seedColor, 39 | brightness: brightness, 40 | surface: surfaceColor, 41 | ); 42 | 43 | final backgroundColor = isDark ? AppColors.backgroundDark : Colors.white; 44 | return ThemeData( 45 | colorScheme: colorScheme, 46 | scaffoldBackgroundColor: backgroundColor, 47 | fontFamily: GoogleFonts.notoSans().fontFamily, 48 | textTheme: textTheme, 49 | iconTheme: const IconThemeData( 50 | color: AppColors.grey, 51 | ), 52 | appBarTheme: AppBarTheme(color: backgroundColor), 53 | navigationRailTheme: NavigationRailThemeData( 54 | selectedIconTheme: IconThemeData( 55 | size: 20, 56 | color: isDark ? AppColors.textLight : AppColors.textDark, 57 | ), 58 | unselectedIconTheme: const IconThemeData( 59 | size: 20, 60 | color: AppColors.grey, 61 | ), 62 | ), 63 | bottomSheetTheme: const BottomSheetThemeData( 64 | showDragHandle: true, 65 | clipBehavior: Clip.antiAlias, 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/ui/core/themes/theme_ext.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/svg.dart'; 3 | 4 | import 'colors.dart'; 5 | import 'icons.dart'; 6 | 7 | extension ThemeExt on BuildContext { 8 | ThemeData get theme => Theme.of(this); 9 | 10 | ColorScheme get color => theme.colorScheme; 11 | 12 | TextTheme get textTheme => theme.textTheme; 13 | 14 | Brightness get brightness => theme.brightness; 15 | 16 | bool get isDarkMode => brightness == Brightness.dark; 17 | 18 | SvgPicture icon(AppIcons appIcon, {double? size, Color? color}) { 19 | return SvgPicture.asset( 20 | appIcon.path, 21 | semanticsLabel: appIcon.name, 22 | width: size ?? 24, 23 | height: size ?? 24, 24 | colorFilter: ColorFilter.mode( 25 | color ?? AppColors.grey, 26 | BlendMode.srcIn, 27 | ), 28 | ); 29 | } 30 | 31 | Material rippleCircle({ 32 | required Widget child, 33 | final GestureTapCallback? onTap, 34 | }) { 35 | return Material( 36 | color: Colors.transparent, 37 | child: Ink( 38 | decoration: const BoxDecoration( 39 | shape: BoxShape.circle, 40 | color: Colors.transparent, 41 | ), 42 | child: InkWell( 43 | customBorder: const CircleBorder(), 44 | splashFactory: InkRipple.splashFactory, 45 | splashColor: color.primary.withValues(alpha: 0.3), 46 | highlightColor: color.primary.withValues(alpha: 0.15), 47 | radius: 20, 48 | onTap: onTap, 49 | child: Padding( 50 | padding: const EdgeInsets.all(8.0), 51 | child: child, 52 | ), 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/ui/core/themes/theme_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'colors.dart'; 4 | 5 | TextTheme createTextTheme({ 6 | required bool isDark, 7 | }) { 8 | final baseStyle = TextStyle( 9 | letterSpacing: 0, 10 | leadingDistribution: TextLeadingDistribution.even, 11 | color: isDark ? AppColors.textLight : AppColors.textDark, 12 | ); 13 | 14 | return TextTheme( 15 | displayLarge: baseStyle.copyWith( 16 | fontSize: 36.0, 17 | fontWeight: FontWeight.bold, 18 | letterSpacing: -0.5, 19 | ), 20 | displayMedium: baseStyle.copyWith( 21 | fontSize: 28.0, 22 | fontWeight: FontWeight.bold, 23 | letterSpacing: -0.5, 24 | ), 25 | titleLarge: baseStyle.copyWith( 26 | fontSize: 24.0, 27 | fontWeight: FontWeight.bold, 28 | ), 29 | titleMedium: baseStyle.copyWith( 30 | fontSize: 22.0, 31 | fontWeight: FontWeight.w600, 32 | ), 33 | titleSmall: baseStyle.copyWith( 34 | fontSize: 18.0, 35 | fontWeight: FontWeight.w600, 36 | ), 37 | headlineLarge: baseStyle.copyWith( 38 | fontSize: 24.0, 39 | fontWeight: FontWeight.w600, 40 | ), 41 | headlineMedium: baseStyle.copyWith( 42 | fontSize: 20.0, 43 | fontWeight: FontWeight.w600, 44 | ), 45 | headlineSmall: baseStyle.copyWith( 46 | fontSize: 18.0, 47 | fontWeight: FontWeight.w500, 48 | ), 49 | bodyLarge: baseStyle.copyWith( 50 | fontSize: 16.0, 51 | fontWeight: FontWeight.w400, 52 | ), 53 | bodyMedium: baseStyle.copyWith( 54 | fontSize: 15.0, 55 | fontWeight: FontWeight.w400, 56 | ), 57 | bodySmall: baseStyle.copyWith( 58 | fontSize: 14.0, 59 | ), 60 | labelLarge: baseStyle.copyWith( 61 | fontSize: 15.0, 62 | fontWeight: FontWeight.w500, 63 | ), 64 | labelMedium: baseStyle.copyWith( 65 | fontSize: 13.0, 66 | fontWeight: FontWeight.w500, 67 | ), 68 | labelSmall: baseStyle.copyWith( 69 | fontSize: 12.0, 70 | fontWeight: FontWeight.w400, 71 | ), 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /lib/ui/core/widget/chip_menu_anchor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/svg.dart'; 3 | 4 | import '../themes/theme_ext.dart'; 5 | 6 | class ChipMenuAnchor extends StatefulWidget { 7 | final bool isMini; 8 | final SvgPicture icon; 9 | final List list; 10 | final int index; 11 | final String? tooltip; 12 | final Function(String?)? onSelected; 13 | 14 | const ChipMenuAnchor({ 15 | super.key, 16 | this.isMini = false, 17 | required this.icon, 18 | required this.list, 19 | required this.index, 20 | required this.onSelected, 21 | this.tooltip, 22 | }); 23 | 24 | @override 25 | State createState() => _ChipMenuAnchorState(); 26 | } 27 | 28 | class _ChipMenuAnchorState extends State { 29 | late MenuController _menuController; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | _menuController = MenuController(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | final borderRadius = BorderRadius.circular(8.0); 40 | 41 | if (widget.list.isEmpty) { 42 | return _buildChild(context, borderRadius, null); 43 | } 44 | 45 | return MenuAnchor( 46 | controller: _menuController, 47 | alignmentOffset: const Offset(0, 4), 48 | menuChildren: widget.list 49 | .map( 50 | (item) => MenuItemButton( 51 | onPressed: () { 52 | widget.onSelected?.call(item); 53 | _menuController.close(); 54 | }, 55 | child: Text(item), 56 | ), 57 | ) 58 | .toList(), 59 | builder: (context, controller, child) { 60 | return GestureDetector( 61 | onTap: () { 62 | if (controller.isOpen) { 63 | controller.close(); 64 | } else { 65 | controller.open(); 66 | } 67 | }, 68 | child: _buildChild(context, borderRadius, widget.list[widget.index]), 69 | ); 70 | }, 71 | ); 72 | } 73 | 74 | Widget _buildChild( 75 | BuildContext context, 76 | BorderRadius borderRadius, 77 | String? title, 78 | ) { 79 | return widget.isMini 80 | ? Material( 81 | color: Colors.transparent, 82 | child: Container( 83 | height: 36, 84 | padding: const EdgeInsets.symmetric(horizontal: 8), 85 | child: widget.icon, 86 | ), 87 | ) 88 | : Container( 89 | height: 36, 90 | padding: const EdgeInsets.symmetric(horizontal: 12), 91 | decoration: BoxDecoration( 92 | color: context.color.surface, 93 | borderRadius: borderRadius, 94 | border: Border.all( 95 | color: context.color.outline.withValues(alpha: 0.5), 96 | ), 97 | ), 98 | child: Row( 99 | mainAxisSize: MainAxisSize.min, 100 | children: [ 101 | widget.icon, 102 | const SizedBox(width: 8), 103 | if (title != null && 104 | widget.list.isNotEmpty && 105 | widget.index >= 0 && 106 | widget.index < widget.list.length) 107 | Text(title), 108 | ], 109 | ), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/ui/core/widget/dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | 4 | Future showAppDialog({ 5 | required BuildContext context, 6 | required String title, 7 | required Widget content, 8 | VoidCallback? onConfirm, 9 | String? confirmText, 10 | bool dismissOnConfirm = true, 11 | String? cancelText, 12 | }) { 13 | final localizations = MaterialLocalizations.of(context); 14 | return showDialog( 15 | context: context, 16 | builder: (context) { 17 | return AlertDialog( 18 | title: Text(title), 19 | content: SizedBox( 20 | width: 550, 21 | child: content, 22 | ), 23 | actions: onConfirm == null 24 | ? [] 25 | : [ 26 | TextButton( 27 | onPressed: () => context.pop(), 28 | child: Text(cancelText ?? localizations.cancelButtonLabel), 29 | ), 30 | TextButton( 31 | onPressed: () { 32 | onConfirm(); 33 | if (dismissOnConfirm) context.pop(); 34 | }, 35 | child: Text(confirmText ?? localizations.okButtonLabel), 36 | ), 37 | ], 38 | ); 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /lib/ui/core/widget/dragging_appbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../utils/platform_util.dart'; 4 | import '../themes/theme_ext.dart'; 5 | import 'dragging_widget.dart'; 6 | 7 | class DraggingAppBar extends StatelessWidget implements PreferredSizeWidget { 8 | static const double _kDesktopExtraHeight = 24.0; 9 | 10 | final String _label; 11 | final Widget? _title; 12 | final List? _actions; 13 | 14 | const DraggingAppBar({ 15 | super.key, 16 | String label = '', 17 | Widget? title, 18 | Widget? leading, 19 | List? actions, 20 | double? toolbarHeight, 21 | EdgeInsets? padding, 22 | }) : _actions = actions, 23 | _title = title, 24 | _label = label; 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Column( 29 | children: [ 30 | if (PlatformUtils.isDesktop) Container(height: _kDesktopExtraHeight), 31 | AppBar( 32 | toolbarHeight: kToolbarHeight, 33 | actions: _actions, 34 | scrolledUnderElevation: 0, 35 | elevation: 0, 36 | centerTitle: true, 37 | title: DraggingWidget( 38 | child: _title ?? 39 | Text( 40 | _label, 41 | textAlign: TextAlign.center, 42 | maxLines: 1, 43 | style: context.textTheme.titleMedium, 44 | ), 45 | ), 46 | ), 47 | ], 48 | ); 49 | } 50 | 51 | @override 52 | Size get preferredSize => Size.fromHeight( 53 | PlatformUtils.isDesktop 54 | ? kToolbarHeight + _kDesktopExtraHeight 55 | : kToolbarHeight, 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /lib/ui/core/widget/dragging_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:window_manager/window_manager.dart'; 3 | 4 | class DraggingWidget extends StatelessWidget { 5 | final Widget child; 6 | 7 | const DraggingWidget({ 8 | super.key, 9 | required this.child, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return GestureDetector( 15 | behavior: HitTestBehavior.translucent, 16 | onPanStart: (details) { 17 | windowManager.startDragging(); 18 | }, 19 | child: child, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/core/widget/overay_boundary.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // https://github.com/singerdmx/flutter-quill/issues/1697#issuecomment-2311548151 4 | class OverlayBoundary extends StatefulWidget { 5 | const OverlayBoundary({super.key, required this.child}); 6 | 7 | final Widget child; 8 | 9 | @override 10 | State createState() => _OverlayBoundaryState(); 11 | } 12 | 13 | class _OverlayBoundaryState extends State { 14 | late final OverlayEntry _overlayEntry = 15 | OverlayEntry(builder: (context) => widget.child); 16 | 17 | @override 18 | void didUpdateWidget(covariant OverlayBoundary oldWidget) { 19 | super.didUpdateWidget(oldWidget); 20 | _overlayEntry.markNeedsBuild(); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Overlay( 26 | initialEntries: [_overlayEntry], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/ui/root/root_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:freezed_annotation/freezed_annotation.dart'; 6 | import 'package:rxdart/rxdart.dart'; 7 | 8 | import '../../domain/repository/preferences_repository.dart'; 9 | import '../core/base/base_command.dart'; 10 | 11 | part 'root_viewmodel.freezed.dart'; 12 | 13 | @freezed 14 | class RootUiState with _$RootUiState { 15 | const factory RootUiState({ 16 | UICommand? command, 17 | ThemeMode? themeMode, 18 | }) = _RootUiState; 19 | } 20 | 21 | class RootViewModel extends Cubit { 22 | final PreferencesRepository _preferencesRepository; 23 | final UICommandController _uiCommandController; 24 | 25 | final _compositeSubscription = CompositeSubscription(); 26 | 27 | RootViewModel( 28 | this._preferencesRepository, 29 | this._uiCommandController, 30 | ) : super(const RootUiState()) { 31 | _init(); 32 | } 33 | 34 | void _init() { 35 | _subscribeToCommands(); 36 | 37 | _preferencesRepository.watch(PreferenceKeys.themeMode).listen( 38 | (String? event) { 39 | final themeMode = 40 | ThemeMode.values.byName(event?.toLowerCase() ?? 'dark'); 41 | emit(state.copyWith(themeMode: themeMode)); 42 | }, 43 | ).addTo(_compositeSubscription); 44 | } 45 | 46 | void _subscribeToCommands() { 47 | _uiCommandController.stream 48 | .listen( 49 | (command) => emit(state.copyWith(command: command)), 50 | ) 51 | .addTo(_compositeSubscription); 52 | } 53 | 54 | void clearCommand() { 55 | emit(state.copyWith(command: null)); 56 | } 57 | 58 | @override 59 | Future close() async { 60 | await _compositeSubscription.dispose(); 61 | await _uiCommandController.dispose(); 62 | await super.close(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/ui/root/widget/root_navigation_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | 4 | import '../../core/themes/theme_ext.dart'; 5 | import 'root_tab.dart'; 6 | 7 | class RootNavigationBar extends StatelessWidget { 8 | final StatefulNavigationShell navigationShell; 9 | 10 | static List? _cachedItems; 11 | 12 | static List _buildTabItems(BuildContext context) { 13 | _cachedItems ??= RootTab.values.map((tab) { 14 | return BottomNavigationBarItem( 15 | label: tab.label, 16 | icon: context.icon(tab.icon, size: 28), 17 | ); 18 | }).toList(); 19 | 20 | return _cachedItems!; 21 | } 22 | 23 | const RootNavigationBar({ 24 | super.key, 25 | required this.navigationShell, 26 | }); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return BottomNavigationBar( 31 | items: _buildTabItems(context), 32 | currentIndex: navigationShell.currentIndex, 33 | onTap: (index) { 34 | context.go(RootTab.values[index].path); 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/ui/root/widget/root_navigation_rail.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:url_launcher/url_launcher.dart'; 4 | 5 | import '../../core/themes/icons.dart'; 6 | import '../../core/themes/theme_ext.dart'; 7 | import '../../core/widget/dragging_widget.dart'; 8 | import 'root_tab.dart'; 9 | 10 | class RootNavigationRail extends StatelessWidget { 11 | final StatefulNavigationShell navigationShell; 12 | final bool isExpanded; 13 | 14 | static List? _cachedList; 15 | 16 | static List _buildList(BuildContext context) { 17 | _cachedList ??= RootTab.values.map( 18 | (tab) { 19 | return NavigationRailDestination( 20 | padding: const EdgeInsets.symmetric(vertical: 6), 21 | label: Text(tab.label), 22 | icon: context.icon(tab.icon), 23 | ); 24 | }, 25 | ).toList(); 26 | return _cachedList!; 27 | } 28 | 29 | const RootNavigationRail({ 30 | super.key, 31 | required this.navigationShell, 32 | required this.isExpanded, 33 | }); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return DraggingWidget( 38 | child: NavigationRail( 39 | destinations: _buildList(context), 40 | selectedIndex: navigationShell.currentIndex, 41 | extended: isExpanded, 42 | minExtendedWidth: 180, 43 | minWidth: 70, 44 | onDestinationSelected: (index) { 45 | context.go(RootTab.values[index].path); 46 | }, 47 | leading: GestureDetector( 48 | onTap: () => context.go(RootTab.home.path), 49 | child: Padding( 50 | padding: const EdgeInsets.only(top: 24 + 16, bottom: 24), 51 | child: context.icon(AppIcons.robot, size: 36), 52 | ), 53 | ), 54 | trailing: Expanded( 55 | child: Column( 56 | mainAxisAlignment: MainAxisAlignment.end, 57 | children: [ 58 | _buildTrailing(context), 59 | const SizedBox(height: 48), 60 | ], 61 | ), 62 | ), 63 | ), 64 | ); 65 | } 66 | 67 | _buildTrailing(BuildContext context) { 68 | return Row( 69 | children: [ 70 | context.rippleCircle( 71 | child: context.icon( 72 | AppIcons.github, 73 | ), 74 | onTap: () async { 75 | await launchUrl( 76 | Uri.parse('https://github.com/shinhyo/OllamaTalk'), 77 | mode: LaunchMode.externalApplication, 78 | ); 79 | }, 80 | ), 81 | ], 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/ui/root/widget/root_tab.dart: -------------------------------------------------------------------------------- 1 | import '../../core/themes/icons.dart'; 2 | import '../../routing/router.dart'; 3 | import '../../tab/history/chat_history_screen.dart'; 4 | import '../../tab/home/home_screen.dart'; 5 | import '../../tab/setting/setting_screen.dart'; 6 | 7 | enum RootTab { 8 | home( 9 | label: HomeScreen.label, 10 | path: HomeScreenRoute.path, 11 | icon: AppIcons.chat, 12 | ), 13 | history( 14 | label: ChatHistoryScreen.label, 15 | path: ChatHistoryScreenRoute.path, 16 | icon: AppIcons.history, 17 | ), 18 | setting( 19 | label: SettingScreen.label, 20 | path: SettingScreenRoute.path, 21 | icon: AppIcons.dotsHorizontal, 22 | ); 23 | 24 | final String path; 25 | final String label; 26 | final AppIcons icon; 27 | 28 | const RootTab({ 29 | required this.path, 30 | required this.label, 31 | required this.icon, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /lib/ui/routing/route/chat_history_shell_branch.dart: -------------------------------------------------------------------------------- 1 | part of '../router.dart'; 2 | 3 | const chatHistoryShellBranch = TypedStatefulShellBranch( 4 | routes: >[ 5 | TypedGoRoute( 6 | path: ChatHistoryScreenRoute.path, 7 | routes: [ 8 | TypedGoRoute( 9 | path: ChatScreenRoute.path, 10 | ), 11 | ], 12 | ), 13 | ], 14 | ); 15 | 16 | class ChatHistoryScreenRoute extends GoRouteData { 17 | const ChatHistoryScreenRoute(); 18 | 19 | static const path = '/history'; 20 | 21 | @override 22 | Widget build(BuildContext context, GoRouterState state) { 23 | return const ChatHistoryScreen(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/ui/routing/route/shell_branch_home.dart: -------------------------------------------------------------------------------- 1 | part of '../router.dart'; 2 | 3 | const homeShellBranch = TypedStatefulShellBranch( 4 | routes: >[ 5 | TypedGoRoute( 6 | path: HomeScreenRoute.path, 7 | routes: [ 8 | TypedGoRoute( 9 | path: ChatScreenRoute.path, 10 | ), 11 | ], 12 | ), 13 | ], 14 | ); 15 | 16 | class HomeScreenRoute extends GoRouteData { 17 | const HomeScreenRoute(); 18 | 19 | static const path = '/'; 20 | 21 | @override 22 | Widget build(BuildContext context, GoRouterState state) { 23 | return const HomeScreen(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/ui/routing/route/shell_branch_setting.dart: -------------------------------------------------------------------------------- 1 | part of '../router.dart'; 2 | 3 | const settingShellBranch = TypedStatefulShellBranch( 4 | routes: >[ 5 | TypedGoRoute( 6 | path: SettingScreenRoute.path, 7 | ), 8 | ], 9 | ); 10 | 11 | class SettingScreenRoute extends GoRouteData { 12 | const SettingScreenRoute(); 13 | 14 | static const path = '/setting'; 15 | 16 | @override 17 | Widget build(BuildContext context, GoRouterState state) { 18 | return const SettingScreen(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/ui/routing/route/shell_route_root.dart: -------------------------------------------------------------------------------- 1 | part of '../router.dart'; 2 | 3 | @TypedStatefulShellRoute( 4 | branches: [ 5 | homeShellBranch, 6 | chatHistoryShellBranch, 7 | settingShellBranch, 8 | ], 9 | ) 10 | class RootPageShellRoute extends StatefulShellRouteData { 11 | const RootPageShellRoute(); 12 | 13 | static DateTime? _lastBackPressTime; 14 | 15 | Future _handleBackPress( 16 | BuildContext context, 17 | StatefulNavigationShell navigationShell, 18 | ) async { 19 | if (defaultTargetPlatform != TargetPlatform.android) { 20 | return false; 21 | } 22 | 23 | if (context.canPop()) { 24 | context.pop(); 25 | return true; 26 | } 27 | 28 | if (navigationShell.currentIndex != 0) { 29 | navigationShell.goBranch(0); 30 | return true; 31 | } 32 | 33 | final now = DateTime.now(); 34 | if (_lastBackPressTime == null || 35 | now.difference(_lastBackPressTime!) > const Duration(seconds: 2)) { 36 | _lastBackPressTime = now; 37 | 38 | ScaffoldMessenger.of(context).showSnackBar( 39 | SnackBar( 40 | content: Text(t.home.backExit), 41 | ), 42 | ); 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | @override 50 | Widget builder( 51 | BuildContext context, 52 | GoRouterState state, 53 | StatefulNavigationShell navigationShell, 54 | ) { 55 | return BackButtonListener( 56 | onBackButtonPressed: () => _handleBackPress(context, navigationShell), 57 | child: RootScreen( 58 | navigationShell: navigationShell, 59 | ), 60 | ); 61 | } 62 | } 63 | 64 | @freezed 65 | class ChatScreenParams with _$ChatScreenParams { 66 | const factory ChatScreenParams({ 67 | @Default(null) int? id, 68 | @Default(null) String? model, 69 | @Default(null) String? mode, 70 | @Default(null) String? prompt, 71 | @Default(null) String? question, 72 | }) = _ChatScreenParams; 73 | } 74 | 75 | class ChatScreenRoute extends GoRouteData { 76 | final int? id; 77 | final String? model; 78 | final String? chatMode; 79 | final String? prompt; 80 | final String? question; 81 | 82 | const ChatScreenRoute({ 83 | this.id, 84 | this.model, 85 | this.chatMode, 86 | this.prompt, 87 | this.question, 88 | }); 89 | 90 | const ChatScreenRoute.loadHistory({ 91 | this.id, 92 | }) : model = null, 93 | chatMode = null, 94 | prompt = null, 95 | question = null; 96 | 97 | static const path = '/chat'; 98 | static final $parentNavigatorKey = rootNavigatorKey; 99 | 100 | @override 101 | Widget build(BuildContext context, GoRouterState state) { 102 | return ChatScreen( 103 | params: ChatScreenParams( 104 | id: id, 105 | model: model, 106 | mode: chatMode, 107 | prompt: prompt, 108 | question: question, 109 | ), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/ui/routing/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:bot_toast/bot_toast.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:freezed_annotation/freezed_annotation.dart'; 5 | import 'package:get_it/get_it.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | import 'package:talker_flutter/talker_flutter.dart'; 8 | 9 | import '../../i18n/strings.g.dart'; 10 | import '../../utils/logger.dart'; 11 | import '../chat/chat_screen.dart'; 12 | import '../root/root_screen.dart'; 13 | import '../tab/history/chat_history_screen.dart'; 14 | import '../tab/home/home_screen.dart'; 15 | import '../tab/setting/setting_screen.dart'; 16 | 17 | part 'route/chat_history_shell_branch.dart'; 18 | 19 | part 'route/shell_branch_home.dart'; 20 | 21 | part 'route/shell_branch_setting.dart'; 22 | 23 | part 'route/shell_route_root.dart'; 24 | 25 | part 'router.freezed.dart'; 26 | 27 | part 'router.g.dart'; 28 | 29 | final rootNavigatorKey = GlobalKey(debugLabel: 'root'); 30 | 31 | final GoRouter router = GoRouter( 32 | debugLogDiagnostics: kDebugMode, 33 | navigatorKey: rootNavigatorKey, 34 | initialLocation: HomeScreenRoute.path, 35 | routes: [ 36 | ...$appRoutes, 37 | ], 38 | observers: [ 39 | TalkerRouteObserver(GetIt.I().talker), 40 | BotToastNavigatorObserver(), 41 | ], 42 | ); 43 | -------------------------------------------------------------------------------- /lib/ui/tab/history/chat_history_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../config/dependencies.dart'; 5 | import '../../../i18n/strings.g.dart'; 6 | import '../../core/base/base_screen.dart'; 7 | import '../../core/themes/theme_ext.dart'; 8 | import 'chat_history_viewmodel.dart'; 9 | import 'widget/chat_room_header.dart'; 10 | import 'widget/chat_room_list.dart'; 11 | 12 | class ChatHistoryScreen extends BaseScreen { 13 | const ChatHistoryScreen({super.key}); 14 | 15 | static const label = 'History'; 16 | 17 | @override 18 | ChatHistoryViewModel createViewModel(BuildContext context) { 19 | return getIt(); 20 | } 21 | 22 | @override 23 | Widget buildScaffold(BuildContext context) { 24 | return Scaffold( 25 | body: BlocConsumer( 26 | builder: (context, state) { 27 | if (state.isLoading) { 28 | return const Center(child: CircularProgressIndicator()); 29 | } 30 | 31 | if (state.chatRooms.isEmpty) { 32 | return _buildEmptyHistory(context); 33 | } 34 | 35 | final chatRooms = state.chatRooms; 36 | return ListView.separated( 37 | itemCount: chatRooms.length, 38 | separatorBuilder: (_, __) => const Divider(thickness: 0.1), 39 | itemBuilder: (context, index) { 40 | final item = chatRooms[index]; 41 | return item.map( 42 | header: (header) => 43 | ChatRoomHeader(date: header.date, index: index), 44 | room: (room) => ChatRoomListItem( 45 | item: room, 46 | isLast: index == chatRooms.length - 1, 47 | ), 48 | ); 49 | }, 50 | ); 51 | }, 52 | listener: (context, state) {}, 53 | ), 54 | ); 55 | } 56 | 57 | Widget _buildEmptyHistory(BuildContext context) { 58 | return Center( 59 | child: Column( 60 | mainAxisAlignment: MainAxisAlignment.center, 61 | children: [ 62 | const Icon(Icons.forum_outlined, size: 48), 63 | const SizedBox(height: 10), 64 | Text( 65 | t.history.emptyTitle, 66 | style: context.textTheme.titleMedium?.copyWith( 67 | fontWeight: FontWeight.bold, 68 | ), 69 | ), 70 | ], 71 | ), 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/ui/tab/history/widget/chat_room_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../core/themes/theme_ext.dart'; 4 | 5 | class ChatRoomHeader extends StatelessWidget { 6 | final String date; 7 | final int index; 8 | 9 | const ChatRoomHeader({ 10 | super.key, 11 | required this.date, 12 | required this.index, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | padding: index == 0 19 | ? const EdgeInsets.fromLTRB(16, 32, 16, 16) 20 | : const EdgeInsets.symmetric(vertical: 16, horizontal: 16), 21 | width: double.infinity, 22 | child: Text( 23 | date, 24 | style: context.textTheme.headlineSmall?.copyWith( 25 | color: Colors.grey, 26 | ), 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/tab/home/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../config/dependencies.dart'; 5 | import '../../../i18n/strings.g.dart'; 6 | import '../../core/base/base_screen.dart'; 7 | import '../../core/base/max_width_container.dart'; 8 | import '../../core/themes/icons.dart'; 9 | import '../../core/themes/theme_ext.dart'; 10 | import '../../routing/router.dart'; 11 | import 'home_viewmodel.dart'; 12 | import 'widget/home_input_text.dart'; 13 | import 'widget/home_tool_bar.dart'; 14 | 15 | class HomeScreen extends BaseStatefulScreen { 16 | const HomeScreen({super.key}); 17 | 18 | static const label = 'New Chat'; 19 | 20 | @override 21 | HomeViewModel createViewModel(BuildContext context) { 22 | return getIt(); 23 | } 24 | 25 | @override 26 | Widget buildScaffold(BuildContext context) { 27 | return BlocListener( 28 | listenWhen: (previous, current) => 29 | previous.navigateChatScreen != current.navigateChatScreen, 30 | listener: (context, state) { 31 | final toolbarState = state.toolbar; 32 | if (state.navigateChatScreen != null && 33 | state.currentInput.isNotEmpty && 34 | toolbarState.prompt != null) { 35 | ChatScreenRoute( 36 | question: state.navigateChatScreen, 37 | model: toolbarState.models[toolbarState.modelIdx], 38 | chatMode: toolbarState.chatModes[toolbarState.chatModeIdx], 39 | prompt: toolbarState.prompt, 40 | ).push(context); 41 | context.read().clearNavigate(); 42 | } 43 | }, 44 | child: Scaffold( 45 | body: Center( 46 | child: ScrollConfiguration( 47 | behavior: ScrollConfiguration.of(context).copyWith( 48 | scrollbars: false, 49 | ), 50 | child: SingleChildScrollView( 51 | primary: false, 52 | child: MaxWidthContainer( 53 | child: Column( 54 | mainAxisAlignment: MainAxisAlignment.center, 55 | children: [ 56 | context.icon(AppIcons.robot, size: 60), 57 | const SizedBox(height: 10), 58 | Text( 59 | t.home.know, 60 | style: context.textTheme.titleLarge?.copyWith( 61 | fontWeight: FontWeight.bold, 62 | ), 63 | textAlign: TextAlign.center, 64 | ), 65 | const SizedBox(height: 40), 66 | const HomeInputField(), 67 | const ChipToolBar(), 68 | ], 69 | ), 70 | ), 71 | ), 72 | ), 73 | ), 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/ui/tab/home/widget/home_tool_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:go_router/go_router.dart'; 4 | 5 | import '../../../../i18n/strings.g.dart'; 6 | import '../../../root/widget/root_tab.dart'; 7 | import '../home_viewmodel.dart'; 8 | import 'home_toolbar_chips.dart'; 9 | 10 | class ChipToolBar extends StatelessWidget { 11 | const ChipToolBar({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Padding( 16 | padding: const EdgeInsets.only(top: 16), 17 | child: BlocBuilder( 18 | builder: (context, state) { 19 | if (state.uiError == const UiError.initDataLoadFailed()) { 20 | return _buildConnectionFailed(context); 21 | } 22 | 23 | final viewModel = context.read(); 24 | 25 | final toolbarState = state.toolbar; 26 | return ChatCommonToolbar( 27 | isLoading: toolbarState.isLoading, 28 | models: toolbarState.models, 29 | modelIdx: toolbarState.modelIdx, 30 | onModel: (value) { 31 | viewModel.updateSelectedModel(value); 32 | }, 33 | chatModes: toolbarState.chatModes, 34 | chatModeIdx: toolbarState.chatModeIdx, 35 | onChatMode: (value) { 36 | viewModel.updateChatMode(value); 37 | }, 38 | prompt: toolbarState.prompt ?? '', 39 | onPrompt: (value) { 40 | viewModel.updatePrompt(value); 41 | }, 42 | ); 43 | }, 44 | ), 45 | ); 46 | } 47 | 48 | ActionChip _buildConnectionFailed(BuildContext context) { 49 | return ActionChip( 50 | label: Row( 51 | mainAxisSize: MainAxisSize.min, 52 | children: [ 53 | const Icon( 54 | Icons.error_outline, 55 | color: Colors.red, 56 | ), 57 | const SizedBox(width: 8), 58 | Flexible( 59 | child: Text( 60 | t.home.unavailable, 61 | overflow: TextOverflow.ellipsis, 62 | maxLines: 3, 63 | softWrap: true, 64 | ), 65 | ), 66 | ], 67 | ), 68 | onPressed: () { 69 | context.go(RootTab.setting.path); 70 | }, 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/utils/keyboard_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | class KeyboardUtils { 5 | KeyboardUtils._(); 6 | 7 | static void handleEnterKeyEvent({ 8 | required bool isMobile, 9 | required KeyEvent event, 10 | required TextEditingController controller, 11 | required VoidCallback onSubmit, 12 | }) { 13 | if (!isMobile && 14 | event is KeyDownEvent && 15 | event.logicalKey == LogicalKeyboardKey.enter) { 16 | if (HardwareKeyboard.instance.isShiftPressed) { 17 | _insertNewLine(controller); 18 | } else { 19 | onSubmit(); 20 | } 21 | } 22 | } 23 | 24 | static void _insertNewLine(TextEditingController controller) { 25 | final text = controller.text; 26 | final selection = controller.selection; 27 | final newText = text.replaceRange(selection.start, selection.end, '\n'); 28 | controller.value = TextEditingValue( 29 | text: newText, 30 | selection: TextSelection.collapsed(offset: selection.start + 1), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/utils/logger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:talker_flutter/talker_flutter.dart'; 4 | 5 | import '../config/dependencies.dart'; 6 | 7 | enum LogTag { error, warning, info, debug, critical, verbose } 8 | 9 | class Logger { 10 | static final Logger _instance = Logger._internal(); 11 | 12 | final Talker _talker; 13 | 14 | static const JsonEncoder _encoder = JsonEncoder.withIndent(' '); 15 | 16 | Logger._internal() 17 | : _talker = Talker( 18 | settings: TalkerSettings(), 19 | ); 20 | 21 | factory Logger() => _instance; 22 | 23 | void log(dynamic message, LogTag tag, [StackTrace? stackTrace]) { 24 | final String formattedMessage = _formatMessage(message); 25 | final logFunction = _getLogFunction(tag, stackTrace); 26 | logFunction(formattedMessage); 27 | } 28 | 29 | String _formatMessage(dynamic message) { 30 | if (message is String) { 31 | return message; 32 | } 33 | return message is Map || message is List 34 | ? _encoder.convert(message) 35 | : message.toString(); 36 | } 37 | 38 | void Function(String) _getLogFunction(LogTag tag, [StackTrace? stackTrace]) { 39 | switch (tag) { 40 | case LogTag.error: 41 | return (String msg) => _talker.error(msg, null, stackTrace); 42 | case LogTag.warning: 43 | return _talker.warning; 44 | case LogTag.info: 45 | return _talker.info; 46 | case LogTag.debug: 47 | return _talker.debug; 48 | case LogTag.critical: 49 | return _talker.critical; 50 | case LogTag.verbose: 51 | return _talker.verbose; 52 | } 53 | } 54 | 55 | Talker get talker => _talker; 56 | } 57 | 58 | void logError(dynamic message, [StackTrace? stackTrace]) => 59 | getIt().log(message, LogTag.error, stackTrace); 60 | 61 | void logWarning(dynamic message) => 62 | getIt().log(message, LogTag.warning); 63 | 64 | void logInfo(dynamic message) => getIt().log(message, LogTag.info); 65 | 66 | void logDebug(dynamic message) => getIt().log(message, LogTag.debug); 67 | 68 | void logCritical(dynamic message) => 69 | getIt().log(message, LogTag.critical); 70 | 71 | void logVerbose(dynamic message) => 72 | getIt().log(message, LogTag.verbose); 73 | -------------------------------------------------------------------------------- /lib/utils/platform_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | class PlatformUtils { 6 | PlatformUtils._(); 7 | 8 | static bool get isDesktop => 9 | !kIsWeb && 10 | (Platform.isWindows || 11 | Platform.isLinux || 12 | Platform.isMacOS || 13 | Platform.isFuchsia); 14 | 15 | static bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS); 16 | 17 | static bool get isWeb => kIsWeb; 18 | } 19 | -------------------------------------------------------------------------------- /lib/utils/size_ext.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum ScreenSize { 4 | compact(maxWidth: 600, spacing: 16), 5 | medium(maxWidth: 840, spacing: 24), 6 | expanded(maxWidth: double.infinity, spacing: 24); 7 | 8 | const ScreenSize({ 9 | required this.maxWidth, 10 | required this.spacing, 11 | }); 12 | 13 | final double maxWidth; 14 | final double spacing; 15 | 16 | static ScreenSize fromWidth(double width) { 17 | if (width < compact.maxWidth) return compact; 18 | if (width < medium.maxWidth) return medium; 19 | return expanded; 20 | } 21 | } 22 | 23 | extension SizeExt on Size { 24 | ScreenSize get screenSize => ScreenSize.fromWidth(width); 25 | 26 | double get spacing => screenSize.spacing; 27 | } 28 | 29 | extension BuildContextExt on BuildContext { 30 | Size get mediaSize => MediaQuery.sizeOf(this); 31 | 32 | ScreenSize get screenSize => mediaSize.screenSize; 33 | 34 | double get spacing => screenSize.spacing; 35 | 36 | bool get isCompact => screenSize == ScreenSize.compact; 37 | 38 | bool get isMedium => screenSize == ScreenSize.medium; 39 | 40 | bool get isExpanded => screenSize == ScreenSize.expanded; 41 | } 42 | -------------------------------------------------------------------------------- /lib/utils/string_ext.dart: -------------------------------------------------------------------------------- 1 | extension StringExt on String { 2 | String get removeNewLines => replaceAll(RegExp(r'\n'), ' '); 3 | 4 | String toFirstUpper() => 5 | length > 0 ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : ''; 6 | } 7 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | screen_retriever_linux 7 | sqlite3_flutter_libs 8 | url_launcher_linux 9 | window_manager 10 | ) 11 | 12 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 13 | ) 14 | 15 | set(PLUGIN_BUNDLED_LIBRARIES) 16 | 17 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 18 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 19 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 22 | endforeach(plugin) 23 | 24 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 25 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 27 | endforeach(ffi_plugin) 28 | -------------------------------------------------------------------------------- /linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /linux/runner/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/runner/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /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/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 | - FlutterMacOS (1.0.0) 3 | - screen_retriever_macos (0.0.1): 4 | - FlutterMacOS 5 | - window_manager (0.2.0): 6 | - FlutterMacOS 7 | 8 | DEPENDENCIES: 9 | - FlutterMacOS (from `Flutter/ephemeral`) 10 | - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) 11 | - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) 12 | 13 | EXTERNAL SOURCES: 14 | FlutterMacOS: 15 | :path: Flutter/ephemeral 16 | screen_retriever_macos: 17 | :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos 18 | window_manager: 19 | :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos 20 | 21 | SPEC CHECKSUMS: 22 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 23 | screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f 24 | window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c 25 | 26 | PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 27 | 28 | COCOAPODS: 1.16.2 29 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "ebc3780269c73e8c5ee41f3c6454f64a7d12fd2bdd6f3ac20085eb05b97a95ab", 3 | "pins" : [ 4 | { 5 | "identity" : "csqlite", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/simolus3/CSQLite.git", 8 | "state" : { 9 | "revision" : "f13dc216059e85d60beed2bd9900f74be241e14d" 10 | } 11 | } 12 | ], 13 | "version" : 3 14 | } 15 | -------------------------------------------------------------------------------- /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.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "ebc3780269c73e8c5ee41f3c6454f64a7d12fd2bdd6f3ac20085eb05b97a95ab", 3 | "pins" : [ 4 | { 5 | "identity" : "csqlite", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/simolus3/CSQLite.git", 8 | "state" : { 9 | "revision" : "f13dc216059e85d60beed2bd9900f74be241e14d" 10 | } 11 | } 12 | ], 13 | "version" : 3 14 | } 15 | -------------------------------------------------------------------------------- /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 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/102.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/108.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/234.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/234.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/258.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/258.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/66.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/macos/Runner/Assets.xcassets/AppIcon.appiconset/92.png -------------------------------------------------------------------------------- /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 = OllamaTalk 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = io.github.shinhyo.ollama.talk 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 io.github.shinhyo.ollama.talk. 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.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | com.apple.security.app-sandbox 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 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /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.network.client 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.app-sandbox 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ollama_talk 2 | description: "A new Flutter project." 3 | 4 | version: 0.2.0+2 5 | 6 | environment: 7 | sdk: '>=3.6.0 <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | flutter_localizations: 13 | sdk: flutter 14 | 15 | # State Management & Reactive Programming 16 | flutter_bloc: ^9.0.0 17 | rxdart: ^0.28.0 18 | 19 | # Logging & Debugging 20 | talker_flutter: ^4.6.11 21 | talker_dio_logger: ^4.6.11 22 | talker_bloc_logger: ^4.6.11 23 | 24 | # Code Generation & Serialization 25 | freezed_annotation: ^2.4.4 26 | json_annotation: ^4.9.0 27 | 28 | # Dependency Injection & Routing 29 | get_it: ^8.0.3 30 | go_router: ^14.8.0 31 | 32 | # UI Components & Styling 33 | cached_network_image: ^3.4.1 34 | google_fonts: ^6.2.1 35 | flutter_svg: ^2.0.17 36 | markdown_widget: ^2.3.2+6 37 | url_launcher: ^6.3.1 38 | window_manager: ^0.4.3 39 | bot_toast: ^4.1.3 40 | 41 | # Storage & Persistence 42 | shared_preferences: ^2.5.2 43 | 44 | # Database 45 | drift: ^2.25.0 46 | drift_flutter: ^0.2.4 47 | 48 | # Networking 49 | dio: ^5.8.0+1 50 | 51 | # Utilities 52 | intl: ^0.20.2 53 | universal_platform: ^1.1.0 54 | path_provider: ^2.1.5 55 | path: ^1.9.0 56 | package_info_plus: ^8.2.1 57 | device_preview: ^1.2.0 58 | visibility_detector: ^0.4.0+2 59 | # flutter_native_splash: ^2.4.4 60 | 61 | # i18n 62 | slang: ^4.4.1 63 | slang_flutter: ^4.4.0 64 | 65 | dev_dependencies: 66 | flutter_test: 67 | sdk: flutter 68 | 69 | # Linting & Analysis 70 | flutter_lints: ^5.0.0 71 | 72 | # Code Generation Tools 73 | build_runner: ^2.4.14 74 | freezed: ^2.5.8 75 | go_router_builder: ^2.7.5 76 | json_serializable: ^6.9.3 77 | drift_dev: ^2.25.0 78 | mockito: ^5.4.5 79 | slang_build_runner: ^4.4.1 80 | # flutter_launcher_icons: ^0.14.3 81 | 82 | dependency_overrides: 83 | intl: ^0.20.2 84 | # async: ^2.12.0 85 | 86 | flutter: 87 | uses-material-design: true 88 | assets: 89 | - assets/ 90 | - assets/icons/ 91 | - assets/images/ 92 | 93 | #flutter_launcher_icons: 94 | # android: "launcher_icon" 95 | # ios: true 96 | # image_path: "assets/icon/icon.png" 97 | # min_sdk_android: 21 # android min sdk min:16, default 21 98 | # web: 99 | # generate: true 100 | # image_path: "path/to/image.png" 101 | # background_color: "#hexcode" 102 | # theme_color: "#hexcode" 103 | # windows: 104 | # generate: true 105 | # image_path: "path/to/image.png" 106 | # icon_size: 48 # min:48, max:256, default: 48 107 | # macos: 108 | # generate: true 109 | # image_path: "path/to/image.png" 110 | 111 | #flutter_native_splash: 112 | # color: "#ffffff" 113 | # image: assets/images/app_icon_light.png 114 | # # branding: assets/branding-development.png 115 | ## color_dark: "#1e272d" 116 | ## image_dark: assets/images/app_icon_dark.png 117 | # # branding_dark: assets/branding-development.png 118 | # 119 | # android_12: 120 | # image: assets/images/app_icon_light.png 121 | # icon_background_color: "#ffffff" 122 | # image_dark: assets/images/app_icon_dark.png 123 | # icon_background_color_dark: "#1e272d" 124 | # 125 | # web: false 126 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | screen_retriever_windows 7 | share_plus 8 | sqlite3_flutter_libs 9 | url_launcher_windows 10 | window_manager 11 | ) 12 | 13 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 14 | ) 15 | 16 | set(PLUGIN_BUNDLED_LIBRARIES) 17 | 18 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 19 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 20 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 23 | endforeach(plugin) 24 | 25 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 26 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 28 | endforeach(ffi_plugin) 29 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"OllamaTalk", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinhyo/OllamaTalk/0de8401277533458d70994a8e7ff250411a948fe/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | unsigned int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length == 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | --------------------------------------------------------------------------------