Repository: ibrahimcetin/reins Branch: main Commit: c4d0d1f620d3 Files: 172 Total size: 466.2 KB Directory structure: gitextract_0cw_5yz_/ ├── .gitignore ├── .metadata ├── .vscode/ │ ├── launch.json │ └── settings.json ├── LICENSE ├── PRIVACY ├── README.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── dev/ │ │ │ │ └── ibrahimcetin/ │ │ │ │ └── reins/ │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-night/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-night-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── values/ │ │ │ │ └── styles.xml │ │ │ ├── values-night/ │ │ │ │ └── styles.xml │ │ │ ├── values-night-v31/ │ │ │ │ └── styles.xml │ │ │ └── values-v31/ │ │ │ └── styles.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle ├── devtools_options.yaml ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── LaunchBackground.imageset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── RunnerTests/ │ └── RunnerTests.swift ├── lib/ │ ├── Constants/ │ │ ├── app_constants.dart │ │ ├── chat_presets.dart │ │ ├── constants.dart │ │ ├── generate_title_constants.dart │ │ └── path_manager.dart │ ├── Extensions/ │ │ └── markdown_stylesheet_extension.dart │ ├── Models/ │ │ ├── api/ │ │ │ ├── create_request.dart │ │ │ ├── show_response.dart │ │ │ └── tags_response.dart │ │ ├── chat_configure_arguments.dart │ │ ├── chat_preset.dart │ │ ├── model_capabilities.dart │ │ ├── ollama_chat.dart │ │ ├── ollama_exception.dart │ │ ├── ollama_message.dart │ │ ├── ollama_model.dart │ │ ├── ollama_request_state.dart │ │ └── settings_route_arguments.dart │ ├── Pages/ │ │ ├── chat_page/ │ │ │ ├── chat_page.dart │ │ │ ├── chat_page_view_model.dart │ │ │ └── subwidgets/ │ │ │ ├── chat_attachment/ │ │ │ │ ├── chat_attachment_image.dart │ │ │ │ ├── chat_attachment_preset.dart │ │ │ │ └── chat_attachment_row.dart │ │ │ ├── chat_bubble/ │ │ │ │ ├── chat_bubble.dart │ │ │ │ ├── chat_bubble_actions.dart │ │ │ │ ├── chat_bubble_bottom_sheet.dart │ │ │ │ ├── chat_bubble_image.dart │ │ │ │ ├── chat_bubble_menu.dart │ │ │ │ └── chat_bubble_think_block.dart │ │ │ ├── chat_empty.dart │ │ │ ├── chat_error.dart │ │ │ ├── chat_list_view.dart │ │ │ ├── chat_select_model_button.dart │ │ │ ├── chat_text_field.dart │ │ │ ├── chat_welcome.dart │ │ │ └── subwidgets.dart │ │ ├── main_page.dart │ │ └── settings_page/ │ │ ├── settings_page.dart │ │ └── subwidgets/ │ │ ├── reins_settings.dart │ │ ├── server_settings.dart │ │ ├── subwidgets.dart │ │ └── themes_settings.dart │ ├── Providers/ │ │ └── chat_provider.dart │ ├── Services/ │ │ ├── database_service.dart │ │ ├── image_service.dart │ │ ├── ollama_service.dart │ │ ├── permission_service.dart │ │ └── services.dart │ ├── Utils/ │ │ ├── border_painter.dart │ │ ├── http_error_formatter.dart │ │ ├── material_color_adapter.dart │ │ ├── observe_size.dart │ │ ├── request_review_helper.dart │ │ └── retained_position_scroll_physics.dart │ ├── Widgets/ │ │ ├── chat_app_bar.dart │ │ ├── chat_configure_bottom_sheet.dart │ │ ├── chat_drawer.dart │ │ ├── chat_image.dart │ │ ├── flexible_text.dart │ │ ├── model_selection_bottom_sheet.dart │ │ ├── ollama_bottom_sheet_header.dart │ │ └── title_divider.dart │ └── main.dart ├── linux/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flatpak/ │ │ ├── dev.ibrahimcetin.reins.desktop │ │ └── dev.ibrahimcetin.reins.metainfo.xml │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ └── runner/ │ ├── CMakeLists.txt │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Configs/ │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── RunnerTests/ │ └── RunnerTests.swift ├── pubspec.yaml ├── test/ │ ├── api_create_request_test.dart │ ├── assets/ │ │ └── settings.hive │ ├── chat_page_view_model_test.dart │ ├── database_service_test.dart │ └── ollama_service_test.dart ├── web/ │ ├── index.html │ └── manifest.json └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake └── runner/ ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "8495dee1fd4aacbe9de707e7581203232f591b2f" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f - platform: android create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f - platform: ios create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f - platform: linux create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f - platform: macos create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f - platform: web create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f - platform: windows create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "reins", "request": "launch", "type": "dart" }, { "name": "reins (profile mode)", "request": "launch", "type": "dart", "flutterMode": "profile" }, { "name": "reins (release mode)", "request": "launch", "type": "dart", "flutterMode": "release" } ] } ================================================ FILE: .vscode/settings.json ================================================ { "cmake.ignoreCMakeListsMissing": true, "cSpell.words": [ "Ollama" ] } ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: PRIVACY ================================================ Reins app does not track any activity or actions of their users. No private information is being collected by the app. ================================================ FILE: README.md ================================================ # Reins Reins is a multi-platform, open-source, privacy-first app designed for Ollama users. **It simplifies chat configurations** with a user-friendly interface to configure system prompts, change the chat model, and adjust options for each conversation **individually**. Reins ensures a smooth, customizable experience for anyone working with self-hosted LLMs. If you like the project, don't forget to give a ⭐️! Download on the App Store Get it on Flathub You can download it for iOS and macOS on the App Store and for Linux on Flathub. You can find Android and Windows releases from [here](https://github.com/ibrahimcetin/reins/releases). ## Key Features - **Customizable Chat Configurations**: Configure system prompt, model, and options (e.g., temperature, seed, context size, max tokens) for each conversation. - **Model Selection & Switching**: Change the model of the current chat without interruption. - **Message Editing & Regeneration**: Edit and regenerate messages - **Save Custom Models**: Save system and chat prompts as new models. - **Image Integration**: Send and receive images within chats. - **Multiple Chat Management**: Easily manage and switch between multiple conversations. - **Real-Time Message Streaming**: Get messages instantly as they arrive. ## Mobile Screenshots Main Configuration Advanced Configurations Edit & Regenerate Change Current Chat Model Select Model Dark Theme ## Large Screen Screenshots Main Configuration ## Contributing Contributions are welcome! Feel free to fork the repository, make changes, and submit a pull request. ## License Reins is licensed under the GPL-3.0. ================================================ FILE: analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at https://dart.dev/lints. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options formatter: page_width: 120 ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks **/.cxx ================================================ FILE: android/app/build.gradle ================================================ plugins { id "com.android.application" id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" } def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } android { namespace = "dev.ibrahimcetin.reins" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "dev.ibrahimcetin.reins" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName } signingConfigs { release { keyAlias = keystoreProperties['keyAlias'] keyPassword = keystoreProperties['keyPassword'] storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword = keystoreProperties['storePassword'] } } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.release } } } flutter { source = "../.." } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/dev/ibrahimcetin/reins/MainActivity.kt ================================================ package dev.ibrahimcetin.reins import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-night/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-night-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night-v31/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-v31/styles.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle ================================================ allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true ================================================ FILE: android/settings.gradle ================================================ pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath }() includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.7.0" apply false id "org.jetbrains.kotlin.android" version "2.3.10" apply false } include ":app" ================================================ FILE: devtools_options.yaml ================================================ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: ================================================ FILE: ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 13.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.photos 'PERMISSION_PHOTOS=1', ] end end end ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import Flutter import UIKit @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "reins.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "reins-dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "reins-dark 1.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json ================================================ { "images" : [ { "filename" : "background.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "darkbackground.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "filename" : "LaunchImage.png", "idiom" : "universal", "scale" : "1x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "LaunchImage@2x.png", "idiom" : "universal", "scale" : "2x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "LaunchImage@3x.png", "idiom" : "universal", "scale" : "3x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. 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. ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Reins CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName reins CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS NSPhotoLibraryUsageDescription Reins needs access to your photo library to let you select images to send in chats. UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIStatusBarHidden UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; D519756EA826A2976D5FA068 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BD3A7DD18536256343BFFDC /* Pods_RunnerTests.framework */; }; F1FA0DD01D618F4B1250B9A3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56D5B87EEA532CCA2F863866 /* Pods_Runner.framework */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0A807142E3816A90C2C1F3D4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 0C36C37B8FE2F882B7F66CF5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 27B7B2D0B1593B1F4FB15290 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 56D5B87EEA532CCA2F863866 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7E0760B186B5CD533FADA873 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 8BD3A7DD18536256343BFFDC /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B457AD5CBA9FD11ED3A21575 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; FA4D4BD68716F904BFDE79C0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 172BA7F1156A620486622267 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D519756EA826A2976D5FA068 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, F1FA0DD01D618F4B1250B9A3 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 864C3237747B13B08BB74FFF /* Pods */ = { isa = PBXGroup; children = ( 27B7B2D0B1593B1F4FB15290 /* Pods-Runner.debug.xcconfig */, FA4D4BD68716F904BFDE79C0 /* Pods-Runner.release.xcconfig */, 0C36C37B8FE2F882B7F66CF5 /* Pods-Runner.profile.xcconfig */, B457AD5CBA9FD11ED3A21575 /* Pods-RunnerTests.debug.xcconfig */, 7E0760B186B5CD533FADA873 /* Pods-RunnerTests.release.xcconfig */, 0A807142E3816A90C2C1F3D4 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 864C3237747B13B08BB74FFF /* Pods */, A531F8A5C015E45D758173D1 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; A531F8A5C015E45D758173D1 /* Frameworks */ = { isa = PBXGroup; children = ( 56D5B87EEA532CCA2F863866 /* Pods_Runner.framework */, 8BD3A7DD18536256343BFFDC /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 2AA46EDF392C6009BDC73374 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 172BA7F1156A620486622267 /* Frameworks */, ); buildRules = ( ); dependencies = ( 331C8086294A63A400263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { packageProductDependencies = ( 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 580E76C33CE6BEC0CAC15957 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 8120DB1D8C623988F45DD411 /* [CP] Embed Pods Frameworks */, 1959A3E64A568FEB8F91481F /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { packageReferences = ( 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 1959A3E64A568FEB8F91481F /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 2AA46EDF392C6009BDC73374 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 580E76C33CE6BEC0CAC15957 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 8120DB1D8C623988F45DD411 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 9HQV48CK77; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Reins; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = B457AD5CBA9FD11ED3A21575 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7E0760B186B5CD533FADA873 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 0A807142E3816A90C2C1F3D4 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 9HQV48CK77; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Reins; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins.debug; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 9HQV48CK77; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Reins; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; productName = FlutterGeneratedPluginSwiftPackage; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: lib/Constants/app_constants.dart ================================================ class AppConstants { static const String appName = 'Reins'; static const String appIconPng = 'assets/images/reins.png'; static const String appIconSvg = 'assets/images/reins.svg'; static const String ollamaIconPng = 'assets/images/ollama.png'; } class NotificationNames { static const String generationBegin = "generation_begin_notification"; } ================================================ FILE: lib/Constants/chat_presets.dart ================================================ import 'package:reins/Models/chat_preset.dart'; class ChatPresets { static final List chatPresets = List.unmodifiable([ ChatPreset( title: "Brainstorm ideas", subtitle: "to spark creativity and solve challenges", prompt: "Can you help brainstorm 10 innovative ideas for a startup focusing on renewable energy and technology?"), ChatPreset( title: "Plan a trip", subtitle: "with a detailed itinerary and suggestions", prompt: "I want to plan a 5-day trip to Paris, France, including must-visit attractions, restaurants, and transportation tips."), ChatPreset( title: "Learn a new skill", subtitle: "with a structured step-by-step plan", prompt: "Help me create a 3-month learning plan to master digital painting as a beginner."), ChatPreset( title: "Write a story", subtitle: "that captivates readers with vivid details", prompt: "Create a short science fiction story about a world where AI governs all aspects of daily life."), ChatPreset( title: "Design a workout plan", subtitle: "to meet fitness goals effectively", prompt: "I want a 4-week workout plan for building muscle and improving endurance. Include exercises, reps, and rest days."), ChatPreset( title: "Cook a special dish", subtitle: "with a simple recipe and clear steps", prompt: "Give me a detailed recipe for making homemade lasagna, including all the ingredients and cooking instructions."), ChatPreset( title: "Explain a concept", subtitle: "in a way that's easy to understand", prompt: "Explain the concept of blockchain technology as if you were teaching a 10-year-old."), ChatPreset( title: "Solve a problem", subtitle: "with practical and actionable advice", prompt: "I am struggling with procrastination. Can you provide strategies to overcome it and improve productivity?"), ChatPreset( title: "Prepare for an interview", subtitle: "with common questions and tips", prompt: "Help me prepare for a software engineering interview, including potential coding challenges and behavioral questions."), ChatPreset( title: "Improve writing", subtitle: "with tips to refine and enhance text", prompt: "I have a draft of an email to my professor. Can you help me improve its tone and clarity?"), ChatPreset( title: "Learn about a topic", subtitle: "with clear and concise information", prompt: "Teach me about the basics of artificial intelligence and its applications in daily life."), ChatPreset( title: "Generate a table", subtitle: "to organize information neatly", prompt: "Create a comparison table for three smartphones: iPhone 15, Samsung Galaxy S23, and Google Pixel 8. Include columns for features, price, and battery life."), ChatPreset( title: "Create a meal plan", subtitle: "tailored to specific dietary needs", prompt: "I need a 5-day vegan meal plan that includes breakfast, lunch, and dinner. Please make it nutritious and easy to prepare."), ChatPreset( title: "Find inspiration", subtitle: "to tackle creative blocks", prompt: "Give me 5 unique ideas for a blog post about sustainable living and eco-friendly habits."), ChatPreset( title: "Analyze data", subtitle: "to extract insights and trends", prompt: "Given the following data about monthly sales, help identify trends and provide suggestions for improvement."), ChatPreset( title: "Write a poem", subtitle: "that captures emotion and depth", prompt: "Write a romantic poem about finding love in an unexpected place."), ChatPreset( title: "Generate a list", subtitle: "of items related to a topic", prompt: "Give me a list of 10 books to read for improving public speaking skills."), ChatPreset( title: "Craft a speech", subtitle: "for a special occasion", prompt: "Help me write a heartfelt speech for my best friend's wedding. Include memories and well wishes."), ChatPreset( title: "Design a budget", subtitle: "to manage expenses effectively", prompt: "I earn \$3,000 per month. Help me create a monthly budget for savings, rent, groceries, and leisure."), ChatPreset( title: "Learn a language", subtitle: "with basic phrases and tips", prompt: "Teach me 10 essential phrases in Spanish for traveling to Spain."), ChatPreset( title: "Plan an event", subtitle: "with all the necessary details", prompt: "I want to organize a surprise birthday party for my partner. Can you help with a plan, including themes, locations, and activities?"), ChatPreset( title: "Write a cover letter", subtitle: "that highlights your strengths", prompt: "Help me write a cover letter for a software engineering job application. Highlight my coding skills and project experience."), ChatPreset( title: "Create a list", subtitle: "of fun activities to try", prompt: "Suggest 10 fun weekend activities for a group of friends in the city."), ChatPreset( title: "Summarize a book", subtitle: "into key points and takeaways", prompt: "Provide a summary of the book 'Atomic Habits' by James Clear, focusing on its key principles."), ChatPreset( title: "Plan a garden", subtitle: "with layout and plant ideas", prompt: "I have a small backyard. Can you suggest a garden layout with plants suitable for a beginner?"), ChatPreset( title: "Write a review", subtitle: "to provide clear and honest feedback", prompt: "Help me write a review for the latest Marvel movie. Include both strengths and areas for improvement."), ChatPreset( title: "Solve a puzzle", subtitle: "to test your problem-solving skills", prompt: "Can you help me solve this Sudoku puzzle? Here's the current grid: [provide grid]."), ChatPreset( title: "Get travel tips", subtitle: "for a destination", prompt: "I am visiting Rome, Italy, next month. Can you suggest travel tips, including local customs and transportation?"), ChatPreset( title: "Discover hidden gems", subtitle: "to explore unique destinations", prompt: "Suggest 5 hidden gem locations in Tokyo that tourists often miss, including why they are worth visiting."), ChatPreset( title: "Plan a study schedule", subtitle: "to prepare effectively for exams", prompt: "Help me create a 2-week study plan for my upcoming biology exam, covering key topics and including breaks."), ChatPreset( title: "Understand a process", subtitle: "with step-by-step guidance", prompt: "Explain the process of photosynthesis in plants step by step in a simple and concise way."), ChatPreset( title: "Improve productivity", subtitle: "with actionable strategies", prompt: "What are some effective time management techniques for balancing work, study, and personal life?"), ChatPreset( title: "Get relationship advice", subtitle: "to navigate challenges effectively", prompt: "I had a misunderstanding with my friend. How can I approach them to resolve the issue and rebuild trust?"), ChatPreset( title: "Explore career options", subtitle: "aligned with skills and interests", prompt: "I am skilled in graphic design and love storytelling. What are some potential career paths for me?"), ChatPreset( title: "Generate a packing list", subtitle: "tailored to your trip destination", prompt: "I'm going on a 10-day trip to Iceland in winter. What should I pack to stay comfortable and prepared?"), ChatPreset( title: "Get gift ideas", subtitle: "for any occasion", prompt: "I'm looking for a birthday gift for my 10-year-old nephew who loves science and puzzles. Can you suggest some ideas?"), ChatPreset( title: "Create a meditation guide", subtitle: "for relaxation and mindfulness", prompt: "Provide a 10-minute guided meditation script to help reduce stress and increase focus."), ChatPreset( title: "Learn a fun fact", subtitle: "to surprise and engage others", prompt: "Tell me a fun and surprising fact about space exploration."), ChatPreset( title: "Host a trivia night", subtitle: "with engaging questions", prompt: "Create a set of 10 trivia questions about world history for a game night."), ChatPreset( title: "Plan a budget-friendly trip", subtitle: "without compromising on experiences", prompt: "How can I plan a 3-day trip to New York City on a budget of \$500? Include activities and food options."), ChatPreset( title: "Write a thank-you note", subtitle: "to express gratitude sincerely", prompt: "Help me draft a thank-you email to my mentor for their guidance during my internship."), ChatPreset( title: "Debunk a myth", subtitle: "with accurate information", prompt: "Is it true that cracking your knuckles causes arthritis? Provide evidence-based information."), ChatPreset( title: "Plan a creative project", subtitle: "from concept to execution", prompt: "I want to start a photography series capturing daily life in my city. Can you help me plan this project?"), ChatPreset( title: "Learn about famous landmarks", subtitle: "and their historical significance", prompt: "Tell me the story behind the construction of the Eiffel Tower and why it is iconic."), ChatPreset( title: "Get workout motivation", subtitle: "to achieve your fitness goals", prompt: "I've been struggling to stay consistent with workouts. Can you provide motivational tips and a simple routine to get back on track?"), ChatPreset( title: "Write a job description", subtitle: "for your team's new role", prompt: "Draft a job description for a junior web developer position, focusing on required skills and job responsibilities."), ChatPreset( title: "Generate a bucket list", subtitle: "for unforgettable experiences", prompt: "Help me create a bucket list of 20 unique activities to try before turning 40."), ChatPreset( title: "Design a menu", subtitle: "for an upcoming dinner party", prompt: "Can you suggest a three-course menu for a dinner party, including an appetizer, main course, and dessert?"), ChatPreset( title: "Write a persuasive letter", subtitle: "to convey your message effectively", prompt: "Help me write a letter to my local council advocating for better public transport services in our area."), ChatPreset( title: "Explore myths and legends", subtitle: "from around the world", prompt: "Share a famous myth or legend from Japanese folklore and its cultural significance."), ChatPreset( title: "Host a themed party", subtitle: "with creative ideas and details", prompt: "I want to organize a 'Roaring Twenties' themed party. Can you suggest decor, costumes, and activities?"), ChatPreset( title: "Write a business pitch", subtitle: "to attract investors", prompt: "Create a pitch for a new app that helps people track and reduce their carbon footprint."), ChatPreset( title: "Plan a charity event", subtitle: "to raise funds for a cause", prompt: "I want to organize a local fun run to raise funds for animal shelters. Can you provide a detailed plan?"), ChatPreset( title: "Teach a life skill", subtitle: "in an easy-to-follow way", prompt: "Explain how to tie a tie step by step, as if teaching a beginner."), ChatPreset( title: "Craft a bedtime story", subtitle: "to delight and calm children", prompt: "Write a short bedtime story about a magical tree that grants wishes to animals in the forest."), ChatPreset( title: "Plan a surprise date", subtitle: "with romantic ideas", prompt: "Suggest a creative and budget-friendly date idea for my partner this weekend."), // More presets can be added here following the same pattern ]); static List get randomPresets { final presetsCopy = List.of(chatPresets); presetsCopy.shuffle(); return presetsCopy.take(5).toList(); } } ================================================ FILE: lib/Constants/constants.dart ================================================ export 'app_constants.dart'; export 'path_manager.dart'; export 'chat_presets.dart'; export 'generate_title_constants.dart'; ================================================ FILE: lib/Constants/generate_title_constants.dart ================================================ class GenerateTitleConstants { static const String systemPrompt = "You are a title generator for a chat application. Your task is to create a concise and descriptive title based on the user's first message in a chat. The title should capture the main topic or intent of the message while being engaging and informative. Avoid generic titles like 'Chat' or 'Conversation.' Keep the title under 5 words. For example: If the first message is 'What's the weather like today in Paris?' the title should be 'Weather in Paris.' If the first message is 'Can you help me with my homework?' the title could be 'Homework Help.' If the first message is 'Tell me a story about a dragon,' the title could be 'Dragon Story Request.' Always focus on the essence of the message and aim for clarity and relevance."; static const String prompt = "You are a title generator for a chat application. Your task is to create a concise and descriptive title based on the user's first message in a chat. Respond with the title only—do not include any additional words, explanations, or phrases. The title should be no more than 5 words and capture the main topic of the user's message. For example: Input: 'What's the weather in Paris?' → Output: 'Weather in Paris' Input: 'Tell me a story about a dragon.' → Output: 'Dragon Story' Input: 'Plan a trip to Italy.' → Output: 'Italy Trip Plan'. Now generate a title for this message: "; } ================================================ FILE: lib/Constants/path_manager.dart ================================================ import 'dart:io'; import 'package:path_provider/path_provider.dart'; class PathManager { static final PathManager _instance = PathManager._internal(); late final Directory documentsDirectory; PathManager._internal(); static Future initialize() async { if (Platform.isLinux) { final directory = await getApplicationSupportDirectory(); _instance.documentsDirectory = directory; } else { final directory = await getApplicationDocumentsDirectory(); _instance.documentsDirectory = directory; } } static PathManager get instance => _instance; } ================================================ FILE: lib/Extensions/markdown_stylesheet_extension.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; /// Extension on [BuildContext] to provide consistent markdown styling across the app. extension MarkdownStyleSheetExtension on BuildContext { /// Returns a [MarkdownStyleSheet] that matches the app's theme with bodyLarge text size. /// /// This ensures markdown content uses the same base size as other readable text /// in the app, while respecting user accessibility settings up to 2x scale. MarkdownStyleSheet get markdownStyleSheet { return MarkdownStyleSheet.fromTheme( Theme.of(this).copyWith( textTheme: Theme.of(this).textTheme.copyWith( bodyMedium: Theme.of(this).textTheme.bodyLarge, ), ), ).copyWith( textScaler: MediaQuery.textScalerOf(this).clamp( minScaleFactor: 0.8, maxScaleFactor: 2.0, ), ); } } ================================================ FILE: lib/Models/api/create_request.dart ================================================ import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_message.dart'; /// Request body for POST /api/create /// /// Creates a model from another model with optional system prompt, /// parameters, and message history. class ApiCreateRequest { /// Name of the model to create. final String model; /// Name of an existing model to create the new model from. final String from; /// A system prompt for the model. final String? system; /// A dictionary of parameters for the model. final Map? parameters; /// A list of message objects used to seed the conversation. final List? messages; /// If `false` the response will be returned as a single response object, /// rather than a stream of objects. final bool stream; ApiCreateRequest({ required this.model, required this.from, this.system, this.parameters, this.messages, this.stream = false, }); /// Constructs an [ApiCreateRequest] from an [OllamaChat] and optional messages. /// /// Only non-default parameters are included in the request to avoid /// overriding the base model's defaults unnecessarily. factory ApiCreateRequest.fromChat( String model, { required OllamaChat chat, List? messages, }) { final defaultOptions = OllamaChatOptions().toMap(); final chatOptions = chat.options.toMap(); // Only include parameters that differ from the defaults. final nonDefaultParameters = {}; chatOptions.forEach((key, value) { if (defaultOptions[key] != value) { nonDefaultParameters[key] = value; } }); return ApiCreateRequest( model: model, from: chat.model, system: chat.systemPrompt, parameters: nonDefaultParameters.isNotEmpty ? nonDefaultParameters : null, messages: messages != null && messages.isNotEmpty ? messages : null, ); } Future> toJson() async { return { 'model': model, 'from': from, if (system != null && system!.isNotEmpty) 'system': system, if (parameters != null) 'parameters': parameters, if (messages != null) 'messages': await Future.wait(messages!.map((m) => m.toChatJson())), 'stream': stream, }; } } ================================================ FILE: lib/Models/api/show_response.dart ================================================ /// Response from POST /api/show class ApiShowResponse { final String modelfile; final String parameters; final String template; final ApiShowModelDetails details; final Map modelInfo; final List capabilities; ApiShowResponse({ required this.modelfile, required this.parameters, required this.template, required this.details, required this.modelInfo, required this.capabilities, }); factory ApiShowResponse.fromJson(Map json) { return ApiShowResponse( modelfile: json['modelfile'] ?? '', parameters: json['parameters'] ?? '', template: json['template'] ?? '', details: ApiShowModelDetails.fromJson(json['details'] ?? {}), modelInfo: json['model_info'] ?? {}, capabilities: json['capabilities'] != null ? List.from(json['capabilities']) : [], ); } } /// Model details from /api/show response class ApiShowModelDetails { final String parentModel; final String format; final String family; final List? families; final String parameterSize; final String quantizationLevel; ApiShowModelDetails({ required this.parentModel, required this.format, required this.family, this.families, required this.parameterSize, required this.quantizationLevel, }); factory ApiShowModelDetails.fromJson(Map json) { return ApiShowModelDetails( parentModel: json['parent_model'] ?? '', format: json['format'] ?? '', family: json['family'] ?? '', families: json['families'] != null ? List.from(json['families']) : null, parameterSize: json['parameter_size'] ?? '', quantizationLevel: json['quantization_level'] ?? '', ); } } ================================================ FILE: lib/Models/api/tags_response.dart ================================================ /// Response from GET /api/tags class ApiTagsResponse { final List models; ApiTagsResponse({required this.models}); factory ApiTagsResponse.fromJson(Map json) { return ApiTagsResponse( models: (json['models'] as List).map((m) => ApiTagsModel.fromJson(m)).toList(), ); } } /// Individual model from /api/tags response class ApiTagsModel { final String name; final String model; final DateTime modifiedAt; final int size; final String digest; final ApiTagsModelDetails details; ApiTagsModel({ required this.name, required this.model, required this.modifiedAt, required this.size, required this.digest, required this.details, }); factory ApiTagsModel.fromJson(Map json) { return ApiTagsModel( name: json['name'], model: json['model'], modifiedAt: DateTime.parse(json['modified_at']), size: json['size'], digest: json['digest'], details: ApiTagsModelDetails.fromJson(json['details']), ); } } /// Model details from /api/tags response class ApiTagsModelDetails { final String parentModel; final String format; final String family; final List? families; final String parameterSize; final String quantizationLevel; ApiTagsModelDetails({ required this.parentModel, required this.format, required this.family, this.families, required this.parameterSize, required this.quantizationLevel, }); factory ApiTagsModelDetails.fromJson(Map json) { return ApiTagsModelDetails( parentModel: json['parent_model'] ?? '', format: json['format'] ?? '', family: json['family'] ?? '', families: json['families'] != null ? List.from(json['families']) : null, parameterSize: json['parameter_size'] ?? '', quantizationLevel: json['quantization_level'] ?? '', ); } } ================================================ FILE: lib/Models/chat_configure_arguments.dart ================================================ import 'package:reins/Models/ollama_chat.dart'; class ChatConfigureArguments { String? systemPrompt; OllamaChatOptions chatOptions; ChatConfigureArguments({ required this.systemPrompt, required this.chatOptions, }); static get defaultArguments => ChatConfigureArguments( systemPrompt: null, chatOptions: OllamaChatOptions(), ); } ================================================ FILE: lib/Models/chat_preset.dart ================================================ class ChatPreset { final String title; final String subtitle; final String prompt; ChatPreset({ required this.title, required this.subtitle, required this.prompt, }); } ================================================ FILE: lib/Models/model_capabilities.dart ================================================ /// Model capabilities extracted from /api/show response class ModelCapabilities { final bool completion; final bool vision; final bool tools; final bool embedding; final bool thinking; const ModelCapabilities({ this.completion = false, this.vision = false, this.tools = false, this.embedding = false, this.thinking = false, }); /// Creates capabilities from the raw capabilities list from /api/show factory ModelCapabilities.fromList(List capabilities) { return ModelCapabilities( completion: capabilities.contains('completion'), vision: capabilities.contains('vision'), tools: capabilities.contains('tools'), embedding: capabilities.contains('embedding'), thinking: capabilities.contains('thinking'), ); } @override String toString() { final caps = []; if (completion) caps.add('completion'); if (vision) caps.add('vision'); if (tools) caps.add('tools'); if (embedding) caps.add('embedding'); if (thinking) caps.add('thinking'); return 'ModelCapabilities(${caps.join(', ')})'; } } ================================================ FILE: lib/Models/ollama_chat.dart ================================================ import 'dart:convert'; import 'package:uuid/uuid.dart'; class OllamaChat { final String id; final String model; final String title; final String? systemPrompt; final OllamaChatOptions options; OllamaChat({ String? id, required this.model, String? title, this.systemPrompt, OllamaChatOptions? options, }) : id = id ?? Uuid().v4(), title = title ?? 'New Chat', options = options ?? OllamaChatOptions(); factory OllamaChat.fromMap(Map map) { return OllamaChat( id: map['chat_id'], model: map['model'], title: map['chat_title'], systemPrompt: map['system_prompt'], options: map['options'] != null ? OllamaChatOptions.fromJson(map['options']) : null, ); } } /// Represents configuration options for controlling the behavior of the Ollama chat model. class OllamaChatOptions { /// Enables Mirostat sampling for controlling perplexity. /// 0 = disabled, 1 = Mirostat, 2 = Mirostat 2.0. int mirostat; /// Influences how quickly the algorithm responds to feedback from the generated text. /// A lower value results in slower adjustments; a higher value makes the algorithm more responsive. double mirostatEta; /// Controls the balance between coherence and diversity of the output. /// A lower value results in more focused and coherent text. double mirostatTau; /// Sets the size of the context window used to generate the next token. int contextSize; /// Sets how far back the model looks to prevent repetition. /// 0 = disabled, -1 = full context size. int repeatLastN; /// Sets the strength of penalizing repetitions. /// A higher value (e.g., 1.5) penalizes repetitions more strongly. double repeatPenalty; /// Controls the temperature of the model. /// Higher values result in more creative outputs, lower values in more deterministic outputs. double temperature; /// Sets the random seed for text generation. /// A specific value ensures the same text is generated for the same input. int seed; /// Controls tail-free sampling to reduce the impact of less probable tokens. /// 1.0 disables this setting; higher values reduce the impact more. double tailFreeSampling; /// Sets the maximum number of tokens to predict during text generation. /// -1 = infinite generation. int maxTokens; /// Limits the probability of generating nonsense. /// A higher value (e.g., 100) allows more diverse answers, while a lower value (e.g., 10) is more conservative. int topK; /// Works with topK to control text diversity. /// Higher values lead to more diverse text, lower values to more focused text. double topP; /// Ensures a balance of quality and variety by setting a minimum token probability relative to the most likely token. /// Tokens with lower probability are filtered out. double minP; /// Creates an instance of [OllamaChatOptions] with default values. OllamaChatOptions({ int? mirostat, double? mirostatEta, double? mirostatTau, int? contextSize, int? repeatLastN, double? repeatPenalty, double? temperature, int? seed, double? tailFreeSampling, int? maxTokens, int? topK, double? topP, double? minP, }) : mirostat = mirostat ?? 0, mirostatEta = mirostatEta ?? 0.1, mirostatTau = mirostatTau ?? 5.0, contextSize = contextSize ?? 2048, repeatLastN = repeatLastN ?? 64, repeatPenalty = repeatPenalty ?? 1.1, temperature = temperature ?? 0.8, seed = seed ?? 0, tailFreeSampling = tailFreeSampling ?? 1.0, maxTokens = maxTokens ?? -1, topK = topK ?? 40, topP = topP ?? 0.9, minP = minP ?? 0.0; /// Factory method for creating an instance of [OllamaChatOptions] from a map. factory OllamaChatOptions.fromMap(Map map) { return OllamaChatOptions( mirostat: map['mirostat'], mirostatEta: map['mirostat_eta']?.toDouble(), mirostatTau: map['mirostat_tau']?.toDouble(), contextSize: map['num_ctx'], repeatLastN: map['repeat_last_n'], repeatPenalty: map['repeat_penalty']?.toDouble(), temperature: map['temperature']?.toDouble(), seed: map['seed'], tailFreeSampling: map['tfs_z']?.toDouble(), maxTokens: map['num_predict'], topK: map['top_k'], topP: map['top_p']?.toDouble(), minP: map['min_p']?.toDouble(), ); } /// Factory method for creating an instance of [OllamaChatOptions] from a JSON string. factory OllamaChatOptions.fromJson(String json) { return OllamaChatOptions.fromMap(jsonDecode(json)); } /// Converts the instance of [OllamaChatOptions] to a map. Map toMap() { return { 'mirostat': mirostat, 'mirostat_eta': mirostatEta, 'mirostat_tau': mirostatTau, 'num_ctx': contextSize, 'repeat_last_n': repeatLastN, 'repeat_penalty': repeatPenalty, 'temperature': temperature, 'seed': seed, 'tfs_z': tailFreeSampling, if (maxTokens > 0) 'num_predict': maxTokens, 'top_k': topK, 'top_p': topP, 'min_p': minP, }; } /// Converts the instance of [OllamaChatOptions] to a JSON string. String toJson() { return jsonEncode(toMap()); } } ================================================ FILE: lib/Models/ollama_exception.dart ================================================ class OllamaException implements Exception { final String message; OllamaException(this.message); @override String toString() { return message; } } ================================================ FILE: lib/Models/ollama_message.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; import 'package:reins/Constants/constants.dart'; import 'package:uuid/uuid.dart'; class OllamaMessage { /// The unique identifier of the message. String id; /// The text content of the message. String content; /// The image content of the message. List? images; /// The date and time the message was created. DateTime createdAt; /// The role of the message. OllamaMessageRole role; /// The model used to generate the message. String? model; // Metadata fields bool? done; String? doneReason; List? context; int? totalDuration; int? loadDuration; int? promptEvalCount; int? promptEvalDuration; int? evalCount; int? evalDuration; OllamaMessage( this.content, { String? id, required this.role, this.images, DateTime? createdAt, this.model, this.done, this.doneReason, this.context, this.totalDuration, this.loadDuration, this.promptEvalCount, this.promptEvalDuration, this.evalCount, this.evalDuration, }) : id = id ?? Uuid().v4(), createdAt = createdAt ?? DateTime.now(); factory OllamaMessage.fromJson(Map json) => OllamaMessage( json["message"] != null ? json["message"]["content"] // For chat messages : json["response"], // For generated messages role: json["message"] != null ? OllamaMessageRole.fromString(json["message"]["role"]) : OllamaMessageRole.assistant, // For generated messages (default) images: null, // TODO: Implement image support createdAt: DateTime.parse(json["created_at"]), model: json["model"], // Metadata fields done: json["done"], doneReason: json["done_reason"], context: json["context"] != null ? List.from(json["context"].map((x) => x)) : null, totalDuration: json["total_duration"], loadDuration: json["load_duration"], promptEvalCount: json["prompt_eval_count"], promptEvalDuration: json["prompt_eval_duration"], evalCount: json["eval_count"], evalDuration: json["eval_duration"], ); factory OllamaMessage.fromDatabase(Map map) { return OllamaMessage( map['content'], id: map['message_id'], role: OllamaMessageRole.fromString(map['role']), images: _constructImages(map['images']), createdAt: DateTime.fromMillisecondsSinceEpoch(map['timestamp']), model: map['model'], ); } Future> toJson() async => { "model": model, "created_at": createdAt.toIso8601String(), "message": { "role": role.name, "content": content, "images": await _base64EncodeImages(), }, "done": done, "done_reason": doneReason, "context": context == null ? null : List.from(context!.map((x) => x)), "total_duration": totalDuration, "load_duration": loadDuration, "prompt_eval_count": promptEvalCount, "prompt_eval_duration": promptEvalDuration, "eval_count": evalCount, "eval_duration": evalDuration, }; Future> toChatJson() async => { "role": role.name, "content": content, "images": await _base64EncodeImages(), }; Map toDatabaseMap() => { 'message_id': id, 'content': content, 'images': _breakImages(images), 'role': role.name, 'timestamp': createdAt.millisecondsSinceEpoch, }; void updateMetadataFrom(OllamaMessage message) { done = message.done; doneReason = message.doneReason; context = message.context; totalDuration = message.totalDuration; loadDuration = message.loadDuration; promptEvalCount = message.promptEvalCount; promptEvalDuration = message.promptEvalDuration; evalCount = message.evalCount; evalDuration = message.evalDuration; } Future?> _base64EncodeImages() async { if (images != null) { return await Future.wait(images!.map( (file) async => base64Encode(await file.readAsBytes()), )); } return null; } static List? _constructImages(String? raw) { if (raw != null) { final List decoded = jsonDecode(raw); return decoded.map((imageRelativePath) { return File(path.join( PathManager.instance.documentsDirectory.path, imageRelativePath, )); }).toList(); } return null; } String? _breakImages(List? images) { if (images != null) { final relativePathImages = images.map((file) { return path.relative( file.path, from: PathManager.instance.documentsDirectory.path, ); }).toList(); return jsonEncode(relativePathImages); } return null; } } enum OllamaMessageRole { user, assistant, system; factory OllamaMessageRole.fromString(String role) { switch (role) { case 'user': return OllamaMessageRole.user; case 'assistant': return OllamaMessageRole.assistant; case 'system': return OllamaMessageRole.system; default: throw ArgumentError('Unknown role: $role'); } } } ================================================ FILE: lib/Models/ollama_model.dart ================================================ import 'package:reins/Models/api/tags_response.dart'; import 'package:reins/Models/api/show_response.dart'; import 'package:reins/Models/model_capabilities.dart'; /// Domain model representing an Ollama model. /// Combines data from /api/tags and optionally /api/show. class OllamaModel { final String name; final String model; final DateTime modifiedAt; final int size; final String digest; final String parameterSize; final ModelCapabilities? capabilities; OllamaModel({ required this.name, required this.model, required this.modifiedAt, required this.size, required this.digest, required this.parameterSize, this.capabilities, }); /// Creates an OllamaModel from /api/tags and optional /api/show response factory OllamaModel.from(ApiTagsModel tagsModel, ApiShowResponse? showResponse) { return OllamaModel( name: tagsModel.name, model: tagsModel.model, modifiedAt: tagsModel.modifiedAt, size: tagsModel.size, digest: tagsModel.digest, parameterSize: tagsModel.details.parameterSize, capabilities: showResponse != null ? ModelCapabilities.fromList(showResponse.capabilities) : null, ); } /// For backward compatibility with existing JSON serialization factory OllamaModel.fromJson(Map json) => OllamaModel( name: json["name"], model: json["model"], modifiedAt: DateTime.parse(json["modified_at"]), size: json["size"], digest: json["digest"], parameterSize: json["details"]["parameter_size"] ?? '', capabilities: null, ); Map toJson() => { "name": name, "model": model, "modified_at": modifiedAt.toIso8601String(), "size": size, "digest": digest, "parameter_size": parameterSize, }; @override String toString() { return name; } @override int get hashCode => digest.hashCode; @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is OllamaModel && other.digest == digest; } } ================================================ FILE: lib/Models/ollama_request_state.dart ================================================ enum OllamaRequestState { error, loading, success, uninitialized, } ================================================ FILE: lib/Models/settings_route_arguments.dart ================================================ class SettingsRouteArguments { final bool autoFocusServerAddress; SettingsRouteArguments({required this.autoFocusServerAddress}); } ================================================ FILE: lib/Pages/chat_page/chat_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:reins/Widgets/chat_app_bar.dart'; import 'package:reins/Widgets/model_selection_bottom_sheet.dart'; import 'chat_page_view_model.dart'; import 'subwidgets/subwidgets.dart'; class ChatPage extends StatefulWidget { const ChatPage({super.key}); @override State createState() => _ChatPageState(); } class _ChatPageState extends State { // ViewModel reference late final ChatPageViewModel _viewModel; // Welcome screen animation state var _crossFadeState = CrossFadeState.showFirst; double _scale = 1.0; @override void initState() { super.initState(); _viewModel = context.read(); } @override Widget build(BuildContext context) { // Subscribe to ViewModel changes context.watch(); return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (!ResponsiveBreakpoints.of(context).isMobile) ChatAppBar(), // If the screen is large, show the app bar Expanded( child: Stack( alignment: Alignment.bottomLeft, children: [ _buildChatBody(), _buildChatFooter(), ], ), ), // TODO: Wrap with ConstrainedBox to limit the height Padding( padding: const EdgeInsets.all(8.0), child: ChatTextField( key: ValueKey(_viewModel.currentChat?.id), controller: _viewModel.textFieldController, onEditingComplete: _sendMessage, prefixIcon: IconButton( icon: Icon(Icons.add), onPressed: _handleAttachmentButton, ), suffixIcon: _buildTextFieldSuffixIcon(), ), ), ], ); } Widget _buildChatBody() { if (_viewModel.messages.isEmpty) { if (_viewModel.currentChat == null) { if (!_viewModel.isServerConfigured) { return ChatEmpty( child: ChatWelcome( showingState: _crossFadeState, onFirstChildFinished: () => setState(() => _crossFadeState = CrossFadeState.showSecond), secondChildScale: _scale, onSecondChildScaleEnd: () => setState(() => _scale = 1.0), ), ); } else { return ChatEmpty( child: ChatSelectModelButton( currentModelName: _viewModel.selectedModel?.name, onPressed: _showModelSelectionBottomSheet, ), ); } } else { return ChatEmpty( child: Text('No messages yet!'), ); } } else { return ChatListView( key: PageStorageKey(_viewModel.currentChat?.id ?? 'empty'), messages: _viewModel.messages, isAwaitingReply: _viewModel.isThinking, error: _viewModel.currentError != null ? ChatError( message: _viewModel.currentError!.message, onRetry: () => _viewModel.retryLastPrompt(), ) : null, bottomPadding: _viewModel.hasImageAttachments ? MediaQuery.of(context).size.height * 0.15 : null, // TODO: Calculate the height of attachments row ); } } Widget _buildChatFooter() { if (_viewModel.hasImageAttachments) { return ChatAttachmentRow( itemCount: _viewModel.imageFiles.length, itemBuilder: (context, index) { return ChatAttachmentImage( imageFile: _viewModel.imageFiles[index], onRemove: (imageFile) => _viewModel.removeImage(imageFile), ); }, ); } else if (_viewModel.messages.isEmpty) { return ChatAttachmentRow( itemCount: _viewModel.presets.length, itemBuilder: (context, index) { final preset = _viewModel.presets[index]; return ChatAttachmentPreset( preset: preset, onPressed: () async { _viewModel.setTextFieldValue(preset.prompt); await _sendMessage(); }, ); }, ); } else { return const SizedBox(); } } Widget? _buildTextFieldSuffixIcon() { if (_viewModel.isStreaming) { return IconButton( icon: const Icon(Icons.stop_rounded), color: Theme.of(context).colorScheme.onSurface, onPressed: _viewModel.cancelStreaming, ); } else if (_viewModel.hasText) { return IconButton( icon: const Icon(Icons.arrow_upward_rounded), color: Theme.of(context).colorScheme.onSurface, onPressed: _sendMessage, ); } else { return null; } } Future _sendMessage() async { await _viewModel.sendMessage( onModelSelectionRequired: _showModelSelectionBottomSheet, onServerNotConfigured: _onServerNotConfigured, ); } Future _showModelSelectionBottomSheet() async { final selectedModel = await showModelSelectionBottomSheet( context: context, title: "Select a Model", currentModelName: _viewModel.selectedModel?.name, ); if (selectedModel != null) { _viewModel.setSelectedModel(selectedModel); } } Future _handleAttachmentButton() async { await _viewModel.pickImages( onPermissionDenied: _showPhotosDeniedAlert, ); } void _onServerNotConfigured() { setState(() { _crossFadeState = CrossFadeState.showSecond; _scale = _scale == 1.0 ? 1.05 : 1.0; }); } Future _showPhotosDeniedAlert() async { await showDialog( context: context, builder: (_) { return AlertDialog( title: const Text('Photos Permission Denied'), content: const Text('Please allow access to photos in the settings.'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ); }, ); } } ================================================ FILE: lib/Pages/chat_page/chat_page_view_model.dart ================================================ import 'dart:async'; import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:image_picker/image_picker.dart'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Models/chat_preset.dart'; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_exception.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:reins/Models/ollama_model.dart'; import 'package:reins/Providers/chat_provider.dart'; import 'package:reins/Services/services.dart'; class ChatPageViewModel extends ChangeNotifier { final ChatProvider _chatProvider; final PermissionService _permissionService; final ImageService _imageService; ChatPageViewModel({ required ChatProvider chatProvider, required PermissionService permissionService, required ImageService imageService, }) : _chatProvider = chatProvider, _permissionService = permissionService, _imageService = imageService { _initialize(); } // ============================================================ // Page State // ============================================================ /// The selected model for new chats OllamaModel? _selectedModel; OllamaModel? get selectedModel => _selectedModel; /// The list of chat presets List _presets = ChatPresets.randomPresets; List get presets => _presets; /// The text field controller final TextEditingController textFieldController = TextEditingController(); /// Whether the text field has text bool get hasText => textFieldController.text.trim().isNotEmpty; /// The app lifecycle listener for cleanup late final AppLifecycleListener _appLifecycleListener; /// The Hive settings subscription late final StreamSubscription _settingsSubscription; bool get isServerConfigured { return Hive.box('settings').get('serverAddress') != null; } // ============================================================ // Initialization // ============================================================ void _initialize() { // Listen to ChatProvider changes and forward notifications _chatProvider.addListener(_onChatProviderChanged); // Listen to text field changes to update UI (e.g., send button visibility) textFieldController.addListener(_onTextFieldChanged); // If the server address changes, reset the selected model _settingsSubscription = Hive.box('settings').watch(key: 'serverAddress').listen((event) { _selectedModel = null; notifyListeners(); }); // Listen for app exit to delete unused attached images _appLifecycleListener = AppLifecycleListener(onExitRequested: () async { await _imageService.deleteImages(imageFiles); return AppExitResponse.exit; }); } void _onChatProviderChanged() { notifyListeners(); } void _onTextFieldChanged() { notifyListeners(); } @override void dispose() { _chatProvider.removeListener(_onChatProviderChanged); textFieldController.removeListener(_onTextFieldChanged); textFieldController.dispose(); _appLifecycleListener.dispose(); _settingsSubscription.cancel(); super.dispose(); } // ============================================================ // ChatProvider State (Proxied) // ============================================================ /// The list of messages in the current chat List get messages => _chatProvider.messages; /// The current chat OllamaChat? get currentChat => _chatProvider.currentChat; /// Whether the current chat is streaming a response bool get isStreaming => _chatProvider.isCurrentChatStreaming; /// Whether the current chat is thinking (waiting for response) bool get isThinking => _chatProvider.isCurrentChatThinking; /// The current chat error, if any OllamaException? get currentError => _chatProvider.currentChatError; // ============================================================ // ChatProvider Actions (Delegated) // ============================================================ /// Cancels the current streaming response void cancelStreaming() { _chatProvider.cancelCurrentStreaming(); } /// Retries the last prompt Future retryLastPrompt() async { await _chatProvider.retryLastPrompt(); } /// Fetches available models from the server Future> fetchAvailableModels() async { return await _chatProvider.fetchAvailableModels(); } // ============================================================ // Model Selection // ============================================================ /// Sets the selected model void setSelectedModel(OllamaModel? model) { _selectedModel = model; notifyListeners(); } // ============================================================ // Text Field // ============================================================ /// Sets the text field value (e.g., for presets) void setTextFieldValue(String value) { textFieldController.text = value; } /// Gets and clears the text field value (for sending) String _takeTextFieldValue() { final value = textFieldController.text; textFieldController.clear(); return value; } // ============================================================ // Image Attachments // ============================================================ final List _imageFiles = []; /// The list of attached image files List get imageFiles => List.unmodifiable(_imageFiles); /// Whether there are any image attachments bool get hasImageAttachments => _imageFiles.isNotEmpty; /// Handles image picking and compression Future pickImages({ VoidCallback? onPermissionDenied, int quality = 10, }) async { // Check permissions final hasPermission = await _permissionService.requestPhotoPermission( onDenied: onPermissionDenied, ); if (!hasPermission) return; // Pick images final picker = ImagePicker(); final pickedImage = await picker.pickImage( source: ImageSource.gallery, ); // await _picker.pickMultiImage(limit: maxImages); if (pickedImage == null) return; // Compress and save final compressedFile = await _imageService.compressAndSave( pickedImage.path, quality: quality, ); // Add an empty path if the image could not be compressed to show error if (compressedFile != null) { _imageFiles.add(compressedFile); } else { _imageFiles.add(File('')); } notifyListeners(); } /// Deletes a single image and removes it from the list Future removeImage(File imageFile) async { await _imageService.deleteImage(imageFile); _imageFiles.remove(imageFile); notifyListeners(); } /// Gets and clears the current images (for sending) List _takeImages() { final images = _imageFiles.toList(); _imageFiles.clear(); return images; } // ============================================================ // Operations // ============================================================ /// Handles sending a message /// Returns true if the message was sent successfully Future sendMessage({ required Future Function() onModelSelectionRequired, required void Function() onServerNotConfigured, }) async { // Early return if nothing to send or currently streaming if (!hasText || isStreaming) { return false; } // Check if server is configured if (!isServerConfigured) { onServerNotConfigured(); return false; } // If no current chat, need to create one if (_chatProvider.currentChat == null) { // If no model selected, request selection if (_selectedModel == null) { await onModelSelectionRequired(); } // If still no model after selection, abort if (_selectedModel == null) { return false; } // Create a new chat with the selected model await _chatProvider.createNewChat(_selectedModel!); // Take the prompt and images and refresh the presets final prompt = _takeTextFieldValue(); final images = _takeImages(); _presets = ChatPresets.randomPresets; // Notify listeners notifyListeners(); // Send the prompt await _chatProvider.sendPrompt(prompt, images: images); // Generate title for the new chat await _chatProvider.generateTitleForCurrentChat(); } else { // Get and clear the prompt and images final prompt = _takeTextFieldValue(); final images = _takeImages(); // Notify listeners (text field is cleared) notifyListeners(); // Send the prompt await _chatProvider.sendPrompt(prompt, images: images); } return true; } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_attachment/chat_attachment_image.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:reins/Widgets/chat_image.dart'; class ChatAttachmentImage extends StatelessWidget { final File imageFile; final Function(File) onRemove; const ChatAttachmentImage({ super.key, required this.imageFile, required this.onRemove, }); @override Widget build(BuildContext context) { return Stack( children: [ ChatImage( image: FileImage(imageFile), height: MediaQuery.of(context).size.height * 0.15, ), Positioned( top: 2, right: 2, child: InkWell( onTap: () => onRemove(imageFile), child: Icon( Icons.close, color: Colors.white, shadows: [BoxShadow(blurRadius: 10)], ), ), ), ], ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_attachment/chat_attachment_preset.dart ================================================ import 'package:flutter/material.dart'; import 'package:reins/Models/chat_preset.dart'; class ChatAttachmentPreset extends StatelessWidget { final ChatPreset preset; final Function() onPressed; const ChatAttachmentPreset({ super.key, required this.preset, required this.onPressed, }); @override Widget build(BuildContext context) { return InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(16), child: Ink( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(16), ), padding: EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(preset.title, style: Theme.of(context).textTheme.titleSmall), Text( preset.subtitle, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_attachment/chat_attachment_row.dart ================================================ import 'package:flutter/material.dart'; class ChatAttachmentRow extends StatelessWidget { final int itemCount; final IndexedWidgetBuilder itemBuilder; const ChatAttachmentRow({ super.key, required this.itemCount, required this.itemBuilder, }); @override Widget build(BuildContext context) { return SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8.0), physics: const ClampingScrollPhysics(), child: Row( spacing: 8.0, children: List.generate( itemCount, (index) => itemBuilder(context, index), ), ), ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_bubble/chat_bubble.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:markdown/markdown.dart' as md; import 'package:reins/Extensions/markdown_stylesheet_extension.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'chat_bubble_actions.dart'; import 'chat_bubble_image.dart'; import 'chat_bubble_menu.dart'; import 'chat_bubble_think_block.dart'; class ChatBubble extends StatelessWidget { final OllamaMessage message; const ChatBubble({ super.key, required this.message, }); @override Widget build(BuildContext context) { final actions = ChatBubbleActions(message); return ChatBubbleMenu( menuChildren: [ MenuItemButton( onPressed: actions.handleCopy, leadingIcon: Icon(Icons.copy_outlined), child: const Text('Copy'), ), MenuItemButton( onPressed: () => actions.handleSelectText(context), leadingIcon: Icon(Icons.select_all_outlined), child: const Text('Select Text'), ), MenuItemButton( onPressed: () => actions.handleRegenerate(context), leadingIcon: Icon(Icons.refresh_outlined), child: const Text('Regenerate'), ), Divider(), MenuItemButton( onPressed: () => actions.handleEdit(context), closeOnActivate: false, leadingIcon: Icon(Icons.edit_outlined), child: const Text('Edit'), ), MenuItemButton( onPressed: () => actions.handleDelete(context), leadingIcon: Icon(Icons.delete_outline), child: const Text('Delete'), ), ], child: _ChatBubbleBody(message: message), ); } } class _ChatBubbleBody extends StatelessWidget { final OllamaMessage message; const _ChatBubbleBody({super.key, required this.message}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 25.0, vertical: 15.0), child: Column( spacing: 8, crossAxisAlignment: bubbleAlignment, children: [ // If the message has an image attachment, display it if (message.images != null && message.images!.isNotEmpty) Wrap( spacing: 8, runSpacing: 8, children: message.images! .map((imageFile) => ChatBubbleImage(imageFile: imageFile)) .toList(), ), Container( padding: isSentFromUser ? const EdgeInsets.all(10.0) : null, constraints: BoxConstraints( maxWidth: isSentFromUser ? MediaQuery.of(context).size.width * 0.8 : double.infinity, ), decoration: BoxDecoration( color: isSentFromUser ? Theme.of(context).colorScheme.primaryContainer : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(10.0), ), child: MarkdownBody( data: message.content, selectable: true, softLineBreak: true, styleSheet: context.markdownStyleSheet.copyWith( code: GoogleFonts.sourceCodePro(), ), builders: {'think': ThinkBlockBuilder()}, extensionSet: md.ExtensionSet( [ ThinkBlockSyntax(), ...md.ExtensionSet.gitHubFlavored.blockSyntaxes ], [ md.EmojiSyntax(), ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes ], ), onTapLink: (text, href, title) => launchUrlString(href!), ), ), Text( TimeOfDay.fromDateTime(message.createdAt.toLocal()).format(context), style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ); } /// Returns true if the message is sent from the user. bool get isSentFromUser => message.role == OllamaMessageRole.user; /// Returns the alignment of the bubble. /// /// If the message is sent from the user, the alignment is [Alignment.centerRight]. /// Otherwise, the alignment is [Alignment.centerLeft]. CrossAxisAlignment get bubbleAlignment => isSentFromUser ? CrossAxisAlignment.end : CrossAxisAlignment.start; } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_bubble/chat_bubble_actions.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:reins/Providers/chat_provider.dart'; import 'chat_bubble_bottom_sheet.dart'; class ChatBubbleActions { final OllamaMessage message; ChatBubbleActions(this.message); void handleCopy() { Clipboard.setData(ClipboardData(text: message.content)); } void handleSelectText(BuildContext context) { showModalBottomSheet( context: context, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.9, ), isScrollControlled: true, builder: (context) { return ChatBubbleBottomSheet( title: 'Select Text', child: SelectableText( message.content, style: Theme.of(context).textTheme.bodyLarge, ), ); }, ); } void handleRegenerate(BuildContext context) { final chatProvider = Provider.of(context, listen: false); chatProvider.regenerateMessage(message); } void handleEdit(BuildContext context) { final chatProvider = Provider.of(context, listen: false); showModalBottomSheet( context: context, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.9, ), isScrollControlled: true, isDismissible: false, enableDrag: false, builder: (context) { String textFieldText = message.content; return ChatBubbleBottomSheet( title: 'Edit Message', actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), TextButton( onPressed: () async { if (textFieldText.isNotEmpty) { await chatProvider.updateMessage( message, newContent: textFieldText, ); if (context.mounted) Navigator.pop(context, textFieldText); } }, child: const Text('Save'), ), ], child: TextFormField( initialValue: textFieldText, onChanged: (value) => textFieldText = value, autofocus: true, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration(border: OutlineInputBorder()), ), ); }, ); } void handleDelete(BuildContext context) { final chatProvider = Provider.of(context, listen: false); showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Delete Message?'), content: const Text('This action cannot be undone.'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), TextButton( onPressed: () async { await chatProvider.deleteMessage(message); if (context.mounted) Navigator.pop(context); }, child: const Text( 'Delete', style: TextStyle(color: Colors.red), ), ), ], ); }, ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_bubble/chat_bubble_bottom_sheet.dart ================================================ import 'package:flutter/material.dart'; class ChatBubbleBottomSheet extends StatelessWidget { final String title; final Widget child; final List actions; const ChatBubbleBottomSheet({ super.key, required this.title, required this.child, this.actions = const [], }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(title), titleTextStyle: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.bold), forceMaterialTransparency: true, automaticallyImplyLeading: false, actions: [ IconButton( onPressed: () { Navigator.pop(context); }, icon: const Icon(Icons.close), ), ], ), body: SafeArea( child: Column( children: [ Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: child, ), ), Row( mainAxisAlignment: MainAxisAlignment.end, children: actions, ), ], ), ), ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_bubble/chat_bubble_image.dart ================================================ import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; import 'package:reins/Widgets/chat_image.dart'; class ChatBubbleImage extends StatelessWidget { final File imageFile; const ChatBubbleImage({super.key, required this.imageFile}); @override Widget build(BuildContext context) { return GestureDetector( onTap: () { Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) { return _ChatBubbleImageFullScreen(imageFile: imageFile); }, transitionsBuilder: (context, animation, _, child) { return FadeTransition(opacity: animation, child: child); }, ), ); }, child: Hero( tag: imageFile.path, child: ChatImage( image: FileImage(imageFile), aspectRatio: 1.5, width: max( MediaQuery.of(context).size.width * 0.35, MediaQuery.of(context).size.height * 0.25, ), ), ), ); } } class _ChatBubbleImageFullScreen extends StatelessWidget { const _ChatBubbleImageFullScreen({required this.imageFile}); final File imageFile; @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Stack( children: [ Center( child: PhotoView( imageProvider: FileImage(imageFile), errorBuilder: (context, error, stackTrace) { return Center( child: Icon(Icons.error, color: Colors.red), ); }, backgroundDecoration: BoxDecoration( color: Colors.transparent, ), heroAttributes: PhotoViewHeroAttributes( tag: imageFile.path, ), ), ), Positioned( top: 5, right: 0, child: IconButton( icon: Icon( Icons.close, color: Colors.white, shadows: [BoxShadow(blurRadius: 10)], ), onPressed: () => Navigator.pop(context), ), ), ], ), ), ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_bubble/chat_bubble_menu.dart ================================================ import 'package:flutter/material.dart'; import 'package:reins/Utils/border_painter.dart'; class ChatBubbleMenu extends StatefulWidget { final Widget child; final List menuChildren; const ChatBubbleMenu({ super.key, required this.child, required this.menuChildren, }); @override State createState() => _ChatBubbleMenuState(); } class _ChatBubbleMenuState extends State { @override Widget build(BuildContext context) { return MenuAnchor( menuChildren: widget.menuChildren, builder: (context, controller, child) { return GestureDetector( onTap: () => controller.close(), onLongPressStart: (details) { controller.open(position: details.localPosition); }, onDoubleTapDown: (details) { if (controller.isOpen) { controller.close(); } else { controller.open(position: details.localPosition); } }, onSecondaryTapDown: (details) { controller.open(position: details.localPosition); }, child: CustomPaint( foregroundPainter: BorderPainter( color: controller.isOpen ? Theme.of(context).colorScheme.primaryContainer : Theme.of(context).colorScheme.surface, borderRadius: Radius.circular(10.0), strokeWidth: 2, padding: EdgeInsets.symmetric(horizontal: 10.0), ), child: child, ), ); }, child: widget.child, onOpen: () => setState(() {}), onClose: () => setState(() {}), ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_bubble/chat_bubble_think_block.dart ================================================ import 'package:flutter/material.dart'; import 'package:markdown/markdown.dart' as md; import 'package:flutter_markdown/flutter_markdown.dart'; class ThinkBlockSyntax extends md.BlockSyntax { @override RegExp get pattern => RegExp(r'^$'); @override bool canEndBlock(md.BlockParser parser) => false; const ThinkBlockSyntax(); @override List parseChildLines(md.BlockParser parser) { final childLines = []; parser.advance(); // Advance past the opening tag while (!parser.isDone) { if (parser.current.content == '') { parser.advance(); // Advance past the closing tag break; } childLines.add(parser.current); parser.advance(); } return childLines; } @override md.Node parse(md.BlockParser parser) { final childLines = parseChildLines(parser); var content = childLines.map((e) => e.content).join('\n'); return md.Element('pre', [md.Element.text('think', content)]); } } class ThinkBlockBuilder extends MarkdownElementBuilder { @override Widget visitElementAfter(md.Element element, TextStyle? preferredStyle) { return ThinkBlockWidget(content: element.textContent); } } class ThinkBlockWidget extends StatefulWidget { final String content; const ThinkBlockWidget({super.key, required this.content}); @override State createState() => _ThinkBlockWidgetState(); } class _ThinkBlockWidgetState extends State { bool _showingThought = true; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( onTap: () { setState(() => _showingThought = !_showingThought); }, child: Row( children: [ Text('Thought', style: TextStyle(color: _thoughtColor)), Icon(_thoughtButtonIcon, color: _thoughtColor), ], ), ), if (_showingThought) SelectableText(widget.content, style: TextStyle(color: _thoughtColor)), ], ); } IconData get _thoughtButtonIcon => _showingThought ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_up; Color get _thoughtColor => Theme.of(context).colorScheme.secondary; } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_empty.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:reins/Constants/constants.dart'; class ChatEmpty extends StatelessWidget { final Widget child; const ChatEmpty({super.key, required this.child}); @override Widget build(BuildContext context) { return Center( child: SingleChildScrollView( physics: NeverScrollableScrollPhysics(), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset( AppConstants.appIconSvg, height: 48, colorFilter: ColorFilter.mode( Theme.of(context).colorScheme.onSurface, BlendMode.srcIn, ), ), child, ], ), ), ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_error.dart ================================================ import 'package:flutter/material.dart'; class ChatError extends StatelessWidget { final String message; final void Function() onRetry; const ChatError({ super.key, required this.message, required this.onRetry, }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.error, ), borderRadius: BorderRadius.circular(10.0), ), padding: EdgeInsets.all(10.0), margin: EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( message, style: TextStyle(color: Theme.of(context).colorScheme.error), ), const SizedBox(height: 10.0), FilledButton( onPressed: onRetry, style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, ), child: Text('Retry'), ), ], ), ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_list_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:shimmer/shimmer.dart'; import 'package:notification_centre/notification_centre.dart'; import 'chat_bubble/chat_bubble.dart'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Utils/observe_size.dart'; import 'package:reins/Utils/retained_position_scroll_physics.dart'; class ChatListView extends StatefulWidget { final List messages; final bool isAwaitingReply; final Widget? error; final double? bottomPadding; const ChatListView({ super.key, required this.messages, required this.isAwaitingReply, this.error, this.bottomPadding, }); @override State createState() => _ChatListViewState(); } class _ChatListViewState extends State { final ScrollController _scrollController = ScrollController(); bool _isScrollToBottomButtonVisible = false; final _messageSizeProxy = WidgetSizeProxy(); @override void initState() { super.initState(); _scrollController.addListener(() { _updateScrollToBottomButtonVisibility(); }); NotificationCenter().addObserver( NotificationNames.generationBegin, this, (n) => _scrollToBottom(), ); } @override void didUpdateWidget(covariant ChatListView oldWidget) { super.didUpdateWidget(oldWidget); // Add to the post frame callback to ensure that the scroll offset is // read after the widget has been updated. WidgetsBinding.instance.addPostFrameCallback((_) { // Update the button visibility when the user switches chats, // regenerates a message or delete a message. _updateScrollToBottomButtonVisibility(); }); } @override void dispose() { _scrollController.dispose(); // Remove the observer for the generation begin notification NotificationCenter().removeObserver(NotificationNames.generationBegin, this); super.dispose(); } @override Widget build(BuildContext context) { return Stack( alignment: Alignment.bottomCenter, children: [ CustomScrollView( controller: _scrollController, reverse: true, physics: RetainedPositionScrollPhysics( widgetSizeProxy: _messageSizeProxy, ), slivers: [ if (widget.bottomPadding != null) SliverPadding( padding: EdgeInsets.only(bottom: widget.bottomPadding!), ), if (widget.error != null) SliverToBoxAdapter( child: widget.error, ), if (widget.isAwaitingReply) SliverToBoxAdapter( child: Shimmer.fromColors( // TODO: Play with the colors to make it look better baseColor: Theme.of(context).colorScheme.onPrimary, highlightColor: Theme.of(context).colorScheme.onSurface, period: const Duration(milliseconds: 2500), child: const ListTile( title: Padding( padding: EdgeInsets.all(10.0), child: Text("Thinking"), ), ), ), ), SliverList.builder( key: widget.key, itemCount: widget.messages.length, itemBuilder: (context, index) { final message = widget.messages[widget.messages.length - index - 1]; if (index == 0) { return ObserveSize( key: Key(message.id), onSizeChanged: _onMessageSizeChanged, child: ChatBubble(message: message), ); } return ChatBubble(message: message); }, ), ], ), if (_isScrollToBottomButtonVisible) IconButton( onPressed: _scrollToBottom, icon: const Icon(Icons.arrow_downward_rounded), style: IconButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.onInverseSurface, ), ), ], ); } void _onMessageSizeChanged(Size? previousSize, Size currentSize) { final currentHeight = currentSize.height; final previousHeight = (previousSize ?? currentSize).height; _messageSizeProxy.deltaHeight = currentHeight - previousHeight; } void _updateScrollToBottomButtonVisibility() { if (_scrollController.position.pixels > 100 && !_isScrollToBottomButtonVisible) { setState(() { _isScrollToBottomButtonVisible = true; }); } if (_scrollController.position.pixels < 100 && _isScrollToBottomButtonVisible) { setState(() { _isScrollToBottomButtonVisible = false; }); } } void _scrollToBottom() { _scrollController.animateTo( 0.0, duration: const Duration(milliseconds: 150), curve: Curves.easeOut, ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_select_model_button.dart ================================================ import 'package:flutter/material.dart'; class ChatSelectModelButton extends StatelessWidget { final String? currentModelName; final void Function() onPressed; const ChatSelectModelButton({ super.key, this.currentModelName, required this.onPressed, }); @override Widget build(BuildContext context) { return TextButton.icon( icon: const Icon(Icons.auto_awesome_outlined), label: Text(currentModelName ?? 'Select a model to start'), iconAlignment: IconAlignment.end, onPressed: onPressed, ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_text_field.dart ================================================ import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class ChatTextField extends StatefulWidget { final TextEditingController? controller; final void Function(String)? onChanged; final void Function()? onEditingComplete; final Widget? prefixIcon; final Widget? suffixIcon; const ChatTextField({ super.key, this.controller, this.onChanged, this.onEditingComplete, this.prefixIcon, this.suffixIcon, }); @override State createState() => _ChatTextFieldState(); } class _ChatTextFieldState extends State { static final _textFieldBucket = PageStorageBucket(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { widget.controller?.text = _readTextFieldState(); widget.onChanged?.call(widget.controller?.text ?? ''); }); } @override void deactivate() { // Write the latest text to the bucket _writeTextFieldState(widget.controller?.text ?? ''); super.deactivate(); } @override Widget build(BuildContext context) { return CallbackShortcuts( bindings: { SingleActivator(LogicalKeyboardKey.enter, shift: true): () { widget.controller?.text += '\n'; }, }, child: TextField( controller: widget.controller, onChanged: widget.onChanged, onEditingComplete: widget.onEditingComplete, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), ), labelText: 'Prompt', prefixIcon: widget.prefixIcon, suffixIcon: widget.suffixIcon, ), minLines: 1, maxLines: 5, textCapitalization: TextCapitalization.sentences, textInputAction: _textInputAction, onTapOutside: (PointerDownEvent event) { FocusManager.instance.primaryFocus?.unfocus(); }, ), ); } TextInputAction get _textInputAction { return Platform.isIOS || Platform.isAndroid ? TextInputAction.newline : TextInputAction.send; } String _readTextFieldState() { return _textFieldBucket.readState(context, identifier: widget.key) ?? ''; } void _writeTextFieldState(String text) { if (widget.key == null) return; if (widget.key is ValueKey && (widget.key as ValueKey).value == null) { return; } _textFieldBucket.writeState(context, text, identifier: widget.key); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/chat_welcome.dart ================================================ import 'package:animated_text_kit/animated_text_kit.dart'; import 'package:flutter/material.dart'; import 'package:reins/Models/settings_route_arguments.dart'; class ChatWelcome extends StatelessWidget { final CrossFadeState showingState; final void Function()? onFirstChildFinished; final double secondChildScale; final void Function()? onSecondChildScaleEnd; const ChatWelcome({ super.key, required this.showingState, this.onFirstChildFinished, required this.secondChildScale, this.onSecondChildScaleEnd, }); @override Widget build(BuildContext context) { return AnimatedCrossFade( crossFadeState: showingState, duration: const Duration(milliseconds: 150), firstChild: _ChatWelcomeText( onFinished: onFirstChildFinished, ), secondChild: AnimatedScale( scale: secondChildScale, duration: const Duration(milliseconds: 100), onEnd: onSecondChildScaleEnd, child: _ChatConfigureServerAddressButton(), ), layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) { return Stack( alignment: Alignment.center, children: [ Positioned( key: topChildKey, child: Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: topChild, ), ), Positioned( key: bottomChildKey, child: Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: bottomChild, ), ), ], ); }, ); } } class _ChatWelcomeText extends StatelessWidget { final void Function()? onFinished; const _ChatWelcomeText({this.onFinished}); @override Widget build(BuildContext context) { return AnimatedTextKit( animatedTexts: [ TyperAnimatedText( 'Welcome to Reins!', speed: const Duration(milliseconds: 100), ), TyperAnimatedText( 'Configure a server address to start.', speed: const Duration(milliseconds: 100), ), ], displayFullTextOnTap: true, isRepeatingAnimation: false, pause: Duration(milliseconds: 1500), stopPauseOnTap: true, onFinished: onFinished, ); } } class _ChatConfigureServerAddressButton extends StatelessWidget { @override Widget build(BuildContext context) { return OutlinedButton.icon( icon: const Icon( Icons.warning_amber_rounded, color: Colors.amber, ), label: Text('Tap to configure a server address'), iconAlignment: IconAlignment.start, onPressed: () { Navigator.pushNamed( context, '/settings', arguments: SettingsRouteArguments(autoFocusServerAddress: true), ); }, ); } } ================================================ FILE: lib/Pages/chat_page/subwidgets/subwidgets.dart ================================================ export 'chat_list_view.dart'; export 'chat_empty.dart'; export 'chat_select_model_button.dart'; export 'chat_welcome.dart'; export 'chat_text_field.dart'; export 'chat_error.dart'; export 'chat_attachment/chat_attachment_row.dart'; export 'chat_attachment/chat_attachment_image.dart'; export 'chat_attachment/chat_attachment_preset.dart'; ================================================ FILE: lib/Pages/main_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:reins/Pages/chat_page/chat_page.dart'; import 'package:reins/Widgets/chat_app_bar.dart'; import 'package:reins/Widgets/chat_drawer.dart'; import 'package:responsive_framework/responsive_framework.dart'; class ReinsMainPage extends StatelessWidget { const ReinsMainPage({super.key}); @override Widget build(BuildContext context) { if (ResponsiveBreakpoints.of(context).isMobile) { return _ReinsMobileMainPage(); } else { return _ReinsLargeMainPage(); } } } class _ReinsMobileMainPage extends StatelessWidget { const _ReinsMobileMainPage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( appBar: ChatAppBar(), body: SafeArea(child: ChatPage()), drawer: ChatDrawer(), ); } } class _ReinsLargeMainPage extends StatelessWidget { const _ReinsLargeMainPage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( body: SafeArea( child: Row( children: [ ChatDrawer(), Expanded(child: ChatPage()), ], ), ), ); } } ================================================ FILE: lib/Pages/settings_page/settings_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:reins/Models/settings_route_arguments.dart'; import 'subwidgets/subwidgets.dart'; class SettingsPage extends StatelessWidget { final SettingsRouteArguments? arguments; const SettingsPage({super.key, this.arguments}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Settings', style: GoogleFonts.pacifico()), ), body: SafeArea( child: _SettingsPageContent(arguments: arguments), ), ); } } class _SettingsPageContent extends StatelessWidget { final SettingsRouteArguments? arguments; const _SettingsPageContent({required this.arguments}); @override Widget build(BuildContext context) { return ListView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), children: [ ThemesSettings(), SizedBox(height: 16), ServerSettings( autoFocusServerAddress: arguments?.autoFocusServerAddress ?? false, ), SizedBox(height: 16), ReinsSettings(), ], ); } } ================================================ FILE: lib/Pages/settings_page/subwidgets/reins_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:in_app_review/in_app_review.dart'; import 'dart:io' show Platform; import 'package:reins/Widgets/flexible_text.dart'; class ReinsSettings extends StatelessWidget { const ReinsSettings({super.key}); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Reins', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), ListTile( leading: Icon(Icons.rate_review), title: Text('Review Reins'), subtitle: Text('Share your feedback'), onTap: () async { if (await InAppReview.instance.isAvailable() && Platform.isIOS) { InAppReview.instance.openStoreListing(appStoreId: "6739738501"); } else { launchUrlString('https://github.com/ibrahimcetin/reins'); } }, ), Builder( builder: (builderContext) => ListTile( leading: Icon(Icons.share), title: Text('Share Reins'), subtitle: Text('Share Reins with your friends'), onTap: () { _openShareSheet(builderContext); }, ), ), if (Platform.isAndroid || Platform.isIOS) ListTile( leading: Icon(Icons.desktop_mac_outlined), title: Text('Try Desktop App'), subtitle: Text('Available on macOS and Linux'), onTap: () { launchUrlString('https://reins.ibrahimcetin.dev'); }, ), if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) ListTile( leading: Icon(Icons.phone_iphone_outlined), title: Text('Try Mobile App'), subtitle: Text('Available on iOS'), onTap: () { launchUrlString('https://reins.ibrahimcetin.dev'); }, ), ListTile( leading: Icon(Icons.code), title: Text('Go to Source Code'), subtitle: Text('View on GitHub'), onTap: () { launchUrlString('https://github.com/ibrahimcetin/reins'); }, ), ListTile( leading: Icon(Icons.star), title: Text('Give a Star on GitHub'), subtitle: Text('Support the project'), onTap: () { launchUrlString('https://github.com/ibrahimcetin/reins'); }, ), Row( mainAxisAlignment: MainAxisAlignment.center, spacing: 5, children: [ Icon(Icons.favorite, color: Colors.red, size: 16), FlexibleText( "Thanks for using Reins!", textAlign: TextAlign.center, ), ], ), ], ); } void _openShareSheet(BuildContext context) { final box = context.findRenderObject() as RenderBox?; if (box != null) { SharePlus.instance.share( ShareParams( text: 'Check out Reins: https://reins.ibrahimcetin.dev', sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size, ), ); } } } ================================================ FILE: lib/Pages/settings_page/subwidgets/server_settings.dart ================================================ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:http/http.dart' as http; import 'package:hive/hive.dart'; import 'package:reins/Extensions/markdown_stylesheet_extension.dart'; import 'package:reins/Models/ollama_exception.dart'; import 'package:reins/Models/ollama_request_state.dart'; import 'package:reins/Widgets/ollama_bottom_sheet_header.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ServerSettings extends StatefulWidget { final bool autoFocusServerAddress; const ServerSettings({super.key, this.autoFocusServerAddress = false}); @override State createState() => _ServerSettingsState(); } class _ServerSettingsState extends State { final _settingsBox = Hive.box('settings'); final _serverAddressController = TextEditingController(); OllamaRequestState _requestState = OllamaRequestState.uninitialized; get _isLoading => _requestState == OllamaRequestState.loading; String? _serverAddressErrorText; @override void initState() { super.initState(); _initialize(); } _initialize() { final serverAddress = _settingsBox.get('serverAddress'); if (serverAddress != null) { _serverAddressController.text = serverAddress; _handleConnectButton(); } } @override void dispose() { _serverAddressController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Server', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), TextField( autofocus: widget.autoFocusServerAddress, controller: _serverAddressController, keyboardType: TextInputType.url, onChanged: (_) { setState(() { _serverAddressErrorText = null; _requestState = OllamaRequestState.uninitialized; }); }, decoration: InputDecoration( labelText: 'Ollama Server Address', border: OutlineInputBorder(), errorText: _serverAddressErrorText, suffixIcon: IconButton( icon: Icon(Icons.info_outline), onPressed: () => _showOllamaInfoBottomSheet(context), ), ), onTapOutside: (PointerDownEvent event) { FocusManager.instance.primaryFocus?.unfocus(); }, ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: Wrap( spacing: 8.0, runSpacing: 8.0, alignment: (Platform.isAndroid || Platform.isIOS) ? WrapAlignment.spaceEvenly : WrapAlignment.spaceBetween, children: [ ElevatedButton( onPressed: _isLoading ? null : _handleSearchLocalNetwork, child: const Text('Search Local Network'), ), ElevatedButton( onPressed: _isLoading ? null : _handleConnectButton, child: _ConnectionStatusIndicator( color: _connectionStatusColor, ), ), ], ), ), ], ); } Color get _connectionStatusColor { switch (_requestState) { case OllamaRequestState.error: return Colors.red; case OllamaRequestState.loading: return Colors.orange; case OllamaRequestState.success: return Colors.green; case OllamaRequestState.uninitialized: return Colors.grey; } } _handleConnectButton() async { setState(() { _serverAddressErrorText = null; _requestState = OllamaRequestState.loading; }); try { // Validate the server address. final newAddress = _validateServerAddress(_serverAddressController.text); // Establish a connection to the server. final result = await _establishServerConnection(Uri.parse(newAddress)); if (!mounted) return; _requestState = result.$1; _saveServerAddressWith(result); } on OllamaException catch (error) { _serverAddressErrorText = error.message; _requestState = OllamaRequestState.error; } catch (_) { _serverAddressErrorText = 'Invalid URL format. Use: http(s)://:'; _requestState = OllamaRequestState.error; } finally { setState(() {}); } } void _saveServerAddressWith((OllamaRequestState, Uri) result) { final state = result.$1; final newAddress = result.$2.toString(); final currentAddress = _settingsBox.get('serverAddress'); if (state == OllamaRequestState.success && newAddress != currentAddress) { _settingsBox.put('serverAddress', newAddress); } } /// Establishes a connection to the Ollama server. /// /// Returns a tuple of the request state and the given server address. static Future<(OllamaRequestState, Uri)> _establishServerConnection( Uri serverAddress, ) async { try { final response = await http.get(serverAddress).timeout(const Duration(seconds: 2)); if (response.statusCode == 200) { return (OllamaRequestState.success, serverAddress); } else { return (OllamaRequestState.error, serverAddress); } } catch (e) { return (OllamaRequestState.error, serverAddress); } } String _validateServerAddress(String address) { if (address.isEmpty) { throw OllamaException('Please enter a server address.'); } final url = Uri.parse(address); if (url.scheme.isEmpty) { throw OllamaException( 'Please include the scheme. e.g. http://localhost:11434', ); } // If user don't include the scheme and just enter host and port like 'localhost:11434'. // The parser will consider the host as the scheme, so host will be empty. But actually the scheme is empty. if (url.scheme != 'http' && url.scheme != 'https' && url.host.isEmpty) { throw OllamaException( 'Please include the scheme. e.g. http://localhost:11434', ); } if (url.host.isEmpty) { throw OllamaException( 'Please include the host. e.g. http://localhost:11434', ); } if (url.scheme != 'http' && url.scheme != 'https') { throw OllamaException( 'Invalid scheme. Only http and https are supported.', ); } final String formattedAddress = "${url.scheme}://${url.host}${url.hasPort ? ":${url.port}" : ""}${url.path}"; return formattedAddress; } void _showOllamaInfoBottomSheet(BuildContext context) { showModalBottomSheet( context: context, builder: (BuildContext context) { return _OllamaInfoBottomSheet(); }, ); } void _handleSearchLocalNetwork() async { setState(() { _serverAddressErrorText = null; _requestState = OllamaRequestState.loading; }); try { final result = await Isolate.run(() => _searchLocalNetwork()); final foundAddress = result.$2.toString(); if (!mounted) return; // Update the server address text field with the found address. _serverAddressController.text = foundAddress; _requestState = result.$1; _saveServerAddressWith(result); } on OllamaException catch (e) { _serverAddressErrorText = e.message; _requestState = OllamaRequestState.error; } catch (e) { _serverAddressErrorText = 'Something went wrong while searching.'; _requestState = OllamaRequestState.error; } finally { setState(() {}); } } static Future<(OllamaRequestState, Uri)> _searchLocalNetwork() async { final networkInterfaces = await NetworkInterface.list( includeLoopback: true, type: InternetAddressType.IPv4, ); final futures = >[]; for (var interface in networkInterfaces) { for (var address in interface.addresses) { if (address.isLoopback) { final url = Uri.parse('http://${address.address}:11434'); futures.add(_establishServerConnection(url)); } else { final segments = address.address.split('.'); for (int i = 1; i < 255; i++) { final url = Uri.parse( 'http://${segments[0]}.${segments[1]}.${segments[2]}.$i:11434', ); futures.add(_establishServerConnection(url)); } } } } final results = await Future.wait(futures); final result = results.firstWhere( (result) => result.$1 == OllamaRequestState.success, orElse: () => throw OllamaException('No Ollama server found on the local network.'), ); return result; } } class _ConnectionStatusIndicator extends StatelessWidget { final Color color; const _ConnectionStatusIndicator({ super.key, required this.color, }); @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ const Text('Connect'), const SizedBox(width: 10), Container( width: MediaQuery.of(context).textScaler.scale(10), height: MediaQuery.of(context).textScaler.scale(10), decoration: BoxDecoration( shape: BoxShape.circle, color: color, ), ), ], ); } } class _OllamaInfoBottomSheet extends StatelessWidget { const _OllamaInfoBottomSheet({ super.key, }); @override Widget build(BuildContext context) { return SafeArea( bottom: false, minimum: EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ OllamaBottomSheetHeader(title: 'What is Ollama?'), Divider(), Expanded( child: ListView( children: [ MarkdownBody( data: "Ollama is a free platform that enables you to run advanced large language models (LLMs) like Llama 3.3, Phi 3, Mistral, Gemma 2, and more directly on your local machine. This setup enhances privacy, security, and control over your AI interactions. Ollama also allows you to customize and create your own models.\n\nTo get started with Ollama, visit their official website: [ollama.com](https://ollama.com). Here, you can explore various models and download the platform to begin using Ollama.", styleSheet: context.markdownStyleSheet, onTapLink: (_, href, __) => launchUrlString(href!), ), ], ), ), ], ), ); } } ================================================ FILE: lib/Pages/settings_page/subwidgets/subwidgets.dart ================================================ export 'server_settings.dart'; export 'themes_settings.dart'; export 'reins_settings.dart'; ================================================ FILE: lib/Pages/settings_page/subwidgets/themes_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:reins/Constants/constants.dart'; class ThemesSettings extends StatefulWidget { const ThemesSettings({super.key}); @override State createState() => _ThemesSettingsState(); } class _ThemesSettingsState extends State { final _settingsBox = Hive.box('settings'); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Align( alignment: Alignment.centerLeft, child: Text( 'Themes', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 16), Container( decoration: ShapeDecoration( shape: StadiumBorder(), color: Theme.of(context).colorScheme.primaryContainer, ), child: Row( children: [ Padding( padding: const EdgeInsets.all(8.0), child: CircleAvatar( backgroundImage: AssetImage(AppConstants.appIconPng), radius: MediaQuery.of(context).textScaler.scale(16), ), ), Expanded(child: Text("Here is your current theme")), IconButton( icon: Icon(_brightnessIcon), iconSize: MediaQuery.of(context).textScaler.scale(24), onPressed: () { setState(() => _toggleBrightness()); }, ), ], ), ), const SizedBox(height: 16), Wrap( spacing: 10, runSpacing: 10, children: [ _ThemeButton( seedColor: Colors.red, onPressed: () => _settingsBox.put("color", Colors.red), ), _ThemeButton( seedColor: Colors.green, onPressed: () => _settingsBox.put("color", Colors.green), ), _ThemeButton( seedColor: Colors.blue, onPressed: () => _settingsBox.put("color", Colors.blue), ), _ThemeButton( seedColor: Colors.purple, onPressed: () => _settingsBox.put("color", Colors.purple), ), _ThemeButton( seedColor: Colors.orange, onPressed: () => _settingsBox.put("color", Colors.orange), ), _ThemeButton( seedColor: Colors.grey, onPressed: () => _settingsBox.put("color", Colors.grey), ), ], ), ], ); } void _toggleBrightness() { final currentBrightness = _settingsBox.get('brightness'); // Brightness: 1 = light, 0 = dark, null = auto // Toggle between light, dark, and auto. 1 > 0 > null > 1 > ... final nb = currentBrightness == 1 ? 0 : (currentBrightness == 0 ? null : 1); _settingsBox.put('brightness', nb); } IconData get _brightnessIcon { final brightness = _settingsBox.get('brightness'); if (brightness == null) return Icons.radio_button_off; return brightness == 1 ? Icons.light_mode_outlined : Icons.dark_mode_outlined; } } class _ThemeButton extends StatelessWidget { final Color seedColor; final Function()? onPressed; const _ThemeButton({required this.seedColor, required this.onPressed}); @override Widget build(BuildContext context) { final colorScheme = ColorScheme.fromSeed( seedColor: seedColor, brightness: Theme.of(context).brightness, dynamicSchemeVariant: DynamicSchemeVariant.neutral, ); return ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( backgroundColor: colorScheme.surfaceContainer, padding: EdgeInsets.all(16.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _colorNames[seedColor] ?? "Custom", style: TextStyle(color: colorScheme.primary), ), Container( height: 20, width: 80, decoration: ShapeDecoration( color: colorScheme.primary, shape: StadiumBorder(), ), ), const SizedBox(height: 8), Container( height: 20, width: 80, decoration: ShapeDecoration( color: colorScheme.surface, shape: StadiumBorder(), ), ), const SizedBox(height: 8), ], ), ); } static final _colorNames = { Colors.red: "Red", Colors.blue: "Blue", Colors.purple: "Purple", Colors.orange: "Orange", Colors.green: "Green", Colors.grey: "Grey", }; } ================================================ FILE: lib/Providers/chat_provider.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:notification_centre/notification_centre.dart'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Models/chat_configure_arguments.dart'; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_exception.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:reins/Models/ollama_model.dart'; import 'package:reins/Services/database_service.dart'; import 'package:reins/Services/ollama_service.dart'; class ChatProvider extends ChangeNotifier { final OllamaService _ollamaService; final DatabaseService _databaseService; List _messages = []; List get messages => _messages; List _chats = []; List get chats => _chats; int _currentChatIndex = -1; int get selectedDestination => _currentChatIndex + 1; OllamaChat? get currentChat => _currentChatIndex == -1 ? null : _chats[_currentChatIndex]; final Map _activeChatStreams = {}; bool get isCurrentChatStreaming => _activeChatStreams.containsKey(currentChat?.id); bool get isCurrentChatThinking => currentChat != null && _activeChatStreams.containsKey(currentChat?.id) && _activeChatStreams[currentChat?.id] == null; /// A map of chat errors, indexed by chat ID. final Map _chatErrors = {}; /// The current chat error. This is the error associated with the current chat. /// If there is no error, this will be `null`. /// /// This is used to display error messages in the chat view. OllamaException? get currentChatError => _chatErrors[currentChat?.id]; /// The current chat configuration. ChatConfigureArguments get currentChatConfiguration { if (currentChat == null) { return _emptyChatConfiguration ?? ChatConfigureArguments.defaultArguments; } else { return ChatConfigureArguments( systemPrompt: currentChat!.systemPrompt, chatOptions: currentChat!.options, ); } } /// The chat configuration for the empty chat. ChatConfigureArguments? _emptyChatConfiguration; ChatProvider({ required OllamaService ollamaService, required DatabaseService databaseService, }) : _ollamaService = ollamaService, _databaseService = databaseService { _initialize(); } Future _initialize() async { _updateOllamaServiceAddress(); await _databaseService.open("ollama_chat.db"); _chats = await _databaseService.getAllChats(); notifyListeners(); } void destinationChatSelected(int destination) { _currentChatIndex = destination - 1; if (destination == 0) { _resetChat(); } else { _loadCurrentChat(); } notifyListeners(); } void _resetChat() { _currentChatIndex = -1; _messages.clear(); notifyListeners(); } Future _loadCurrentChat() async { _messages = await _databaseService.getMessages(currentChat!.id); // Add the streaming message to the chat if it exists final streamingMessage = _activeChatStreams[currentChat!.id]; if (streamingMessage != null) { _messages.add(streamingMessage); } // Unfocus the text field to dismiss the keyboard FocusManager.instance.primaryFocus?.unfocus(); notifyListeners(); } Future createNewChat(OllamaModel model) async { final chat = await _databaseService.createChat(model.name); _chats.insert(0, chat); _currentChatIndex = 0; if (_emptyChatConfiguration != null) { await updateCurrentChat( newSystemPrompt: _emptyChatConfiguration!.systemPrompt, newOptions: _emptyChatConfiguration!.chatOptions, ); _emptyChatConfiguration = null; } notifyListeners(); } Future updateCurrentChat({ String? newModel, String? newTitle, String? newSystemPrompt, OllamaChatOptions? newOptions, }) async { await updateChat( currentChat, newModel: newModel, newTitle: newTitle, newSystemPrompt: newSystemPrompt, newOptions: newOptions, ); } /// Updates the chat with the given parameters. /// /// If the chat is `null`, it updates the empty chat configuration. Future updateChat( OllamaChat? chat, { String? newModel, String? newTitle, String? newSystemPrompt, OllamaChatOptions? newOptions, }) async { if (chat == null) { final chatOptions = newOptions ?? _emptyChatConfiguration?.chatOptions; _emptyChatConfiguration = ChatConfigureArguments( systemPrompt: newSystemPrompt ?? _emptyChatConfiguration?.systemPrompt, chatOptions: chatOptions ?? OllamaChatOptions(), ); } else { await _databaseService.updateChat( chat, newModel: newModel, newTitle: newTitle, newSystemPrompt: newSystemPrompt, newOptions: newOptions, ); final chatIndex = _chats.indexWhere((c) => c.id == chat.id); if (chatIndex != -1) { _chats[chatIndex] = (await _databaseService.getChat(chat.id))!; notifyListeners(); } else { throw OllamaException("Chat not found."); } } } Future deleteCurrentChat() async { final chat = currentChat; if (chat == null) return; _resetChat(); _chats.remove(chat); _activeChatStreams.remove(chat.id); await _databaseService.deleteChat(chat.id); } Future sendPrompt(String text, {List? images}) async { // Save the chat where the prompt was sent final associatedChat = currentChat!; // Create a user prompt message and add it to the chat final prompt = OllamaMessage( text.trim(), images: images, role: OllamaMessageRole.user, ); _messages.add(prompt); notifyListeners(); // Save the user prompt to the database await _databaseService.addMessage(prompt, chat: associatedChat); // Initialize the chat stream with the messages in the chat await _initializeChatStream(associatedChat); } Future _initializeChatStream(OllamaChat associatedChat) async { // Send a notification to inform generation begin NotificationCenter().postNotification(NotificationNames.generationBegin); // Clear the active chat streams to cancel the previous stream _activeChatStreams.remove(associatedChat.id); // Clear the error message associated with the chat if (_chatErrors.remove(associatedChat.id) != null) { notifyListeners(); // Wait for a short time to show the user that the error message is cleared await Future.delayed(Duration(milliseconds: 250)); } // Update the chat list to show the latest chat at the top _moveCurrentChatToTop(); // Add the chat to the active chat streams to show the thinking indicator _activeChatStreams[associatedChat.id] = null; // Notify the listeners to show the thinking indicator notifyListeners(); // Stream the Ollama message OllamaMessage? ollamaMessage; try { ollamaMessage = await _streamOllamaMessage(associatedChat); } on OllamaException catch (error) { _chatErrors[associatedChat.id] = error; } on SocketException catch (_) { _chatErrors[associatedChat.id] = OllamaException( 'Network connection lost. Check your server address or internet connection.', ); } catch (error) { _chatErrors[associatedChat.id] = OllamaException("Something went wrong."); } finally { // Remove the chat from the active chat streams _activeChatStreams.remove(associatedChat.id); notifyListeners(); } // Save the Ollama message to the database if (ollamaMessage != null) { await _databaseService.addMessage(ollamaMessage, chat: associatedChat); } } Future _streamOllamaMessage(OllamaChat associatedChat) async { if (_messages.isEmpty) return null; final stream = _ollamaService.chatStream(_messages, chat: associatedChat); OllamaMessage? streamingMessage; OllamaMessage? receivedMessage; await for (receivedMessage in stream) { // If the chat id is not in the active chat streams, it means the stream // is cancelled by the user. So, we need to break the loop. if (_activeChatStreams.containsKey(associatedChat.id) == false) { streamingMessage?.createdAt = DateTime.now(); return streamingMessage; } // Ignore empty initial messages, preventing disruption of the thinking indicator if (receivedMessage.content.isEmpty && streamingMessage == null) { continue; } if (streamingMessage == null) { // Keep the first received message to add the content of the following messages streamingMessage = receivedMessage; // Update the active chat streams key with the ollama message // to be able to show the stream in the chat. // We also use this when the user switches between chats while streaming. _activeChatStreams[associatedChat.id] = streamingMessage; // Be sure the user is in the same chat while the initial message is received if (associatedChat.id == currentChat?.id) { _messages.add(streamingMessage); } } else { streamingMessage.content += receivedMessage.content; } notifyListeners(); } if (receivedMessage != null) { // Update the metadata of the streaming message with the last received message streamingMessage?.updateMetadataFrom(receivedMessage); } // Update created at time to the current time when the stream is finished streamingMessage?.createdAt = DateTime.now(); return streamingMessage; } Future regenerateMessage(OllamaMessage message) async { final associatedChat = currentChat!; final messageIndex = _messages.indexOf(message); if (messageIndex == -1) return; final includeMessage = (message.role == OllamaMessageRole.user ? 1 : 0); final stayedMessages = _messages.sublist(0, messageIndex + includeMessage); final removeMessages = _messages.sublist(messageIndex + includeMessage); _messages = stayedMessages; notifyListeners(); await _databaseService.deleteMessages(removeMessages); // Reinitialize the chat stream with the messages in the chat await _initializeChatStream(associatedChat); } Future retryLastPrompt() async { if (_messages.isEmpty) return; final associatedChat = currentChat!; if (_messages.last.role == OllamaMessageRole.assistant) { final message = _messages.removeLast(); await _databaseService.deleteMessage(message.id); } // Reinitialize the chat stream with the messages in the chat await _initializeChatStream(associatedChat); notifyListeners(); } Future updateMessage( OllamaMessage message, { String? newContent, }) async { message.content = newContent ?? message.content; notifyListeners(); await _databaseService.updateMessage(message, newContent: newContent); } Future deleteMessage(OllamaMessage message) async { await _databaseService.deleteMessage(message.id); // If the message is in the chat, remove it from the chat if (_messages.remove(message)) { notifyListeners(); } } void cancelCurrentStreaming() { _activeChatStreams.remove(currentChat?.id); notifyListeners(); } void _moveCurrentChatToTop() { if (_currentChatIndex == 0) return; final chat = _chats.removeAt(_currentChatIndex); _chats.insert(0, chat); _currentChatIndex = 0; } Future> fetchAvailableModels() async { return await _ollamaService.listModels(); } void _updateOllamaServiceAddress() { final settingsBox = Hive.box('settings'); _ollamaService.baseUrl = settingsBox.get('serverAddress'); settingsBox.listenable(keys: ["serverAddress"]).addListener(() { _ollamaService.baseUrl = settingsBox.get('serverAddress'); // This will update empty chat state to dismiss "Tap to configure server address" message notifyListeners(); }); } Future saveAsNewModel(String modelName) async { final associatedChat = currentChat; if (associatedChat == null) { // TODO: Empty chat should be saved as a new model. throw OllamaException("No chat is selected."); } await _ollamaService.createModel( modelName, chat: associatedChat, messages: _messages.toList(), ); } Future generateTitleForCurrentChat() async { final associatedChat = currentChat; final message = _messages.firstOrNull; if (associatedChat == null || message == null) return; // Create a temp chat with necessary system prompt final chat = OllamaChat( model: associatedChat.model, systemPrompt: GenerateTitleConstants.systemPrompt, ); // Generate a title for the message final stream = _ollamaService.generateStream( GenerateTitleConstants.prompt + message.content, chat: chat, ); var title = ""; await for (final titleMessage in stream) { // Ignore empty initial messages, preventing empty title if (title.isEmpty && titleMessage.content.isEmpty) { continue; } title += titleMessage.content; // If tag exists, do not stream chat title if (title.startsWith("")) { await updateChat(associatedChat, newTitle: "Thinking for a title..."); } else { await updateChat(associatedChat, newTitle: title); } } // Remove tag and its content if (title.startsWith("")) { title = title.replaceAll(RegExp(r'.*?', dotAll: true), ''); } // Save the title as the chat title await updateChat(associatedChat, newTitle: title.trim()); } } ================================================ FILE: lib/Services/database_service.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; import 'package:path/path.dart' as path; class DatabaseService { late Database _db; Future getDatabasesPathForPlatform() async { if (Platform.isLinux) { return PathManager.instance.documentsDirectory.path; } else { return await getDatabasesPath(); } } Future open(String databaseFile) async { _db = await openDatabase( path.join(await getDatabasesPathForPlatform(), databaseFile), version: 1, onCreate: (Database db, int version) async { await db.execute('''CREATE TABLE IF NOT EXISTS chats ( chat_id TEXT PRIMARY KEY, model TEXT NOT NULL, chat_title TEXT NOT NULL, system_prompt TEXT, options TEXT ) WITHOUT ROWID;'''); await db.execute('''CREATE TABLE IF NOT EXISTS messages ( message_id TEXT PRIMARY KEY, chat_id TEXT NOT NULL, content TEXT NOT NULL, images TEXT, role TEXT CHECK(role IN ('user', 'assistant', 'system')) NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (chat_id) REFERENCES chats(chat_id) ON DELETE CASCADE ) WITHOUT ROWID;'''); // Create cleanup_jobs table await db.execute('''CREATE TABLE IF NOT EXISTS cleanup_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_paths TEXT NOT NULL )'''); // Create trigger to handle image deletion await db.execute('''CREATE TRIGGER IF NOT EXISTS delete_images_trigger AFTER DELETE ON messages WHEN OLD.images IS NOT NULL BEGIN INSERT INTO cleanup_jobs (image_paths) VALUES (OLD.images); END;'''); }, ); } Future close() async => _db.close(); // Chat Operations Future createChat(String model) async { final id = Uuid().v4(); await _db.insert('chats', { 'chat_id': id, 'model': model, 'chat_title': 'New Chat', 'system_prompt': null, 'options': null, }); return (await getChat(id))!; } Future getChat(String chatId) async { final List> maps = await _db.query( 'chats', where: 'chat_id = ?', whereArgs: [chatId], ); if (maps.isEmpty) { return null; } else { return OllamaChat.fromMap(maps.first); } } Future updateChat( OllamaChat chat, { String? newModel, String? newTitle, String? newSystemPrompt, OllamaChatOptions? newOptions, }) async { await _db.update( 'chats', { 'model': newModel ?? chat.model, 'chat_title': newTitle ?? chat.title, 'system_prompt': newSystemPrompt ?? chat.systemPrompt, 'options': newOptions?.toJson() ?? chat.options.toJson(), }, where: 'chat_id = ?', whereArgs: [chat.id], ); } Future deleteChat(String chatId) async { await _db.delete( 'chats', where: 'chat_id = ?', whereArgs: [chatId], ); await _db.delete( 'messages', where: 'chat_id = ?', whereArgs: [chatId], ); // ? Should we run with Isolate.run? _cleanupDeletedImages(); } Future> getAllChats() async { final List> maps = await _db.rawQuery( '''SELECT chats.chat_id, chats.model, chats.chat_title, chats.system_prompt, chats.options, MAX(messages.timestamp) AS last_update FROM chats LEFT JOIN messages ON chats.chat_id = messages.chat_id GROUP BY chats.chat_id ORDER BY last_update DESC;'''); return List.generate(maps.length, (i) { return OllamaChat.fromMap(maps[i]); }); } // Message Operations Future addMessage( OllamaMessage message, { required OllamaChat chat, }) async { await _db.insert('messages', { 'chat_id': chat.id, ...message.toDatabaseMap(), }); } Future getMessage(String messageId) async { final List> maps = await _db.query( 'messages', where: 'message_id = ?', whereArgs: [messageId], ); if (maps.isEmpty) { return null; } else { return OllamaMessage.fromDatabase(maps.first); } } Future updateMessage( OllamaMessage message, { String? newContent, }) async { await _db.update( 'messages', { 'content': newContent ?? message.content, }, where: 'message_id = ?', whereArgs: [message.id], ); } Future deleteMessage(String messageId) async { await _db.delete( 'messages', where: 'message_id = ?', whereArgs: [messageId], ); _cleanupDeletedImages(); } Future> getMessages(String chatId) async { final List> maps = await _db.query( 'messages', where: 'chat_id = ?', whereArgs: [chatId], orderBy: 'timestamp ASC', ); return List.generate(maps.length, (i) { return OllamaMessage.fromDatabase(maps[i]); }); } Future deleteMessages(List messages) async { await _db.transaction((txn) async { for (final message in messages) { await txn.delete( 'messages', where: 'message_id = ?', whereArgs: [message.id], ); } }); _cleanupDeletedImages(); } // ? Should we trigger this cleanup on every message deletion? // ? Or should we run it on every app start? Future _cleanupDeletedImages() async { final List> results = await _db.query( 'cleanup_jobs', columns: ['id', 'image_paths'], where: 'image_paths IS NOT NULL', ); for (final result in results) { try { final images = _constructImages(result['image_paths']); if (images == null) continue; for (final image in images) { if (await image.exists()) { await image.delete(); } } // Delete the row after images are deleted await _db.delete( 'cleanup_jobs', where: 'id = ?', whereArgs: [result['id']], ); } catch (_) {} } } static List? _constructImages(String? raw) { if (raw != null) { final List decoded = jsonDecode(raw); return decoded.map((imageRelativePath) { return File(path.join( PathManager.instance.documentsDirectory.path, imageRelativePath, )); }).toList(); } return null; } } ================================================ FILE: lib/Services/image_service.dart ================================================ import 'dart:io'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:image_compression/image_compression.dart' as img_compress; import 'package:path/path.dart' as path; import 'package:reins/Constants/constants.dart'; /// Handles all image storage and compression operations class ImageService { Future getImagesDirectory() async { final documentsDirectory = PathManager.instance.documentsDirectory; final imagesPath = path.join(documentsDirectory.path, 'images'); return await Directory(imagesPath).create(recursive: true); } Future compressAndSave(String sourcePath, {int quality = 10}) async { try { final imagesDir = await getImagesDirectory(); final targetPath = path.join( imagesDir.path, '${DateTime.now().microsecondsSinceEpoch}.jpg', ); return await _compressAndSaveImageForPlatform( sourcePath, targetPath, quality: quality, ); } catch (e) { return null; } } Future _compressAndSaveImageForPlatform( String sourcePath, String targetPath, { int quality = 10, }) async { Function(String, String, {int quality}) function; if (Platform.isLinux) { function = _compressAndSaveImageLinux; } else { function = _compressAndSaveImage; } return function( sourcePath, targetPath, quality: quality, ); } Future _compressAndSaveImage( String sourcePath, String targetPath, { int quality = 10, }) async { final compressed = await FlutterImageCompress.compressAndGetFile( sourcePath, targetPath, quality: quality, ); return compressed != null ? File(compressed.path) : null; } Future _compressAndSaveImageLinux( String sourcePath, String targetPath, { int quality = 10, }) async { final sourceFile = File(sourcePath); if (!await sourceFile.exists()) return null; final inputImage = img_compress.ImageFile( filePath: sourcePath, rawBytes: await sourceFile.readAsBytes(), ); final compressedImage = await img_compress.compressInQueue( img_compress.ImageFileConfiguration( input: inputImage, config: img_compress.Configuration(jpgQuality: quality), ), ); return await File(targetPath).writeAsBytes(compressedImage.rawBytes); } Future deleteImage(File imageFile) async { if (await imageFile.exists()) { await imageFile.delete(); } } Future deleteImages(List imageFiles) async { await Future.wait(imageFiles.map((file) => deleteImage(file))); } } ================================================ FILE: lib/Services/ollama_service.dart ================================================ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:reins/Utils/http_error_formatter.dart'; import 'package:reins/Models/api/tags_response.dart'; import 'package:reins/Models/api/show_response.dart'; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_exception.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:reins/Models/ollama_model.dart'; import 'package:reins/Models/api/create_request.dart'; class OllamaService { /// The base URL for the Ollama service API. /// /// This URL is used as the root endpoint for all network requests /// made by the Ollama service. It should be set to the base address /// of the API server. /// /// The default value is "http://localhost:11434". String _baseUrl; String get baseUrl => _baseUrl; set baseUrl(String? value) => _baseUrl = value ?? "http://localhost:11434"; /// The headers to include in all network requests. final headers = {'Content-Type': 'application/json'}; /// Creates a new instance of the Ollama service. OllamaService({String? baseUrl}) : _baseUrl = baseUrl ?? "http://localhost:11434"; /// Constructs a URL by resolving the provided path against the base URL. Uri constructUrl(String path) { final baseUri = Uri.parse(baseUrl); // Split the base URI path into segments, filtering out empty strings final segments = baseUri.pathSegments.where((s) => s.isNotEmpty).toList(); // Split the provided path into segments, filtering out empty strings final extraSegments = path.split('/').where((s) => s.isNotEmpty).toList(); // Combine both sets of segments and create a new URI return baseUri.replace(pathSegments: [...segments, ...extraSegments]); } /// Generates an OllamaMessage. /// /// This method is responsible for generating an instance of /// [OllamaMessage] based on the provided prompt and options. /// /// [prompt] is the input string used to generate the message. /// [options] is a map of additional options that can be used to /// customize the generation process. It defaults to an empty map. /// /// Returns a [Future] that completes with an [OllamaMessage]. Future generate( String prompt, { required OllamaChat chat, }) async { final url = constructUrl("/api/generate"); final response = await http.post( url, headers: headers, body: json.encode({ "model": chat.model, "prompt": prompt, "system": chat.systemPrompt, "options": chat.options.toMap(), "stream": false, }), ); if (response.statusCode == 200) { final jsonBody = json.decode(response.body); return OllamaMessage.fromJson(jsonBody); } else if (response.statusCode == 404) { throw OllamaException("${chat.model} not found on the server."); } else if (response.statusCode == 500) { throw OllamaException("Internal server error."); } else { throw OllamaException(HttpErrorFormatter.formatHttpError(response.statusCode, body: response.body)); } } Stream generateStream( String prompt, { required OllamaChat chat, }) async* { final url = constructUrl('/api/generate'); final request = http.Request("POST", url); request.headers.addAll(headers); request.body = json.encode({ "model": chat.model, "prompt": prompt, "system": chat.systemPrompt, "options": chat.options.toMap(), "stream": true, }); http.StreamedResponse response = await request.send(); if (response.statusCode == 200) { await for (final message in _processStream(response.stream)) { yield message; } } else if (response.statusCode == 404) { throw OllamaException("${chat.model} not found on the server."); } else if (response.statusCode == 500) { throw OllamaException("Internal server error."); } else { final body = await response.stream.bytesToString(); throw OllamaException(HttpErrorFormatter.formatHttpError(response.statusCode, body: body)); } } /// Sends a chat message to the Ollama service and returns the response. /// /// This method takes a message and sends it to the Ollama service, which /// processes the message and returns a response. The response is then /// encapsulated in an [OllamaMessage] object. /// /// Returns an [OllamaMessage] containing the response from the Ollama service. /// /// Throws an [Exception] if there is an error during the communication with /// the Ollama service. Future chat( List messages, { required OllamaChat chat, }) async { final url = constructUrl("/api/chat"); final response = await http.post( url, headers: headers, body: json.encode({ "model": chat.model, "messages": await _prepareMessagesWithSystemPrompt(messages, chat.systemPrompt), "options": chat.options.toMap(), "stream": false, }), ); if (response.statusCode == 200) { final jsonBody = json.decode(response.body); return OllamaMessage.fromJson(jsonBody); } else if (response.statusCode == 404) { throw OllamaException("${chat.model} not found on the server."); } else if (response.statusCode == 500) { throw OllamaException("Internal server error."); } else { throw OllamaException(HttpErrorFormatter.formatHttpError(response.statusCode, body: response.body)); } } Stream chatStream( List messages, { required OllamaChat chat, }) async* { final url = constructUrl('/api/chat'); final request = http.Request("POST", url); request.headers.addAll(headers); request.body = json.encode({ "model": chat.model, "messages": await _prepareMessagesWithSystemPrompt(messages, chat.systemPrompt), "options": chat.options.toMap(), "stream": true, }); http.StreamedResponse response = await request.send(); if (response.statusCode == 200) { await for (final message in _processStream(response.stream)) { yield message; } } else if (response.statusCode == 404) { throw OllamaException("${chat.model} not found on the server."); } else if (response.statusCode == 500) { throw OllamaException("Internal server error."); } else { final body = await response.stream.bytesToString(); throw OllamaException(HttpErrorFormatter.formatHttpError(response.statusCode, body: body)); } } Stream _processStream(Stream stream) async* { // Buffer to store the incomplete JSON object. This is necessary because // the Ollama service may send partial JSON objects in a single response. // We need to buffer the partial JSON objects and combine them to form // complete JSON objects. String buffer = ''; await for (var chunk in stream.transform(utf8.decoder)) { chunk = buffer + chunk; buffer = ''; // Split the chunk into lines and parse each line as JSON. This is // necessary because the Ollama service may send multiple JSON objects // in a single response. final lines = LineSplitter.split(chunk); for (var line in lines) { try { final jsonBody = json.decode(line); yield OllamaMessage.fromJson(jsonBody); } catch (_) { buffer = line; } } } } // Serializes chat messages with a system prompt. Future>> _prepareMessagesWithSystemPrompt( List messages, String? systemPrompt, ) async { final jsonMessages = await Future.wait(messages.map((m) async => await m.toChatJson())); if (systemPrompt != null && systemPrompt.isNotEmpty) { final sp = OllamaMessage(systemPrompt, role: OllamaMessageRole.system); jsonMessages.insert(0, await sp.toChatJson()); } return jsonMessages; } /// Lists the available models on the Ollama service. /// /// Fetches models from /api/tags and enriches each with capabilities /// from /api/show. If /api/show fails for a model, capabilities will be null. Future> listModels() async { final tagsResponse = await _fetchTags(); // Fetch capabilities for each model in parallel final models = await Future.wait( tagsResponse.models.map((model) async { final showResponse = await _showModel(model.name); return OllamaModel.from(model, showResponse); }), ); return models; } /// Fetches the list of models from /api/tags Future _fetchTags() async { final url = constructUrl("/api/tags"); final response = await http.get(url, headers: headers); if (response.statusCode == 200) { final jsonBody = json.decode(response.body); return ApiTagsResponse.fromJson(jsonBody); } else if (response.statusCode == 500) { throw OllamaException("Internal server error."); } else { throw OllamaException(HttpErrorFormatter.formatHttpError(response.statusCode, body: response.body)); } } /// Fetches detailed model information from /api/show /// /// Returns null if the endpoint is unavailable or returns an error. /// This ensures graceful degradation for older Ollama versions. Future _showModel(String name) async { try { final url = constructUrl("/api/show"); final response = await http.post( url, headers: headers, body: json.encode({"model": name}), ); if (response.statusCode == 200) { final jsonBody = json.decode(response.body); return ApiShowResponse.fromJson(jsonBody); } } catch (_) { // Silently ignore - endpoint may not exist on older Ollama versions } return null; } Future createModel( String model, { required OllamaChat chat, List? messages, }) async { final url = constructUrl("/api/create"); final request = ApiCreateRequest.fromChat( model, chat: chat, messages: messages, ); final response = await http.post( url, headers: headers, body: json.encode(await request.toJson()), ); if (response.statusCode == 200) { return; } else if (response.statusCode == 500) { throw OllamaException("Internal server error."); } else { throw OllamaException(HttpErrorFormatter.formatHttpError(response.statusCode, body: response.body)); } } Future deleteModel(String model) async { final url = constructUrl("/api/delete"); final response = await http.delete( url, headers: headers, body: json.encode({"model": model}), ); if (response.statusCode == 200) { return; } else if (response.statusCode == 404) { throw OllamaException("$model not found on the server."); } else if (response.statusCode == 500) { throw OllamaException("Internal server error."); } else { throw OllamaException(HttpErrorFormatter.formatHttpError(response.statusCode, body: response.body)); } } } ================================================ FILE: lib/Services/permission_service.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart' show VoidCallback; import 'package:permission_handler/permission_handler.dart'; class PermissionService { Future requestPhotoPermission({ VoidCallback? onDenied, }) async { if (!Platform.isIOS) return true; final status = await Permission.photos .onDeniedCallback(onDenied ?? () {}) .onPermanentlyDeniedCallback(onDenied ?? () {}) .request(); return status.isGranted || status.isLimited; } } ================================================ FILE: lib/Services/services.dart ================================================ export 'database_service.dart'; export 'ollama_service.dart'; export 'permission_service.dart'; export 'image_service.dart'; ================================================ FILE: lib/Utils/border_painter.dart ================================================ import 'package:flutter/material.dart'; class BorderPainter extends CustomPainter { final Color? color; final Gradient? gradient; final double strokeWidth; final Radius borderRadius; final EdgeInsets padding; BorderPainter({ this.color, this.gradient, this.borderRadius = const Radius.circular(0.0), this.padding = EdgeInsets.zero, this.strokeWidth = 1.0, }); @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; final paint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth; if (color != null) { paint.color = color!; } else if (gradient != null) { paint.shader = gradient!.createShader(rect); } final borderRect = RRect.fromRectAndRadius( Rect.fromLTWH( strokeWidth / 2 + padding.left, strokeWidth / 2 + padding.top, size.width - strokeWidth - padding.horizontal, size.height - strokeWidth - padding.vertical, ), borderRadius, ); canvas.drawRRect(borderRect, paint); } @override bool shouldRepaint(BorderPainter oldDelegate) => color != oldDelegate.color || gradient != oldDelegate.gradient || borderRadius != oldDelegate.borderRadius || padding != oldDelegate.padding || strokeWidth != oldDelegate.strokeWidth; } ================================================ FILE: lib/Utils/http_error_formatter.dart ================================================ import 'dart:async'; import 'dart:io'; /// A utility class for formatting HTTP errors and exceptions into human-readable messages. /// /// Provides static methods to convert common network exceptions and HTTP status codes /// into user-friendly error messages. class HttpErrorFormatter { HttpErrorFormatter._(); // Private constructor - use static methods /// Converts common exceptions to human-readable error messages. static String formatException(Object error) { if (error is TimeoutException) { return 'Connection timed out. Please check if the server is running.'; } else if (error is SocketException) { final message = error.message.toLowerCase(); if (message.contains('no route to host') || message.contains('network is unreachable')) { return 'Network unreachable. Please check your internet connection.'; } else if (message.contains('connection refused')) { return 'Connection refused. The server may not be running.'; } else if (message.contains('no address associated') || message.contains('failed host lookup')) { return 'Could not find server. Please verify the server address in settings.'; } return 'Network error: ${error.message}'; } else if (error is HttpException) { return 'HTTP error: ${error.message}'; } else if (error is FormatException) { return 'Invalid server address format. Please check the server configuration.'; } else if (error is HandshakeException) { return 'SSL/TLS handshake failed. Check server certificate.'; } else if (error is TlsException) { return 'Secure connection failed. Check server certificate.'; } return 'Connection failed: ${error.toString()}'; } /// Converts HTTP status codes to human-readable error messages. /// /// [statusCode] is the HTTP status code returned by the server. /// [body] is the optional response body that will be appended to the message. /// /// Returns a formatted error message with the status code and optional body. static String formatHttpError(int statusCode, {String? body}) { final reason = switch (statusCode) { 400 => 'Bad request. Please check the server address.', 401 => 'Unauthorized. Please check your API key.', 403 => 'Access forbidden. You don\'t have permission to access this server.', 404 => 'Resource not found. The requested model or endpoint does not exist.', 408 => 'Request timed out. Please try again.', 429 => 'Too many requests. Please wait and try again.', 500 => 'Internal server error. The server encountered a problem.', 502 => 'Bad gateway. There may be a problem with the server or proxy', 503 => 'Service unavailable. The server is temporarily down.', 504 => 'Gateway timeout. The server took too long to respond.', _ => 'Server returned an error.', }; final trimmedBody = body?.trim(); if (trimmedBody == null || trimmedBody.isEmpty) { return '$reason\n(HTTP $statusCode)'; } return '$reason\n(HTTP $statusCode)\n\n$trimmedBody'; } } ================================================ FILE: lib/Utils/material_color_adapter.dart ================================================ import 'package:hive/hive.dart'; import 'package:flutter/material.dart'; class MaterialColorAdapter extends TypeAdapter { @override final typeId = 0; @override MaterialColor read(BinaryReader reader) { final value = reader.readInt(); return Colors.primaries.firstWhere( (color) => color.value == value, orElse: () => Colors.grey, ); } @override void write(BinaryWriter writer, MaterialColor obj) { writer.writeInt(obj.value); } } ================================================ FILE: lib/Utils/observe_size.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class ObserveSize extends SingleChildRenderObjectWidget { final Function(Size?, Size) onSizeChanged; const ObserveSize({ super.key, required this.onSizeChanged, required super.child, }); @override RenderObject createRenderObject(BuildContext context) { return _RenderObserveSize(onSizeChanged); } } class _RenderObserveSize extends RenderProxyBox { final Function(Size?, Size) onSizeChanged; _RenderObserveSize(this.onSizeChanged); Size? _previousSize; @override void performLayout() { super.performLayout(); final newSize = (child?.size ?? size); onSizeChanged(_previousSize, newSize); _previousSize = newSize; } } ================================================ FILE: lib/Utils/request_review_helper.dart ================================================ import 'package:hive/hive.dart'; final class RequestReviewHelper { /// The number of times the app has been launched int _launchCount = 0; get launchCount => _launchCount; /// The last time a review request was made DateTime? _lastReviewRequest; get _isReviewReminderDue => _lastReviewRequest == null || DateTime.now().difference(_lastReviewRequest!) >= Duration(days: 3); /// Private constructor RequestReviewHelper._internal(); // Singleton instance static final RequestReviewHelper _instance = RequestReviewHelper._internal(); // Factory constructor to return the singleton instance static RequestReviewHelper get instance => _instance; // Hive box late final Box _box; // Initialize the singleton instance from Hive static Future initialize() async { final box = await Hive.openBox('reviewBox'); _instance._launchCount = box.get('launchCount', defaultValue: 0); _instance._lastReviewRequest = box.get('lastReviewRequest'); _instance._box = box; return _instance; } Future incrementCount({bool isLaunch = false}) async { if (isLaunch) { _launchCount++; } await save(); } bool shouldRequestReview() { final shouldRequestReview = _launchCount >= 10 && _isReviewReminderDue; if (shouldRequestReview) { _lastReviewRequest = DateTime.now(); save(); } return shouldRequestReview; } Future save() async { await _box.put('launchCount', _launchCount); await _box.put('lastReviewRequest', _lastReviewRequest); } } ================================================ FILE: lib/Utils/retained_position_scroll_physics.dart ================================================ import 'package:flutter/material.dart'; class WidgetSizeProxy { double deltaHeight = 0.0; } class RetainedPositionScrollPhysics extends ScrollPhysics { const RetainedPositionScrollPhysics({ super.parent, required this.widgetSizeProxy, }); final WidgetSizeProxy widgetSizeProxy; @override ScrollPhysics applyTo(ScrollPhysics? ancestor) { return RetainedPositionScrollPhysics( parent: ancestor, widgetSizeProxy: widgetSizeProxy, ); } @override double adjustPositionForNewDimensions({ required ScrollMetrics oldPosition, required ScrollMetrics newPosition, required bool isScrolling, required double velocity, }) { final adjustPosition = super.adjustPositionForNewDimensions( oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity, ); if (adjustPosition <= 44) { // 44 is just a threshold to adjust the position when the user scrolls to the bottom // if the user scrolls to the bottom, the adjustPosition is 0 // so we need to return the original position return adjustPosition; } else { // Add the delta height to keep the scroll position stable return adjustPosition + widgetSizeProxy.deltaHeight; } } } ================================================ FILE: lib/Widgets/chat_app_bar.dart ================================================ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Widgets/chat_configure_bottom_sheet.dart'; import 'package:reins/Widgets/model_selection_bottom_sheet.dart'; import 'package:provider/provider.dart'; import 'package:reins/Providers/chat_provider.dart'; import 'package:responsive_framework/responsive_framework.dart'; class ChatAppBar extends StatelessWidget implements PreferredSizeWidget { const ChatAppBar({super.key}); @override Widget build(BuildContext context) { final chatProvider = Provider.of(context); return AppBar( title: Column( children: [ Text(AppConstants.appName, style: GoogleFonts.pacifico()), if (chatProvider.currentChat != null) InkWell( onTap: () { _handleModelSelectionButton(context); }, customBorder: StadiumBorder(), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( chatProvider.currentChat!.model, style: GoogleFonts.kodeMono( textStyle: Theme.of(context).textTheme.labelSmall, ), ), ), ), ], ), actions: [ IconButton( icon: const Icon(Icons.tune), onPressed: () { _handleConfigureButton(context); }, ), ], forceMaterialTransparency: !ResponsiveBreakpoints.of(context).isMobile, ); } Future _handleModelSelectionButton(BuildContext context) async { final chatProvider = Provider.of(context, listen: false); final selectedModel = await showModelSelectionBottomSheet( context: context, title: "Change The Model", currentModelName: chatProvider.currentChat?.model, ); if (selectedModel != null) { await chatProvider.updateCurrentChat(newModel: selectedModel.name); } } Future _handleConfigureButton(BuildContext context) async { final chatProvider = Provider.of(context, listen: false); final arguments = chatProvider.currentChatConfiguration; final ChatConfigureBottomSheetAction? action = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) { return Padding( padding: MediaQuery.of(context).viewInsets, child: ChatConfigureBottomSheet(arguments: arguments), ); }, ); // If the user deletes the chat, we don't need to update the chat. if (action == ChatConfigureBottomSheetAction.delete) return; await chatProvider.updateCurrentChat( newSystemPrompt: arguments.systemPrompt, newOptions: arguments.chatOptions, ); } @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); } ================================================ FILE: lib/Widgets/chat_configure_bottom_sheet.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:reins/Models/chat_configure_arguments.dart'; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_exception.dart'; import 'package:reins/Providers/chat_provider.dart'; import 'package:reins/Widgets/flexible_text.dart'; import 'ollama_bottom_sheet_header.dart'; class ChatConfigureBottomSheet extends StatelessWidget { final ChatConfigureArguments arguments; const ChatConfigureBottomSheet({super.key, required this.arguments}); @override Widget build(BuildContext context) { return ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.58, ), child: SafeArea( bottom: false, minimum: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ OllamaBottomSheetHeader(title: 'Configure The Chat'), Divider(), Expanded( child: _ChatConfigureBottomSheetContent(arguments: arguments), ), ], ), ), ); } } class _ChatConfigureBottomSheetContent extends StatefulWidget { final ChatConfigureArguments arguments; const _ChatConfigureBottomSheetContent({ super.key, required this.arguments, }); @override State<_ChatConfigureBottomSheetContent> createState() => __ChatConfigureBottomSheetContentState(); } class __ChatConfigureBottomSheetContentState extends State<_ChatConfigureBottomSheetContent> { late OllamaChatOptions _chatOptions; final _scrollController = ScrollController(); bool _showAdvancedConfigurations = false; @override void initState() { super.initState(); _chatOptions = widget.arguments.chatOptions; } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListView( controller: _scrollController, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(vertical: 16.0), keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, children: [ // The buttons to rename, save as a new model, and delete the chat Row( spacing: 16.0, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(child: _RenameButton()), Expanded(child: _SaveAsNewModelButton()), Expanded(child: _DeleteButton()), ], ), // The chat configurations section const SizedBox(height: 16), _BottomSheetTextField( initialValue: widget.arguments.systemPrompt, labelText: 'System Prompt', infoText: 'The system prompt is the message that the AI will see before generating a response. It is used to provide context to the AI.', type: _BottomSheetTextFieldType.text, onChanged: (value) => widget.arguments.systemPrompt = value ?? '', ), const SizedBox(height: 16), Divider(), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.temperature, labelText: 'Temperature', infoText: 'The temperature of the model. Increasing the temperature will make the model answer more creatively.', type: _BottomSheetTextFieldType.decimalBetween0And1, onChanged: (v) => _chatOptions.temperature = v ?? 0.8, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.seed, labelText: 'Seed', infoText: 'Sets the random number seed to use for generation. Setting this to a specific number will make the model generate the same text for the same prompt.', type: _BottomSheetTextFieldType.number, onChanged: (v) => _chatOptions.seed = v ?? 0, ), // The advanced configurations section TextButton( onPressed: () { setState(() { _showAdvancedConfigurations = !_showAdvancedConfigurations; _scrollController.animateTo( _showAdvancedConfigurations ? _scrollController.position.pixels + 100 : _scrollController.position.minScrollExtent, duration: const Duration(milliseconds: 500), curve: Curves.ease, ); }); }, child: Text( _showAdvancedConfigurations ? 'Hide Advanced Configurations' : 'Show Advanced Configurations', ), ), if (_showAdvancedConfigurations) ...[ _BottomSheetTextField( initialValue: _chatOptions.maxTokens, labelText: 'Max Tokens', infoText: 'Maximum number of tokens to predict when generating text. -1 = infinite generation.', type: _BottomSheetTextFieldType.number, onChanged: (v) => _chatOptions.maxTokens = v ?? -1, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.repeatLastN, labelText: 'Repeat Last N', infoText: 'How far back the model looks to prevent repetition. 0 = disabled, -1 = full context size.', type: _BottomSheetTextFieldType.number, onChanged: (v) => _chatOptions.repeatLastN = v ?? 64, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.contextSize, labelText: 'Context Size', infoText: 'Size of the context window used to generate the next token. A larger context size results in more coherent text.', type: _BottomSheetTextFieldType.number, onChanged: (v) => _chatOptions.contextSize = v ?? 2048, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.repeatPenalty, labelText: 'Repeat Penalty', infoText: 'The penalty for repeating tokens in the output text. 0 = disabled.', type: _BottomSheetTextFieldType.decimal, onChanged: (v) => _chatOptions.repeatPenalty = v ?? 1.1, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.tailFreeSampling, labelText: 'Tail Free Sampling', infoText: 'Controls tail-free sampling to reduce the impact of less probable tokens. 1.0 disables this setting; higher values reduce the impact more.', type: _BottomSheetTextFieldType.decimal, onChanged: (v) => _chatOptions.tailFreeSampling = v ?? 1.0, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.topK, labelText: 'Top K', infoText: 'Limits the probability of generating nonsense. A higher value (e.g., 100) allows more diverse answers, while a lower value (e.g., 10) is more conservative.', type: _BottomSheetTextFieldType.number, onChanged: (v) => _chatOptions.topK = v ?? 40, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.topP, labelText: 'Top P', infoText: 'Works with Top K to control text diversity. Higher values lead to more diverse text, lower values to more focused text.', type: _BottomSheetTextFieldType.decimalBetween0And1, onChanged: (v) => _chatOptions.topP = v ?? 0.9, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.minP, labelText: 'Min P', infoText: 'Ensures a balance of quality and variety by setting a minimum token probability relative to the most likely token. Tokens with lower probability are filtered out.', type: _BottomSheetTextFieldType.decimalBetween0And1, onChanged: (v) => _chatOptions.minP = v ?? 0.0, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.mirostat, labelText: 'Mirostat', infoText: 'Enable Mirostat sampling for controlling perplexity. (default: 0, 0 = disabled, 1 = Mirostat, 2 = Mirostat 2.0)', type: _BottomSheetTextFieldType.number, onChanged: (v) => _chatOptions.mirostat = v ?? 0, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.mirostatEta, labelText: 'Mirostat Eta', infoText: 'Influences how quickly the algorithm responds to feedback from the generated text. A lower value results in slower adjustments; a higher value makes the algorithm more responsive.', type: _BottomSheetTextFieldType.decimalBetween0And1, onChanged: (v) => _chatOptions.mirostatEta = v ?? 0.1, ), const SizedBox(height: 16), _BottomSheetTextField( initialValue: _chatOptions.mirostatTau, labelText: 'Mirostat Tau', infoText: 'Controls the balance between coherence and diversity of the output. A lower value results in more focused and coherent text. A higher value results in more diverse text.', type: _BottomSheetTextFieldType.decimal, onChanged: (v) => _chatOptions.mirostatTau = v ?? 5.0, ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.info_outline_rounded), const SizedBox(width: 8), FlexibleText('Leave empty to use the default value'), ], ), TextButton.icon( label: const Text('Reset to Defaults'), icon: const Icon(Icons.settings_backup_restore_rounded), style: TextButton.styleFrom( foregroundColor: Colors.red, iconColor: Colors.red, iconSize: 24, ), onPressed: () { setState(() { final defaults = ChatConfigureArguments.defaultArguments; widget.arguments.systemPrompt = defaults.systemPrompt; widget.arguments.chatOptions = defaults.chatOptions; }); Navigator.of(context).pop(); }, ), ], ], ); } } class _RenameButton extends StatelessWidget { const _RenameButton({super.key}); @override Widget build(BuildContext context) { return _BottomSheetButton( icon: const Icon(Icons.edit_outlined), title: 'Rename', onPressed: () async { final chatProvider = Provider.of(context, listen: false); final newTitle = await _showRenameDialog( context, currentTitle: chatProvider.currentChat?.title, ); if (newTitle != null) { await chatProvider.updateCurrentChat(newTitle: newTitle); } }, isDisabled: Provider.of(context, listen: false).currentChat == null, ); } Future _showRenameDialog( BuildContext context, { String? currentTitle, }) async { String? newTitle; return await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Rename Chat'), content: TextFormField( initialValue: currentTitle, decoration: const InputDecoration( labelText: 'New Name', border: OutlineInputBorder(), ), textCapitalization: TextCapitalization.sentences, onChanged: (value) => newTitle = value, onTapOutside: (PointerDownEvent event) { FocusManager.instance.primaryFocus?.unfocus(); }), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), TextButton( onPressed: () { if (newTitle != null && newTitle!.trim().isNotEmpty) { Navigator.of(context).pop(newTitle!.trim()); } }, child: const Text('Rename'), ), ], ); }, ); } } class _SaveAsNewModelButton extends StatelessWidget { const _SaveAsNewModelButton({super.key}); @override Widget build(BuildContext context) { return _BottomSheetButton( icon: const Icon(Icons.save_as_outlined), title: 'Save as a new model', onPressed: () async { final chatProvider = Provider.of(context, listen: false); final newModelName = await _showSaveAsNewModelDialog(context); if (newModelName != null) { bool success = false; String errorMessage = ''; try { await chatProvider.saveAsNewModel(newModelName); success = true; } on OllamaException catch (error) { success = false; errorMessage = '\n${error.message}'; } catch (error) { success = false; } final snackBarText = success ? 'Model "$newModelName" saved successfully!' : 'Failed to save model "$newModelName".$errorMessage'; if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(snackBarText), showCloseIcon: true, backgroundColor: success ? Colors.green : Colors.red, ), ); Navigator.of(context).pop(); } } }, ); } Future _showSaveAsNewModelDialog(BuildContext context) { return showDialog( context: context, barrierDismissible: false, builder: (context) => const _SaveAsNewModelDialog(), ); } } class _SaveAsNewModelDialog extends StatefulWidget { const _SaveAsNewModelDialog(); @override State<_SaveAsNewModelDialog> createState() => _SaveAsNewModelDialogState(); } class _SaveAsNewModelDialogState extends State<_SaveAsNewModelDialog> { String _modelName = ''; String? _errorText; @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Save As New Model', maxLines: 2, overflow: TextOverflow.ellipsis), content: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: TextField( decoration: InputDecoration( labelText: 'New Model Name', errorText: _errorText, errorMaxLines: 5, helperText: 'Format: model, namespace/model, or model:tag', helperMaxLines: 5, border: const OutlineInputBorder(), ), onChanged: (value) { setState(() { _modelName = value; _errorText = _validateModelName(value); }); }, onTapOutside: (PointerDownEvent event) { FocusManager.instance.primaryFocus?.unfocus(); }, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), TextButton( onPressed: _isValid ? () => Navigator.of(context).pop(_modelName.trim()) : null, child: const Text('Save'), ), ], ); } /// Model names follow the `[namespace/]model[:tag]` format. /// Each segment must start with an alphanumeric character and /// can contain alphanumeric characters, hyphens, underscores, and dots. static final _modelNamePattern = RegExp( r'^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9][a-zA-Z0-9._-]*)?(:[a-zA-Z0-9][a-zA-Z0-9._-]*)?$', ); bool get _isValid => _modelName.trim().isNotEmpty && _errorText == null; String? _validateModelName(String value) { if (value.trim().isEmpty) { return null; } if (value.contains(' ')) { return 'Model name must not contain spaces'; } if (!_modelNamePattern.hasMatch(value.trim())) { return 'Invalid model name format. Use [namespace/]model[:tag]'; } return null; } } class _DeleteButton extends StatelessWidget { const _DeleteButton({super.key}); @override Widget build(BuildContext context) { return _BottomSheetButton( icon: const Icon(Icons.delete_outline), title: 'Delete', onPressed: () { _showDeleteDialog(context); }, isDestructive: true, isDisabled: Provider.of(context, listen: false).currentChat == null, ); } void _showDeleteDialog(BuildContext context) { showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Delete Chat?'), content: const Text( 'This action can\'t be undone.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), TextButton( onPressed: () { Provider.of(context, listen: false).deleteCurrentChat(); Navigator.of(context) ..pop() ..pop(ChatConfigureBottomSheetAction.delete); }, style: TextButton.styleFrom( foregroundColor: Colors.red, ), child: const Text('Delete'), ), ], ); }, ); } } class _BottomSheetButton extends StatelessWidget { final Icon icon; final String title; final VoidCallback? onPressed; final bool isDisabled; final bool isDestructive; const _BottomSheetButton({ required this.icon, required this.title, required this.onPressed, this.isDisabled = false, this.isDestructive = false, }); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: isDisabled ? null : onPressed, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surfaceContainer, foregroundColor: isDestructive ? Colors.red : null, iconColor: isDestructive ? Colors.red : null, iconSize: 24, padding: const EdgeInsets.all(16.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ icon, FlexibleText( title, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ); } } class _BottomSheetTextField extends StatefulWidget { final T? initialValue; final String labelText; final String infoText; final _BottomSheetTextFieldType type; final Function(T?)? onChanged; const _BottomSheetTextField({ super.key, this.initialValue, required this.labelText, required this.infoText, required this.type, this.onChanged, }); @override State<_BottomSheetTextField> createState() => _BottomSheetTextFieldState(); } class _BottomSheetTextFieldState extends State<_BottomSheetTextField> { String? _errorText; @override Widget build(BuildContext context) { return TextFormField( initialValue: widget.initialValue?.toString(), decoration: InputDecoration( labelText: widget.labelText, hintText: _hintText, errorText: _errorText, border: OutlineInputBorder(), suffixIcon: IconButton( onPressed: () { showDialog( context: context, builder: (context) { return AlertDialog( title: Text(widget.labelText), content: Text(widget.infoText), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Close'), ), ], ); }, ); }, icon: Icon(Icons.info_outline), ), ), onChanged: (value) { final (validValue, errorText) = _validator(value); setState(() => _errorText = errorText); widget.onChanged?.call(validValue); }, keyboardType: _keyboardType, textCapitalization: TextCapitalization.sentences, onTapOutside: (PointerDownEvent event) { FocusManager.instance.primaryFocus?.unfocus(); }, ); } String get _hintText { switch (widget.type) { case _BottomSheetTextFieldType.text: return 'Enter a text'; case _BottomSheetTextFieldType.number: return 'Enter a number'; case _BottomSheetTextFieldType.decimal: return 'Enter a value'; case _BottomSheetTextFieldType.decimalBetween0And1: return 'Enter a value between 0 and 1'; } } (T?, String?) Function(String?) get _validator { switch (widget.type) { case _BottomSheetTextFieldType.text: return (v) { if (v == null) { return (null, '${widget.labelText} must not be empty'); } else if (v.isEmpty) { return (null, null); } else { return (v as T?, null); } }; case _BottomSheetTextFieldType.number: return (v) { if (v == null) { return (null, '${widget.labelText} must not be empty'); } else if (v.isEmpty) { return (null, null); } else if (int.tryParse(v) == null) { return (null, '${widget.labelText} must be a number'); } else { return (int.tryParse(v) as T?, null); } }; case _BottomSheetTextFieldType.decimal: return (value) { final v = value?.replaceAll(',', '.'); if (v == null) { return (null, '${widget.labelText} must not be empty'); } else if (v.isEmpty) { return (null, null); } else if (double.tryParse(v) == null) { return (null, '${widget.labelText} must be a decimal number'); } else { return (double.tryParse(v) as T?, null); } }; case _BottomSheetTextFieldType.decimalBetween0And1: return (value) { final v = value?.replaceAll(',', '.'); if (v == null) { return (null, '${widget.labelText} must not be empty'); } else if (v.isEmpty) { return (null, null); } else if (double.tryParse(v) == null) { return (null, '${widget.labelText} must be a decimal number'); } else { final value = double.parse(v); if (value < 0 || value > 1) { return (null, '${widget.labelText} must be between 0 and 1'); } else { return (double.tryParse(v) as T?, null); } } }; } } TextInputType get _keyboardType { switch (widget.type) { case _BottomSheetTextFieldType.text: return TextInputType.text; case _BottomSheetTextFieldType.number: return TextInputType.number; case _BottomSheetTextFieldType.decimal: return TextInputType.numberWithOptions(decimal: true); case _BottomSheetTextFieldType.decimalBetween0And1: return TextInputType.numberWithOptions(decimal: true); } } } enum _BottomSheetTextFieldType { text, number, decimal, decimalBetween0And1, } enum ChatConfigureBottomSheetAction { delete, } ================================================ FILE: lib/Widgets/chat_drawer.dart ================================================ import 'package:flutter/material.dart'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Providers/chat_provider.dart'; import 'package:provider/provider.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'title_divider.dart'; class ChatDrawer extends StatelessWidget { const ChatDrawer({super.key}); @override Widget build(BuildContext context) { return Drawer( child: SafeArea( child: Column( children: [ const Expanded(child: ChatNavigationDrawer()), Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.fromLTRB(28, 16, 28, 10), child: IconButton( icon: const Icon(Icons.settings_outlined), onPressed: () { if (ResponsiveBreakpoints.of(context).isMobile) { Navigator.pop(context); } Navigator.pushNamed(context, '/settings'); }, ), ), ], ), ), ); } } class ChatNavigationDrawer extends StatelessWidget { const ChatNavigationDrawer({super.key}); @override Widget build(BuildContext context) { return Consumer( builder: (context, chatProvider, _) { return NavigationDrawer( selectedIndex: chatProvider.selectedDestination, onDestinationSelected: (destination) { chatProvider.destinationChatSelected(destination); if (ResponsiveBreakpoints.of(context).isMobile) { Navigator.pop(context); } }, children: [ Padding( padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), child: Text( AppConstants.appName, style: Theme.of(context).textTheme.titleSmall, ), ), const NavigationDrawerDestination( icon: CircleAvatar( backgroundImage: AssetImage(AppConstants.ollamaIconPng), radius: 16, ), label: Text("Ollama"), ), const Padding( padding: EdgeInsets.fromLTRB(28, 16, 28, 10), child: TitleDivider(title: "Chats"), ), ...chatProvider.chats.map((chat) { return NavigationDrawerDestination( icon: const Icon(Icons.chat_outlined), label: Expanded( child: Text( chat.title, overflow: TextOverflow.ellipsis, ), ), selectedIcon: const Icon(Icons.chat), ); }), ], ); }, ); } } ================================================ FILE: lib/Widgets/chat_image.dart ================================================ import 'package:flutter/material.dart'; class ChatImage extends StatelessWidget { final ImageProvider image; final double aspectRatio; final double? height; final double? width; const ChatImage({ super.key, required this.image, this.aspectRatio = 1.0, this.height, this.width, }); @override Widget build(BuildContext context) { return SizedBox( height: height, width: width, child: ClipRRect( borderRadius: BorderRadius.circular(8.0), child: AspectRatio( aspectRatio: aspectRatio, child: Image( image: image, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Center( child: Icon(Icons.error, color: Colors.red), ); }, ), ), ), ); } } ================================================ FILE: lib/Widgets/flexible_text.dart ================================================ import 'package:flutter/material.dart'; /// A [Text] widget wrapped in [Flexible] for use inside [Row] or [Column]. /// /// Prevents text overflow by allowing text to wrap up to [maxLines] /// and truncating with ellipsis if needed. class FlexibleText extends StatelessWidget { final String text; final TextStyle? style; final int? maxLines; final TextOverflow? overflow; final TextAlign? textAlign; final TextScaler? textScaler; const FlexibleText( this.text, { super.key, this.style, this.maxLines, this.overflow, this.textAlign, this.textScaler, }); @override Widget build(BuildContext context) { return Flexible( child: Text( text, style: style, maxLines: maxLines, overflow: overflow, textAlign: textAlign, textScaler: textScaler, ), ); } } ================================================ FILE: lib/Widgets/model_selection_bottom_sheet.dart ================================================ import 'package:flutter/material.dart'; import 'package:async/async.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:provider/provider.dart'; import 'package:reins/Models/model_capabilities.dart'; import 'package:reins/Models/ollama_model.dart'; import 'package:reins/Models/ollama_request_state.dart'; import 'package:reins/Providers/chat_provider.dart'; import 'package:reins/Widgets/ollama_bottom_sheet_header.dart'; class ModelSelectionBottomSheet extends StatefulWidget { final String title; final String? currentModelName; const ModelSelectionBottomSheet({ super.key, required this.title, this.currentModelName, }); @override State createState() => _ModelSelectionBottomSheetState(); } class _ModelSelectionBottomSheetState extends State { static final _modelsBucket = PageStorageBucket(); late final ChatProvider _chatProvider; OllamaModel? _selectedModel; List _models = []; var _state = OllamaRequestState.uninitialized; late CancelableOperation _fetchOperation; /// Cache key derived from server address String get _cacheKey => Hive.box('settings').get('serverAddress') ?? 'default'; @override void initState() { super.initState(); _chatProvider = context.read(); // Load the previous state of the models list _models = _modelsBucket.readState(context, identifier: _cacheKey) ?? []; _selectedModel = _findModelByName(widget.currentModelName); _fetchOperation = CancelableOperation.fromFuture(_fetchModels()); } @override void dispose() { _fetchOperation.cancel(); super.dispose(); } OllamaModel? _findModelByName(String? name) { if (name == null) return null; try { return _models.firstWhere((m) => m.name == name); } catch (_) { return null; } } Future _fetchModels() async { setState(() { _state = OllamaRequestState.loading; }); try { _models = await _chatProvider.fetchAvailableModels(); _state = OllamaRequestState.success; // Update selection if we were searching by name (cache was empty) if (_selectedModel == null && widget.currentModelName != null) { _selectedModel = _findModelByName(widget.currentModelName); } if (mounted) { _modelsBucket.writeState(context, _models, identifier: _cacheKey); } } catch (e) { _state = OllamaRequestState.error; } if (mounted) { setState(() {}); } } @override Widget build(BuildContext context) { return SafeArea( minimum: const EdgeInsets.all(16.0), child: Column( children: [ Row( children: [ Expanded(child: OllamaBottomSheetHeader(title: widget.title)), if (_models.isNotEmpty && _state == OllamaRequestState.loading) const Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: CircularProgressIndicator(), ), ], ), const Divider(), Expanded(child: _buildBody(context)), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), TextButton( onPressed: _selectedModel != null ? () => Navigator.of(context).pop(_selectedModel) : null, child: const Text('Select'), ), ], ), ], ), ); } Widget _buildBody(BuildContext context) { if (_state == OllamaRequestState.error) { return Center( child: Text( 'An error occurred while fetching models.' '\nCheck your server connection and try again.', style: TextStyle(color: Theme.of(context).colorScheme.error), textAlign: TextAlign.center, ), ); } else if (_state == OllamaRequestState.loading && _models.isEmpty) { return const Center(child: CircularProgressIndicator()); } else if (_state == OllamaRequestState.success || _models.isNotEmpty) { if (_models.isEmpty) { return const Center(child: Text('No models found.')); } return RefreshIndicator( onRefresh: () async { _fetchOperation = CancelableOperation.fromFuture(_fetchModels()); }, child: RadioGroup( groupValue: _selectedModel, onChanged: (model) => setState(() => _selectedModel = model), child: ListView.builder( itemCount: _models.length, itemBuilder: (context, index) { return _ModelListTile(model: _models[index]); }, ), ), ); } else { return const SizedBox.shrink(); } } } class _ModelListTile extends StatelessWidget { final OllamaModel model; const _ModelListTile({required this.model}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final capabilities = model.capabilities; return RadioListTile( value: model, title: Text(model.name), subtitle: model.parameterSize.isNotEmpty ? Text( model.parameterSize, style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), ) : null, secondary: capabilities != null ? Row( spacing: 8, mainAxisSize: MainAxisSize.min, children: _buildCapabilityChips(capabilities), ) : null, ); } List _buildCapabilityChips(ModelCapabilities capabilities) { final chips = []; if (capabilities.vision) { chips.add(_CapabilityChip( icon: Icons.visibility_outlined, label: 'Vision', )); } if (capabilities.tools) { chips.add(_CapabilityChip( icon: Icons.build_outlined, label: 'Tools', )); } if (capabilities.thinking) { chips.add(_CapabilityChip( icon: Icons.lightbulb_outline, label: 'Thinking', )); } return chips; } } class _CapabilityChip extends StatelessWidget { final IconData icon; final String label; const _CapabilityChip({ required this.icon, required this.label, }); @override Widget build(BuildContext context) { return Tooltip( message: label, child: Icon(icon, size: 22), ); } } /// Shows a model selection bottom sheet and returns the selected model. /// /// Returns the selected [OllamaModel], or the current model if cancelled. Future showModelSelectionBottomSheet({ required BuildContext context, required String title, String? currentModelName, }) async { return await showModalBottomSheet( context: context, builder: (context) { return ModelSelectionBottomSheet(title: title, currentModelName: currentModelName); }, isDismissible: false, enableDrag: false, ); } ================================================ FILE: lib/Widgets/ollama_bottom_sheet_header.dart ================================================ import 'package:flutter/material.dart'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Widgets/flexible_text.dart'; class OllamaBottomSheetHeader extends StatelessWidget { final String title; const OllamaBottomSheetHeader({super.key, required this.title}); @override Widget build(BuildContext context) { return Row( children: [ Padding( padding: const EdgeInsets.all(16.0), child: ClipRRect( borderRadius: BorderRadius.circular(8.0), child: Image.asset(AppConstants.appIconPng, height: 48), ), ), FlexibleText( title, style: TextStyle(fontWeight: FontWeight.bold), ), ], ); } } ================================================ FILE: lib/Widgets/title_divider.dart ================================================ import 'package:flutter/material.dart'; class TitleDivider extends StatelessWidget { final String title; const TitleDivider({super.key, required this.title}); @override Widget build(BuildContext context) { return Row( children: [ Expanded(child: Divider()), Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: Text(title), ), Expanded(child: Divider()) ], ); } } ================================================ FILE: lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:in_app_review/in_app_review.dart'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Models/settings_route_arguments.dart'; import 'package:reins/Pages/chat_page/chat_page_view_model.dart'; import 'package:reins/Pages/main_page.dart'; import 'package:reins/Pages/settings_page/settings_page.dart'; import 'package:reins/Providers/chat_provider.dart'; import 'package:reins/Services/services.dart'; import 'package:reins/Utils/material_color_adapter.dart'; import 'package:provider/provider.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:reins/Utils/request_review_helper.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'dart:io' show Platform; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); if (Platform.isWindows || Platform.isLinux) { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; } // Initialize PathManager await PathManager.initialize(); // Initialize Hive if (Platform.isLinux) { Hive.init(PathManager.instance.documentsDirectory.path); } else { await Hive.initFlutter(); } Hive.registerAdapter(MaterialColorAdapter()); await Hive.openBox('settings'); // Initialize RequestReviewHelper and request review if needed final reviewHelper = await RequestReviewHelper.initialize(); await reviewHelper.incrementCount(isLaunch: true); final inAppReview = InAppReview.instance; if (await inAppReview.isAvailable() && reviewHelper.shouldRequestReview()) { await inAppReview.requestReview(); } runApp( MultiProvider( providers: [ Provider(create: (_) => OllamaService()), Provider(create: (_) => DatabaseService()), Provider(create: (_) => PermissionService()), Provider(create: (_) => ImageService()), ChangeNotifierProvider( create: (context) => ChatProvider( ollamaService: context.read(), databaseService: context.read(), ), ), ChangeNotifierProvider( create: (context) => ChatPageViewModel( chatProvider: context.read(), permissionService: context.read(), imageService: context.read(), ), ), ], child: const ReinsApp(), ), ); } class ReinsApp extends StatelessWidget { const ReinsApp({super.key}); @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: Hive.box('settings').listenable( keys: ['color', 'brightness'], ), builder: (context, box, _) { return MaterialApp( title: AppConstants.appName, theme: ThemeData( colorScheme: ColorScheme.fromSeed( brightness: _brightness ?? MediaQuery.platformBrightnessOf(context), dynamicSchemeVariant: DynamicSchemeVariant.neutral, seedColor: box.get('color', defaultValue: Colors.grey), ), appBarTheme: const AppBarTheme(centerTitle: true), useMaterial3: true, ), builder: (context, child) => ResponsiveBreakpoints.builder( breakpoints: [ const Breakpoint(start: 0, end: 450, name: MOBILE), const Breakpoint(start: 451, end: 800, name: TABLET), const Breakpoint(start: 801, end: 1920, name: DESKTOP), ], useShortestSide: true, child: child!, ), onGenerateRoute: (settings) { if (settings.name == '/') { return MaterialPageRoute( builder: (context) => const ReinsMainPage(), ); } if (settings.name == '/settings') { final args = settings.arguments as SettingsRouteArguments?; return MaterialPageRoute( builder: (context) => SettingsPage(arguments: args), ); } assert(false, 'Need to implement ${settings.name}'); return null; }, ); }, ); } Brightness? get _brightness { final brightnessValue = Hive.box('settings').get('brightness'); if (brightnessValue == null) return null; return brightnessValue == 1 ? Brightness.light : Brightness.dark; } } ================================================ FILE: linux/.gitignore ================================================ flutter/ephemeral ================================================ FILE: linux/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.13) project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "reins") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "dev.ibrahimcetin.reins") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) # Load bundled libraries from the lib/ directory relative to the binary. set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Root filesystem for cross-building. if(FLUTTER_TARGET_PLATFORM_SYSROOT) set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) endif() # Define build configuration options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of # the default top-level location. set_target_properties(${BINARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() # Start with a clean build bundle directory every time. install(CODE " file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") " COMPONENT Runtime) set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endforeach(bundled_library) # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() ================================================ FILE: linux/flatpak/dev.ibrahimcetin.reins.desktop ================================================ [Desktop Entry] Type=Application Name=Reins Comment=Private AI Chat Categories=Utility;Development;Chat; Keywords=ai;chat;private;ollama;llm Icon=dev.ibrahimcetin.reins Exec=reins Terminal=false StartupNotify=true ================================================ FILE: linux/flatpak/dev.ibrahimcetin.reins.metainfo.xml ================================================ dev.ibrahimcetin.reins Reins: Chat for Ollama Private AI Chat

