├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.run/build_macos.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.run/build_runner.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.run/dev.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.run/prod.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
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 |
--------------------------------------------------------------------------------