Reins is a powerful chat client for your self-hosted Ollama AI models. Connect remotely, customize prompts, adjust parameters, and manage multiple conversations with ease.

Key Features:

  • Connect to remote Ollama servers
  • Custom system prompts per chat
  • Edit and regenerate responses
  • Send images
  • Fine-tune model parameters (temperature, context size, etc.) for each chat
  • Switch models mid-conversation
  • Create custom models from chats
  • Real-time message streaming

Requires a self-hosted Ollama Server.

dev.ibrahimcetin.reins dev.ibrahimcetin.reins.desktop İbrahim Çetin Utility Development Chat CC0-1.0 GPL-3.0-only pointing keyboard touch https://reins.ibrahimcetin.dev https://github.com/ibrahimcetin/reins https://github.com/ibrahimcetin/reins/issues https://reins.ibrahimcetin.dev/assets/screenshots/desktop/1.png Chatting with a model using Reins https://reins.ibrahimcetin.dev/assets/screenshots/desktop/2.png Configure your chat's options https://reins.ibrahimcetin.dev/assets/screenshots/desktop/3.png Advanced chat options https://reins.ibrahimcetin.dev/assets/screenshots/desktop/4.png Dynamically switch LLM models https://reins.ibrahimcetin.dev/assets/screenshots/desktop/5.png Select a model for the chat https://reins.ibrahimcetin.dev/assets/screenshots/desktop/6.png Edit and regenerate prompt https://reins.ibrahimcetin.dev/assets/screenshots/desktop/7.png Customize app theme as you want https://reins.ibrahimcetin.dev/assets/screenshots/desktop/8.png Open source and privacy prioritized https://github.com/ibrahimcetin/reins/releases/tag/v1.3.3

Fixes

  • Improved accessibility: all UI now respects system font size settings.
  • Fixed issue where thinking indicator would disappear unexpectedly.
  • Fixed issue where chat titles appeared empty while generating.
https://github.com/ibrahimcetin/reins/releases/tag/v1.3.2

Fixes

  • Fixed an issue where the chat could scroll up automatically while generating a response.
================================================ FILE: linux/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. # Serves the same purpose as list(TRANSFORM ... PREPEND ...), # which isn't available in 3.10. function(list_prepend LIST_NAME PREFIX) set(NEW_LIST "") foreach(element ${${LIST_NAME}}) list(APPEND NEW_LIST "${PREFIX}${element}") endforeach(element) set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) endfunction() # === Flutter Library === # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "fl_basic_message_channel.h" "fl_binary_codec.h" "fl_binary_messenger.h" "fl_dart_project.h" "fl_engine.h" "fl_json_message_codec.h" "fl_json_method_codec.h" "fl_message_codec.h" "fl_method_call.h" "fl_method_channel.h" "fl_method_codec.h" "fl_method_response.h" "fl_plugin_registrar.h" "fl_plugin_registry.h" "fl_standard_message_codec.h" "fl_standard_method_codec.h" "fl_string_codec.h" "fl_value.h" "fl_view.h" "flutter_linux.h" ) list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO ) add_dependencies(flutter flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/_phony_ COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ) ================================================ FILE: linux/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } ================================================ FILE: linux/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: linux/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: linux/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.13) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} "main.cc" "my_application.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add preprocessor definitions for the application ID. add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") ================================================ FILE: linux/runner/main.cc ================================================ #include "my_application.h" int main(int argc, char** argv) { g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } ================================================ FILE: linux/runner/my_application.cc ================================================ #include "my_application.h" #include #ifdef GDK_WINDOWING_X11 #include #endif #include "flutter/generated_plugin_registrant.h" struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). // If running on X and not using GNOME then just use a traditional title bar // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; } } #endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "reins"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { gtk_window_set_title(window, "reins"); } gtk_window_set_default_size(window, 1280, 720); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); } // Implements GApplication::local_command_line. static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { g_warning("Failed to register: %s", error->message); *exit_status = 1; return TRUE; } g_application_activate(application); *exit_status = 0; return TRUE; } // Implements GApplication::startup. static void my_application_startup(GApplication* application) { //MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application startup. G_APPLICATION_CLASS(my_application_parent_class)->startup(application); } // Implements GApplication::shutdown. static void my_application_shutdown(GApplication* application) { //MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application shutdown. G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); } // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_APPLICATION_CLASS(klass)->startup = my_application_startup; G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { // Set the program name to the application ID, which helps various systems // like GTK and desktop environments map this running application to its // corresponding .desktop file. This ensures better integration by allowing // the application to be recognized beyond its binary name. g_set_prgname(APPLICATION_ID); return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } ================================================ FILE: linux/runner/my_application.h ================================================ #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ #include G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication) /** * my_application_new: * * Creates a new Flutter-based application. * * Returns: a new #MyApplication. */ MyApplication* my_application_new(); #endif // FLUTTER_MY_APPLICATION_H_ ================================================ FILE: macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: macos/Flutter/Flutter-Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/Flutter-Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import file_selector_macos import flutter_image_compress_macos import in_app_review import path_provider_foundation import share_plus import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } ================================================ FILE: macos/Podfile ================================================ platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_macos_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end end ================================================ FILE: macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } } ================================================ FILE: macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "info": { "version": 1, "author": "xcode" }, "images": [ { "size": "16x16", "idiom": "mac", "filename": "app_icon_16.png", "scale": "1x" }, { "size": "16x16", "idiom": "mac", "filename": "app_icon_32.png", "scale": "2x" }, { "size": "32x32", "idiom": "mac", "filename": "app_icon_32.png", "scale": "1x" }, { "size": "32x32", "idiom": "mac", "filename": "app_icon_64.png", "scale": "2x" }, { "size": "128x128", "idiom": "mac", "filename": "app_icon_128.png", "scale": "1x" }, { "size": "128x128", "idiom": "mac", "filename": "app_icon_256.png", "scale": "2x" }, { "size": "256x256", "idiom": "mac", "filename": "app_icon_256.png", "scale": "1x" }, { "size": "256x256", "idiom": "mac", "filename": "app_icon_512.png", "scale": "2x" }, { "size": "512x512", "idiom": "mac", "filename": "app_icon_512.png", "scale": "1x" }, { "size": "512x512", "idiom": "mac", "filename": "app_icon_1024.png", "scale": "2x" } ] } ================================================ FILE: macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = reins // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2024 dev.ibrahimcetin. All rights reserved. ================================================ FILE: macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.files.user-selected.read-only com.apple.security.network.client com.apple.security.network.server ================================================ FILE: macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) ITSAppUsesNonExemptEncryption LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only com.apple.security.network.client ================================================ FILE: macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 71F7DA8F04247AE1D19A2DCD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841B3B8083C57E19AD9E9A39 /* Pods_Runner.framework */; }; 9785DAF82BC9CBFE9E6331D8 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 40313F7815DB5898F649E72C /* Pods_RunnerTests.framework */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC10EC2044A3C60003C045; remoteInfo = Runner; }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 036D24F7E68C6137DC488107 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 32612926487FDF8584D285A9 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* Reins.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reins.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 40313F7815DB5898F649E72C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6BA878D82D9B7B2488681BB9 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 6C75AEA4E1BCA2C4E7D8E1F6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 841B3B8083C57E19AD9E9A39 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; D9E98E29FD205FB645BE4B3A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; F523C65FA81D55D22704E750 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 331C80D2294CF70F00263BE5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 9785DAF82BC9CBFE9E6331D8 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 71F7DA8F04247AE1D19A2DCD /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 06C9C8E19A73CAC5DE5685EB /* Pods */ = { isa = PBXGroup; children = ( F523C65FA81D55D22704E750 /* Pods-Runner.debug.xcconfig */, 6C75AEA4E1BCA2C4E7D8E1F6 /* Pods-Runner.release.xcconfig */, D9E98E29FD205FB645BE4B3A /* Pods-Runner.profile.xcconfig */, 036D24F7E68C6137DC488107 /* Pods-RunnerTests.debug.xcconfig */, 6BA878D82D9B7B2488681BB9 /* Pods-RunnerTests.release.xcconfig */, 32612926487FDF8584D285A9 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C80D7294CF71000263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 06C9C8E19A73CAC5DE5685EB /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* Reins.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( 841B3B8083C57E19AD9E9A39 /* Pods_Runner.framework */, 40313F7815DB5898F649E72C /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C80D4294CF70F00263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( DC4AA0769FA3F95E92DA4FCB /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C80DA294CF71000263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { packageProductDependencies = ( 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, ); isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( DAD02DEC3673A023C609651F /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, AAE1BDC4A4DA9744508E12A9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* Reins.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { packageReferences = ( 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 33CC10EC2044A3C60003C045; }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C80D3294CF70F00263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; AAE1BDC4A4DA9744508E12A9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; DAD02DEC3673A023C609651F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; DC4AA0769FA3F95E92DA4FCB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C80D1294CF70F00263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC10EC2044A3C60003C045 /* Runner */; targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 036D24F7E68C6137DC488107 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/reins.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/reins"; }; name = Debug; }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 6BA878D82D9B7B2488681BB9 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/reins.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/reins"; }; name = Release; }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 32612926487FDF8584D285A9 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/reins.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/reins"; }; name = Profile; }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 9HQV48CK77; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Reins; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_NAME = Reins; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 9HQV48CK77; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Reins; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = dev.ibrahimcetin.reins.debug; PRODUCT_NAME = Reins; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 9HQV48CK77; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Reins; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_NAME = Reins; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C80DB294CF71000263BE5 /* Debug */, 331C80DC294CF71000263BE5 /* Release */, 331C80DD294CF71000263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, 338D0CE9231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, 338D0CEA231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, 338D0CEB231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; productName = FlutterGeneratedPluginSwiftPackage; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/RunnerTests/RunnerTests.swift ================================================ import Cocoa import FlutterMacOS import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: pubspec.yaml ================================================ name: reins description: "Best-in-class chat experience for Ollama." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 1.3.4+1 environment: sdk: ^3.5.4 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter # HTTP package for making network requests http: ^1.2.2 async: ^2.11.0 # State management solution provider: ^6.1.2 # SQLite plugin for Flutter sqflite: ^2.4.1 sqflite_common_ffi: ^2.3.4 uuid: ^4.5.1 # Path provider for SQLite path: ^1.9.0 # Hive, key-value store hive: ^2.2.3 hive_flutter: ^1.1.0 # Google Fonts package for custom fonts google_fonts: ^6.2.1 # SVG rendering and widget library flutter_svg: ^2.0.14 # Animation package animated_text_kit: ^4.2.2 shimmer: ^3.0.0 # Responsive framework responsive_framework: ^1.5.1 # Markdown rendering flutter_markdown: ^0.7.4+3 markdown: any url_launcher: ^6.3.1 # Image support image_picker: ^1.1.2 path_provider: ^2.1.5 flutter_image_compress: ^2.3.0 image_compression: ^1.0.5 photo_view: ^0.15.0 permission_handler: ^11.3.1 # In-app review in_app_review: ^2.0.10 share_plus: ^12.0.1 notification_centre: ^0.0.3 dev_dependencies: flutter_test: sdk: flutter # Linting rules for Flutter flutter_lints: ^5.0.0 # Testing framework test: ^1.25.7 # Create custom icons for Android and iOS flutter_launcher_icons: ^0.14.1 # Splash screen generator flutter_native_splash: ^2.4.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - assets/images/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images # For details regarding adding assets from package dependencies, see # https://flutter.dev/to/asset-from-package # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package flutter_launcher_icons: image_path: "assets/images/icons/reins.png" android: true ios: true macos: generate: true image_path: "assets/images/icons/reins-macos.png" windows: generate: true image_path: "assets/images/icons/reins-windows.png" icon_size: 256 flutter_native_splash: color: "#ffffff" color_dark: "#000000" image: assets/images/splash/splash_light.png image_dark: assets/images/splash/splash_dark.png android_12: image: assets/images/splash/splash_light_android12.png image_dark: assets/images/splash/splash_dark_android12.png android: true ios: true web: false ================================================ FILE: test/api_create_request_test.dart ================================================ import 'package:reins/Models/api/create_request.dart'; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:test/test.dart'; void main() { test('fromChat with default options produces no parameters', () async { final chat = OllamaChat( model: 'llama3.2:latest', systemPrompt: 'You are Mario from super mario bros, acting as an assistant.', options: OllamaChatOptions(), title: 'New Chat', ); final request = ApiCreateRequest.fromChat( 'my-model', chat: chat, ); final json = await request.toJson(); expect(json['model'], 'my-model'); expect(json['from'], 'llama3.2:latest'); expect(json['system'], 'You are Mario from super mario bros, acting as an assistant.'); expect(json.containsKey('parameters'), isFalse); expect(json.containsKey('messages'), isFalse); expect(json['stream'], false); }); test('fromChat with non-default temperature includes only that parameter', () async { final chat = OllamaChat( model: 'llama3.2:latest', systemPrompt: 'You are Mario from super mario bros, acting as an assistant.', options: OllamaChatOptions()..temperature = 0.5, title: 'New Chat', ); final request = ApiCreateRequest.fromChat( 'my-model', chat: chat, ); final json = await request.toJson(); expect(json['model'], 'my-model'); expect(json['from'], 'llama3.2:latest'); expect(json['system'], 'You are Mario from super mario bros, acting as an assistant.'); expect(json['parameters'], {'temperature': 0.5}); expect(json.containsKey('messages'), isFalse); expect(json['stream'], false); }); test('fromChat with messages includes them in JSON output', () async { final chat = OllamaChat( model: 'llama3.2:latest', systemPrompt: 'You are Mario from super mario bros, acting as an assistant.', options: OllamaChatOptions()..temperature = 0.5, title: 'New Chat', ); final messages = [ OllamaMessage('Hello!', role: OllamaMessageRole.user), OllamaMessage('How can I help you?', role: OllamaMessageRole.assistant), ]; final request = ApiCreateRequest.fromChat( 'my-model', chat: chat, messages: messages, ); final json = await request.toJson(); expect(json['model'], 'my-model'); expect(json['from'], 'llama3.2:latest'); expect(json['parameters'], {'temperature': 0.5}); expect(json['messages'], hasLength(2)); expect(json['messages'][0]['role'], 'user'); expect(json['messages'][0]['content'], 'Hello!'); expect(json['messages'][1]['role'], 'assistant'); expect(json['messages'][1]['content'], 'How can I help you?'); }); test('fromChat with all non-default options includes all parameters', () async { final options = OllamaChatOptions() ..mirostat = 1 ..mirostatEta = 0.2 ..mirostatTau = 4.0 ..contextSize = 1024 ..repeatLastN = 32 ..repeatPenalty = 1.2 ..temperature = 0.5 ..seed = 42 ..tailFreeSampling = 0.9 ..maxTokens = 100 ..topK = 50 ..topP = 0.6 ..minP = 0.1; final chat = OllamaChat( model: 'llama3.2:latest', systemPrompt: 'You are Mario from super mario bros, acting as an assistant.', options: options, title: 'New Chat', ); final messages = [ OllamaMessage('Hello!', role: OllamaMessageRole.user), OllamaMessage('How can I help you?', role: OllamaMessageRole.assistant), ]; final request = ApiCreateRequest.fromChat( 'my-model', chat: chat, messages: messages, ); final json = await request.toJson(); expect(json['parameters'], { 'mirostat': 1, 'mirostat_eta': 0.2, 'mirostat_tau': 4.0, 'num_ctx': 1024, 'repeat_last_n': 32, 'repeat_penalty': 1.2, 'temperature': 0.5, 'seed': 42, 'tfs_z': 0.9, 'num_predict': 100, 'top_k': 50, 'top_p': 0.6, 'min_p': 0.1, }); expect(json['messages'], hasLength(2)); }); test('fromChat with no system prompt omits system from JSON', () async { final chat = OllamaChat( model: 'llama3.2:latest', options: OllamaChatOptions(), title: 'New Chat', ); final request = ApiCreateRequest.fromChat( 'my-model', chat: chat, ); final json = await request.toJson(); expect(json.containsKey('system'), isFalse); }); test('fromChat with empty messages list omits messages', () async { final chat = OllamaChat( model: 'llama3.2:latest', systemPrompt: 'Test prompt', options: OllamaChatOptions(), title: 'New Chat', ); final request = ApiCreateRequest.fromChat( 'my-model', chat: chat, messages: [], ); final json = await request.toJson(); expect(json.containsKey('messages'), isFalse); }); } ================================================ FILE: test/chat_page_view_model_test.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path/path.dart' as path; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_exception.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:reins/Models/ollama_model.dart'; import 'package:reins/Pages/chat_page/chat_page_view_model.dart'; import 'package:reins/Providers/chat_provider.dart'; import 'package:reins/Services/services.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late FakeChatProvider fakeChatProvider; late FakePermissionService fakePermissionService; late FakeImageService fakeImageService; late ChatPageViewModel viewModel; setUpAll(() async { // Setup fake path provider for Hive PathProviderPlatform.instance = FakePathProviderPlatform(); // Initialize Hive for testing final testDir = path.join(Directory.current.path, 'test', 'assets'); Hive.init(testDir); await Hive.openBox('settings'); }); setUp(() async { fakeChatProvider = FakeChatProvider(); fakePermissionService = FakePermissionService(); fakeImageService = FakeImageService(); // Ensure server is configured for most tests await Hive.box('settings').put('serverAddress', 'http://localhost:11434'); viewModel = ChatPageViewModel( chatProvider: fakeChatProvider, permissionService: fakePermissionService, imageService: fakeImageService, ); }); tearDown(() { viewModel.dispose(); }); tearDownAll(() async { await Hive.close(); }); group('Initial State', () { test('selectedModel should be null initially', () { expect(viewModel.selectedModel, isNull); }); test('presets should not be empty', () { expect(viewModel.presets, isNotEmpty); }); test('hasText should be false initially', () { expect(viewModel.hasText, isFalse); }); test('imageFiles should be empty initially', () { expect(viewModel.imageFiles, isEmpty); }); test('hasImageAttachments should be false initially', () { expect(viewModel.hasImageAttachments, isFalse); }); }); group('Model Selection', () { test('setSelectedModel should update selectedModel', () { final model = createTestModel('llama3.2'); viewModel.setSelectedModel(model); expect(viewModel.selectedModel, model); }); test('setSelectedModel should notify listeners', () { final model = createTestModel('llama3.2'); var notified = false; viewModel.addListener(() => notified = true); viewModel.setSelectedModel(model); expect(notified, isTrue); }); test('setSelectedModel with null should clear selection', () { final model = createTestModel('llama3.2'); viewModel.setSelectedModel(model); viewModel.setSelectedModel(null); expect(viewModel.selectedModel, isNull); }); }); group('Text Field', () { test('setTextFieldValue should update text field', () { viewModel.setTextFieldValue('Hello'); expect(viewModel.textFieldController.text, 'Hello'); }); test('hasText should return true when text field has content', () { viewModel.setTextFieldValue('Hello'); expect(viewModel.hasText, isTrue); }); test('hasText should return false for whitespace only', () { viewModel.setTextFieldValue(' '); expect(viewModel.hasText, isFalse); }); test('textFieldController changes should notify listeners', () { var notifyCount = 0; viewModel.addListener(() => notifyCount++); viewModel.textFieldController.text = 'Test'; expect(notifyCount, 1); }); }); group('ChatProvider State (Proxied)', () { test('messages should proxy ChatProvider messages', () { final messages = [ OllamaMessage('Hello', role: OllamaMessageRole.user), ]; fakeChatProvider.setMessages(messages); expect(viewModel.messages, messages); }); test('currentChat should proxy ChatProvider currentChat', () { final chat = createTestChat('test-id'); fakeChatProvider.setCurrentChat(chat); expect(viewModel.currentChat, chat); }); test('isStreaming should proxy ChatProvider isCurrentChatStreaming', () { fakeChatProvider.setIsStreaming(true); expect(viewModel.isStreaming, isTrue); }); test('isThinking should proxy ChatProvider isCurrentChatThinking', () { fakeChatProvider.setIsThinking(true); expect(viewModel.isThinking, isTrue); }); test('currentError should proxy ChatProvider currentChatError', () { final error = OllamaException('Test error'); fakeChatProvider.setCurrentError(error); expect(viewModel.currentError, error); }); test('ChatProvider changes should notify ViewModel listeners', () { var notified = false; viewModel.addListener(() => notified = true); fakeChatProvider.triggerNotifyListeners(); expect(notified, isTrue); }); }); group('ChatProvider Actions (Delegated)', () { test('cancelStreaming should delegate to ChatProvider', () { viewModel.cancelStreaming(); expect(fakeChatProvider.cancelStreamingCalled, isTrue); }); test('retryLastPrompt should delegate to ChatProvider', () async { await viewModel.retryLastPrompt(); expect(fakeChatProvider.retryLastPromptCalled, isTrue); }); test('fetchAvailableModels should delegate to ChatProvider', () async { final models = [createTestModel('llama3.2')]; fakeChatProvider.setAvailableModels(models); final result = await viewModel.fetchAvailableModels(); expect(result, models); }); }); group('sendMessage', () { test('should return false when text field is empty', () async { final result = await viewModel.sendMessage( onModelSelectionRequired: () async {}, onServerNotConfigured: () {}, ); expect(result, isFalse); }); test('should return false when currently streaming', () async { viewModel.setTextFieldValue('Hello'); fakeChatProvider.setIsStreaming(true); final result = await viewModel.sendMessage( onModelSelectionRequired: () async {}, onServerNotConfigured: () {}, ); expect(result, isFalse); }); test('should call onServerNotConfigured when server not configured', () async { await Hive.box('settings').delete('serverAddress'); viewModel.setTextFieldValue('Hello'); var serverNotConfiguredCalled = false; final result = await viewModel.sendMessage( onModelSelectionRequired: () async {}, onServerNotConfigured: () => serverNotConfiguredCalled = true, ); expect(result, isFalse); expect(serverNotConfiguredCalled, isTrue); }); test('should call onModelSelectionRequired when no model selected and no current chat', () async { viewModel.setTextFieldValue('Hello'); var modelSelectionCalled = false; await viewModel.sendMessage( onModelSelectionRequired: () async { modelSelectionCalled = true; }, onServerNotConfigured: () {}, ); expect(modelSelectionCalled, isTrue); }); test('should return false if no model selected after selection callback', () async { viewModel.setTextFieldValue('Hello'); final result = await viewModel.sendMessage( onModelSelectionRequired: () async {}, onServerNotConfigured: () {}, ); expect(result, isFalse); }); test('should create new chat and send message when model selected', () async { viewModel.setTextFieldValue('Hello'); final model = createTestModel('llama3.2'); viewModel.setSelectedModel(model); final result = await viewModel.sendMessage( onModelSelectionRequired: () async {}, onServerNotConfigured: () {}, ); expect(result, isTrue); expect(fakeChatProvider.createNewChatCalled, isTrue); expect(fakeChatProvider.sendPromptCalled, isTrue); expect(fakeChatProvider.generateTitleCalled, isTrue); }); test('should clear text field after sending', () async { viewModel.setTextFieldValue('Hello'); viewModel.setSelectedModel(createTestModel('llama3.2')); await viewModel.sendMessage( onModelSelectionRequired: () async {}, onServerNotConfigured: () {}, ); expect(viewModel.textFieldController.text, isEmpty); }); test('should send message directly when current chat exists', () async { viewModel.setTextFieldValue('Hello'); fakeChatProvider.setCurrentChat(createTestChat('test-id')); final result = await viewModel.sendMessage( onModelSelectionRequired: () async {}, onServerNotConfigured: () {}, ); expect(result, isTrue); expect(fakeChatProvider.createNewChatCalled, isFalse); expect(fakeChatProvider.sendPromptCalled, isTrue); expect(fakeChatProvider.generateTitleCalled, isFalse); }); }); group('isServerConfigured', () { test('should return true when serverAddress is set', () { expect(viewModel.isServerConfigured, isTrue); }); test('should return false when serverAddress is null', () async { await Hive.box('settings').delete('serverAddress'); expect(viewModel.isServerConfigured, isFalse); }); }); } // ============================================================ // Test Helpers // ============================================================ OllamaModel createTestModel(String name) { return OllamaModel( name: name, model: name, modifiedAt: DateTime.now(), size: 1000, digest: 'test-digest-$name', parameterSize: '1B', ); } OllamaChat createTestChat(String id) { return OllamaChat( id: id, model: 'llama3.2', title: 'Test Chat', options: OllamaChatOptions(), systemPrompt: null, ); } // ============================================================ // Fake Classes // ============================================================ class FakeChatProvider extends ChangeNotifier implements ChatProvider { List _messages = []; OllamaChat? _currentChat; bool _isStreaming = false; bool _isThinking = false; OllamaException? _currentError; List _availableModels = []; bool cancelStreamingCalled = false; bool retryLastPromptCalled = false; bool createNewChatCalled = false; bool sendPromptCalled = false; bool generateTitleCalled = false; String? lastSentPrompt; List? lastSentImages; void setMessages(List messages) { _messages = messages; } void setCurrentChat(OllamaChat? chat) { _currentChat = chat; } void setIsStreaming(bool value) { _isStreaming = value; } void setIsThinking(bool value) { _isThinking = value; } void setCurrentError(OllamaException? error) { _currentError = error; } void setAvailableModels(List models) { _availableModels = models; } void triggerNotifyListeners() { notifyListeners(); } @override List get messages => _messages; @override OllamaChat? get currentChat => _currentChat; @override bool get isCurrentChatStreaming => _isStreaming; @override bool get isCurrentChatThinking => _isThinking; @override OllamaException? get currentChatError => _currentError; @override void cancelCurrentStreaming() { cancelStreamingCalled = true; } @override Future retryLastPrompt() async { retryLastPromptCalled = true; } @override Future> fetchAvailableModels() async { return _availableModels; } @override Future createNewChat(OllamaModel model) async { createNewChatCalled = true; _currentChat = createTestChat('new-chat-id'); } @override Future sendPrompt(String prompt, {List? images}) async { sendPromptCalled = true; lastSentPrompt = prompt; lastSentImages = images; } @override Future generateTitleForCurrentChat() async { generateTitleCalled = true; } // Unused ChatProvider methods - stub implementations @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class FakePermissionService implements PermissionService { bool shouldGrantPermission = true; bool permissionRequested = false; @override Future requestPhotoPermission({VoidCallback? onDenied}) async { permissionRequested = true; if (!shouldGrantPermission) { onDenied?.call(); } return shouldGrantPermission; } } class FakeImageService implements ImageService { List deletedImages = []; File? compressedFile; @override Future compressAndSave(String sourcePath, {int quality = 10}) async { return compressedFile; } @override Future deleteImage(File imageFile) async { deletedImages.add(imageFile); } @override Future deleteImages(List imageFiles) async { deletedImages.addAll(imageFiles); } @override Future getImagesDirectory() async { return Directory.systemTemp; } } class FakePathProviderPlatform extends Fake with MockPlatformInterfaceMixin implements PathProviderPlatform { @override Future getApplicationDocumentsPath() async { return path.join(Directory.current.path, 'test', 'assets'); } } ================================================ FILE: test/database_service_test.dart ================================================ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:reins/Constants/constants.dart'; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:reins/Services/database_service.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:path/path.dart' as path; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() async { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; PathProviderPlatform.instance = FakePathProviderPlatform(); await PathManager.initialize(); final databasePath = path.join(await getDatabasesPath(), 'test_database.db'); await databaseFactoryFfi.deleteDatabase(databasePath); final service = DatabaseService(); await service.open('test_database.db'); const model = "llama3.2"; final assetsPath = path.join(Directory.current.path, 'test', 'assets'); final imageFile = File(path.join(assetsPath, 'images', 'ollama.png')); test("Test database open", () async { await service.open('test_database.db'); }); test("Test database create chat", () async { final chat = await service.createChat(model); expect(chat.id, isNotEmpty); expect(chat.model, model); expect(chat.title, "New Chat"); expect(chat.systemPrompt, isNull); expect(chat.options.toJson(), OllamaChatOptions().toJson()); }); test("Test database get chat", () async { final chat = await service.createChat(model); final retrievedChat = (await service.getChat(chat.id))!; expect(retrievedChat.id, chat.id); expect(retrievedChat.model, chat.model); expect(retrievedChat.title, chat.title); expect(retrievedChat.systemPrompt, chat.systemPrompt); expect(retrievedChat.options.toJson(), chat.options.toJson()); }); test("Test database update chat title", () async { final chat = await service.createChat(model); await service.updateChat(chat, newModel: "llama3.2"); final updatedChat = (await service.getChat(chat.id))!; expect(updatedChat.model, "llama3.2"); expect(updatedChat.title, "New Chat"); expect(updatedChat.systemPrompt, isNull); expect(chat.options.toJson(), OllamaChatOptions().toJson()); }); test('Test database update chat system prompt', () async { const systemPrompt = "You are Mario from super mario bros, acting as an assistant."; final chat = await service.createChat(model); await service.updateChat( chat, newSystemPrompt: systemPrompt, ); final updatedChat = (await service.getChat(chat.id))!; expect(updatedChat.model, model); expect(updatedChat.title, "New Chat"); expect(updatedChat.systemPrompt, systemPrompt); expect(chat.options.toJson(), OllamaChatOptions().toJson()); await service.updateChat(updatedChat, newSystemPrompt: null); }); test('Test database update chat options', () async { final chat = await service.createChat(model); await service.updateChat( chat, newOptions: OllamaChatOptions( mirostat: 1, mirostatEta: 0.1, mirostatTau: 0.1, contextSize: 1, repeatLastN: 1, repeatPenalty: 0.1, temperature: 0.1, seed: 1, ), ); final updatedChat = (await service.getChat(chat.id))!; expect(updatedChat.model, model); expect(updatedChat.title, "New Chat"); expect(updatedChat.systemPrompt, isNull); expect(updatedChat.options.mirostat, 1); expect(updatedChat.options.mirostatEta, 0.1); expect(updatedChat.options.mirostatTau, 0.1); expect(updatedChat.options.contextSize, 1); expect(updatedChat.options.repeatLastN, 1); expect(updatedChat.options.repeatPenalty, 0.1); expect(updatedChat.options.temperature, 0.1); expect(updatedChat.options.seed, 1); }); test("Test database delete chat", () async { final chat = await service.createChat(model); await service.deleteChat(chat.id); expect(await service.getChat(chat.id), isNull); }); test('Test database delete chat with images', () async { List images = []; for (var i = 0; i < 10; i++) { final image = File(path.join(assetsPath, 'images', 'test_image$i.png')); await imageFile.copy(image.path); images.add(image); } final chat = await service.createChat(model); for (final image in images) { await service.addMessage( OllamaMessage( "Hello, this is a test message.", images: [image], role: OllamaMessageRole.user, ), chat: chat, ); } await service.deleteChat(chat.id); expect(await service.getChat(chat.id), isNull); // Wait for the images to be deleted await Future.delayed(Duration(seconds: 1)); for (final image in images) { expect(await image.exists(), isFalse); } }); test("Test database get all chats", () async { await service.createChat(model); final chats = await service.getAllChats(); if (chats.isNotEmpty) { expect(chats.first.id, isNotEmpty); expect(chats.first.model, model); expect(chats.first.title, "New Chat"); expect(chats.first.systemPrompt, isNull); expect(chats.first.options.toJson(), OllamaChatOptions().toJson()); } }, retry: 5); test("Test database add message", () async { final chat = await service.createChat(model); final message = OllamaMessage( "Hello, this is a test message.", role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); final messages = await service.getMessages(chat.id); expect(messages.length, 1); expect(messages.first.id, message.id); expect(messages.first.content, message.content); expect(messages.first.role, message.role); }); test('Test database add message with images', () async { final chat = await service.createChat(model); final message = OllamaMessage( "Hello, this is a test message.", images: [imageFile], role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); final messages = await service.getMessages(chat.id); expect(messages.length, 1); expect(messages.first.id, message.id); expect(messages.first.content, message.content); expect(messages.first.images!.first.path, message.images!.first.path); expect(messages.first.role, message.role); }); test("Test database get message", () async { final chat = await service.createChat(model); final message = OllamaMessage( "Hello, this is a test message.", role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); final retrievedMessage = await service.getMessage(message.id); expect(retrievedMessage, isNotNull); expect(retrievedMessage!.id, message.id); expect(retrievedMessage.content, message.content); expect(retrievedMessage.role, message.role); }); test('Test database get message with images', () async { final chat = await service.createChat(model); final message = OllamaMessage( "Hello, this is a test message.", images: [imageFile], role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); final retrievedMessage = await service.getMessage(message.id); expect(retrievedMessage, isNotNull); expect(retrievedMessage!.id, message.id); expect(retrievedMessage.content, message.content); expect(retrievedMessage.images!.first.path, message.images!.first.path); expect(retrievedMessage.role, message.role); }); test('Test database update message', () async { final chat = await service.createChat(model); final message = OllamaMessage("Message", role: OllamaMessageRole.user); await service.addMessage(message, chat: chat); await service.updateMessage(message, newContent: "Updated message"); final retrievedMessage = (await service.getMessage(message.id))!; expect(retrievedMessage, isNotNull); expect(retrievedMessage.id, message.id); expect(retrievedMessage.content, 'Updated message'); expect(retrievedMessage.role, message.role); }); test('Test database delete message', () async { final chat = await service.createChat(model); final message = OllamaMessage( "Hello, this is a test message.", role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); expect(await service.getMessage(message.id), isNotNull); await service.deleteMessage(message.id); expect(await service.getMessage(message.id), isNull); }); test('Test database delete message with images', () async { final testImagePath = path.join(assetsPath, 'images', 'test_image.png'); await imageFile.copy(testImagePath); final testImageFile = File(testImagePath); final chat = await service.createChat(model); final message = OllamaMessage( "Hello, this is a test message.", images: [testImageFile], role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); expect(await service.getMessage(message.id), isNotNull); await service.deleteMessage(message.id); expect(await service.getMessage(message.id), isNull); // Wait for the image to be deleted await Future.delayed(Duration(seconds: 1)); expect(await testImageFile.exists(), isFalse); }); test("Test database get messages", () async { final chat = await service.createChat(model); final message = OllamaMessage( "Hello, this is a test message.", role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); final messages = await service.getMessages(chat.id); expect(messages.length, 1); expect(messages.first.id, message.id); expect(messages.first.content, message.content); expect(messages.first.role, message.role); }); test("Test database delete messages", () async { final chat = await service.createChat(model); final message = OllamaMessage( "Hello, this is a test message.", role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); expect(await service.getMessage(message.id), isNotNull); await service.deleteMessages([message]); expect(await service.getMessage(message.id), isNull); }); test('Test database delete messages with images', () async { List images = []; for (var i = 0; i < 10; i++) { final image = File(path.join(assetsPath, 'images', 'test_image$i.png')); await imageFile.copy(image.path); images.add(image); } final chat = await service.createChat(model); List messages = []; for (final image in images) { final message = OllamaMessage( "Hello, this is a test message.", images: [image], role: OllamaMessageRole.user, ); await service.addMessage(message, chat: chat); messages.add(message); } await service.deleteMessages(messages); for (final message in messages) { expect(await service.getMessage(message.id), isNull); } // Wait for the images to be deleted await Future.delayed(Duration(seconds: 1)); for (final image in images) { expect(await image.exists(), isFalse); } }); } class FakePathProviderPlatform extends Fake with MockPlatformInterfaceMixin implements PathProviderPlatform { @override Future getApplicationDocumentsPath() async { return path.join(Directory.current.path, 'test', 'assets'); } } ================================================ FILE: test/ollama_service_test.dart ================================================ import 'dart:io'; import 'package:path/path.dart' as path; import 'package:reins/Models/ollama_chat.dart'; import 'package:reins/Services/ollama_service.dart'; import 'package:reins/Models/ollama_message.dart'; import 'package:test/test.dart'; void main() { final service = OllamaService(); const model = "llama3.2:latest"; final chat = OllamaChat( model: model, title: "Test chat", systemPrompt: "You are a pirate who don't talk too much, acting as an assistant.", ); chat.options.temperature = 0; chat.options.seed = 1453; const ollamaChatResponseText = '''*nods* Alright then...\n\n```dart\nprint('Hello, world!');\n```\n\n*hands over a piece of parchment with the code on it*'''; final assetsPath = path.join(Directory.current.path, 'test', 'assets'); final imageFile = File(path.join(assetsPath, 'images', 'ollama.png')); final chatForImage = OllamaChat( model: 'llama3.2-vision:latest', title: "Test chat", systemPrompt: "You are a pirate who don't talk too much, acting as an assistant.", ); chatForImage.options.temperature = 0; chatForImage.options.seed = 1453; test("Test Ollama generate endpoint (non-stream)", () async { final message = await service.generate("Hello", chat: chat); expect(message.content, "How can I assist you today?"); }); test("Test Ollama generate endpoint (stream)", () async { final stream = service.generateStream("Hello", chat: chat); var ollamaMessage = ""; await for (final message in stream) { ollamaMessage += message.content; } expect(ollamaMessage, "How can I assist you today?"); }); test("Test Ollama chat endpoint (non-stream)", () async { final message = await service.chat( [ OllamaMessage( "Hello!", role: OllamaMessageRole.user, ), OllamaMessage( "*grunts* Ye be lookin' fer somethin', matey?", role: OllamaMessageRole.assistant, ), OllamaMessage( "Write me a dart code which prints 'Hello, world!'.", role: OllamaMessageRole.user, ), ], chat: chat, ); expect(message.content, ollamaChatResponseText); }); test("Test Ollama chat endpoint (stream)", () async { final stream = service.chatStream( [ OllamaMessage( "Hello!", role: OllamaMessageRole.user, ), OllamaMessage( "*grunts* Ye be lookin' fer somethin', matey?", role: OllamaMessageRole.assistant, ), OllamaMessage( "Write me a dart code which prints 'Hello, world!'.", role: OllamaMessageRole.user, ), ], chat: chat, ); List ollamaMessages = []; await for (final message in stream) { ollamaMessages.add(message.content); } expect(ollamaMessages.join(), ollamaChatResponseText); }); test('Test Ollama chat endpoint with images (stream)', () async { final stream = service.chatStream( [ OllamaMessage( "Hello!, What is in the image?", images: [imageFile], role: OllamaMessageRole.user, ), ], chat: chatForImage, ); List ollamaMessages = []; await for (final message in stream) { ollamaMessages.add(message.content); } final message = ollamaMessages.join(); expect( message, '* The image features a simple black and white line drawing of an alpaca\'s head.\n' '* The alpaca has two small ears on top of its head, large eyes, and a small nose.\n' '* Its mouth is closed, giving it a calm expression.', ); }, timeout: Timeout.none); test("Test listModels returns models from /api/tags", () async { final models = await service.listModels(); expect(models, isNotEmpty); expect(models.map((e) => e.model).contains(model), true); // Verify model structure final testModel = models.firstWhere((m) => m.model == model); expect(testModel.name, isNotEmpty); expect(testModel.digest, isNotEmpty); expect(testModel.size, greaterThan(0)); }); test("Test listModels enriches models with capabilities from /api/show", () async { final models = await service.listModels(); expect(models, isNotEmpty); // Find a model that should have capabilities final testModel = models.firstWhere((m) => m.model == model); // Capabilities should be populated from /api/show expect(testModel.capabilities, isNotNull); expect(testModel.capabilities!.completion, isTrue); }); test("Test listModels returns vision capability for vision models", () async { final models = await service.listModels(); // Find vision model if available final visionModel = models.where((m) => m.model.contains('vision')).firstOrNull; if (visionModel != null) { expect(visionModel.capabilities, isNotNull); expect(visionModel.capabilities!.vision, isTrue); } }); test("Test Ollama create endpoint without messages", () async { await service.createModel("test_model", chat: chat); }); test("Test Ollama create endpoint", () async { final messages = [ OllamaMessage( "Hello!", role: OllamaMessageRole.user, ), OllamaMessage( "*grunts* Ye be lookin' fer somethin', matey?", role: OllamaMessageRole.assistant, ), OllamaMessage( "Write me a dart code which prints 'Hello, world!'.", role: OllamaMessageRole.user, ), ]; await service.createModel( "test_model_with_messages", chat: chat, messages: messages, ); }); test("Test Ollama delete endpoint", () async { await service.deleteModel("test_model:latest"); await service.deleteModel("test_model_with_messages:latest"); }); test("Test constructUrl with various base URLs", () { // Test with trailing slash var service = OllamaService(baseUrl: "http://localhost:11434/"); expect(service.constructUrl("/api/chat").toString(), "http://localhost:11434/api/chat"); expect(service.constructUrl("api/generate").toString(), "http://localhost:11434/api/generate"); // Test without trailing slash service = OllamaService(baseUrl: "http://localhost:11434"); expect(service.constructUrl("/api/tags").toString(), "http://localhost:11434/api/tags"); expect(service.constructUrl("api/models").toString(), "http://localhost:11434/api/models"); // Test with path component service = OllamaService(baseUrl: "http://localhost:11434/ollama"); expect(service.constructUrl("/api/chat").toString(), "http://localhost:11434/ollama/api/chat"); expect(service.constructUrl("api/generate").toString(), "http://localhost:11434/ollama/api/generate"); // Test with path component and trailing slash service = OllamaService(baseUrl: "http://localhost:11434/ollama/"); expect(service.constructUrl("/api/chat").toString(), "http://localhost:11434/ollama/api/chat"); expect(service.constructUrl("api/generate").toString(), "http://localhost:11434/ollama/api/generate"); // Test with IP address service = OllamaService(baseUrl: "http://192.168.1.100:11434"); expect(service.constructUrl("/api/chat").toString(), "http://192.168.1.100:11434/api/chat"); expect(service.constructUrl("api/generate").toString(), "http://192.168.1.100:11434/api/generate"); // Test with subdomain service = OllamaService(baseUrl: "http://ollama.mydomain.com/"); expect(service.constructUrl("/api/chat").toString(), "http://ollama.mydomain.com/api/chat"); // Test with HTTPS service = OllamaService(baseUrl: "https://ollama.mydomain.com"); expect(service.constructUrl("/api/chat").toString(), "https://ollama.mydomain.com/api/chat"); // Test setting baseUrl after initialization service = OllamaService(); service.baseUrl = "http://newhost:11434/"; expect(service.constructUrl("/api/chat").toString(), "http://newhost:11434/api/chat"); }); } ================================================ FILE: web/index.html ================================================ reins ================================================ FILE: web/manifest.json ================================================ { "name": "reins", "short_name": "reins", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: windows/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.14) project(reins LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "reins") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() # Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: windows/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # Set fallback configurations for older versions of the flutter tool. if (NOT DEFINED FLUTTER_TARGET_PLATFORM) set(FLUTTER_TARGET_PLATFORM "windows-x64") endif() # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } ================================================ FILE: windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows permission_handler_windows share_plus url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add preprocessor definitions for the build version. target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else #define VERSION_AS_NUMBER 1,0,0,0 #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "dev.ibrahimcetin" "\0" VALUE "FileDescription", "reins" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "reins" "\0" VALUE "LegalCopyright", "Copyright (C) 2024 dev.ibrahimcetin. All rights reserved." "\0" VALUE "OriginalFilename", "reins.exe" "\0" VALUE "ProductName", "reins" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); // Flutter can complete the first frame before the "show window" callback is // registered. The following call ensures a frame is pending to ensure the // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.Create(L"reins", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } unsigned int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr) -1; // remove the trailing null character int input_length = (int)wcslen(utf16_string); std::string utf8_string; if (target_length == 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include #include "resource.h" namespace { /// Window attribute that enables dark mode window decorations. /// /// Redefined in case the developer's machine has a Windows SDK older than /// version 10.0.22000.0. /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; /// Registry key for app theme preference. /// /// A value of 0 indicates apps should use dark mode. A non-zero or missing /// value indicates apps should use light mode. constexpr const wchar_t kGetPreferredBrightnessRegKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); } FreeLibrary(user32_module); } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::Create(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } UpdateTheme(window); return OnCreate(); } bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; case WM_DWMCOLORIZATIONCOLORCHANGED: UpdateTheme(hwnd); return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } void Win32Window::UpdateTheme(HWND const window) { DWORD light_mode; DWORD light_mode_size = sizeof(light_mode); LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, &light_mode, &light_mode_size); if (result == ERROR_SUCCESS) { BOOL enable_dark_mode = light_mode == 0; DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enable_dark_mode, sizeof(enable_dark_mode)); } } ================================================ FILE: windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size this function will scale the inputted width and height as // as appropriate for the default monitor. The window is invisible until // |Show| is called. Returns true if the window was created successfully. bool Create(const std::wstring& title, const Point& origin, const Size& size); // Show the current window. Returns true if the window was successfully shown. bool Show(); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; // Update the window frame's theme to match the system theme. static void UpdateTheme(HWND const window); bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_