From 32dddd62cb824dc98195d44652e5a62c93bb23ca Mon Sep 17 00:00:00 2001 From: Anze Mavric Date: Thu, 21 Jul 2022 14:56:05 +0200 Subject: [PATCH] first commit --- .gitignore | 47 + .metadata | 30 + LICENSE | 630 +++++++++++++ README.md | 0 analysis_options.yaml | 29 + android/.gitignore | 13 + android/app/build.gradle | 82 ++ android/app/src/debug/AndroidManifest.xml | 8 + android/app/src/main/AndroidManifest.xml | 20 + .../com/example/portarius/MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1895 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1311 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2642 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4011 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5493 bytes .../app/src/main/res/values-night/styles.xml | 18 + android/app/src/main/res/values/styles.xml | 18 + android/app/src/profile/AndroidManifest.xml | 8 + android/build.gradle | 31 + android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + android/settings.gradle | 11 + assets/icons/icon.png | Bin 0 -> 182920 bytes lib/components/appbar/appbar.dart | 113 +++ lib/components/buttons/big_blue_button.dart | 43 + lib/components/cards/about_tile.dart | 95 ++ lib/components/cards/docker_card.dart | 265 ++++++ lib/components/cards/setting_tile.dart | 49 + lib/components/cards/status_card.dart | 41 + lib/components/drawer/drawer.dart | 216 +++++ lib/components/lists/container_grid_list.dart | 185 ++++ .../text_components/double_text.dart | 23 + lib/main.dart | 200 +++++ lib/models/docker/detailed_container.dart | 839 ++++++++++++++++++ lib/models/docker/docker_container.dart | 290 ++++++ lib/models/hive/token.g.dart | 41 + lib/models/hive/user.g.dart | 50 ++ lib/models/portainer/endpoint.dart | 617 +++++++++++++ lib/models/portainer/token.dart | 36 + lib/models/portainer/user.dart | 124 +++ lib/pages/auth/authpage.dart | 271 ++++++ lib/pages/container/container_details.dart | 319 +++++++ lib/pages/home/home.dart | 149 ++++ lib/pages/loading/loading.dart | 85 ++ lib/pages/settings/settings.dart | 210 +++++ lib/pages/users/user_managment.dart | 339 +++++++ lib/pages/wrapper.dart | 70 ++ lib/services/local_auth.dart | 46 + lib/services/remote.dart | 285 ++++++ lib/services/storage.dart | 225 +++++ lib/utils/settings.dart | 59 ++ lib/utils/style.dart | 23 + pubspec.lock | 831 +++++++++++++++++ pubspec.yaml | 90 ++ 56 files changed, 7213 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 LICENSE create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/example/portarius/MainActivity.kt create mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/values-night/styles.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/settings.gradle create mode 100644 assets/icons/icon.png create mode 100644 lib/components/appbar/appbar.dart create mode 100644 lib/components/buttons/big_blue_button.dart create mode 100644 lib/components/cards/about_tile.dart create mode 100644 lib/components/cards/docker_card.dart create mode 100644 lib/components/cards/setting_tile.dart create mode 100644 lib/components/cards/status_card.dart create mode 100644 lib/components/drawer/drawer.dart create mode 100644 lib/components/lists/container_grid_list.dart create mode 100644 lib/components/text_components/double_text.dart create mode 100644 lib/main.dart create mode 100644 lib/models/docker/detailed_container.dart create mode 100644 lib/models/docker/docker_container.dart create mode 100644 lib/models/hive/token.g.dart create mode 100644 lib/models/hive/user.g.dart create mode 100644 lib/models/portainer/endpoint.dart create mode 100644 lib/models/portainer/token.dart create mode 100644 lib/models/portainer/user.dart create mode 100644 lib/pages/auth/authpage.dart create mode 100644 lib/pages/container/container_details.dart create mode 100644 lib/pages/home/home.dart create mode 100644 lib/pages/loading/loading.dart create mode 100644 lib/pages/settings/settings.dart create mode 100644 lib/pages/users/user_managment.dart create mode 100644 lib/pages/wrapper.dart create mode 100644 lib/services/local_auth.dart create mode 100644 lib/services/remote.dart create mode 100644 lib/services/storage.dart create mode 100644 lib/utils/settings.dart create mode 100644 lib/utils/style.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79cf399 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +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 +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# 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 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..7fd5968 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# 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. + +version: + revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + - platform: linux + create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + + # 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' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07c272a --- /dev/null +++ b/LICENSE @@ -0,0 +1,630 @@ +Copyright (C) 2022 Anže Mavrič + +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. + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# 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-lang.github.io/linter/lints/index.html. + # + # 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 diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..568cf1a --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,82 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException('Flutter SDK not found. Define location with flutter.sdk in the local.properties file.') +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId 'si.zbe.portarius' + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion 21 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { + release { + signingConfig signingConfigs.release + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..0d467f4 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4c207f3 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/portarius/MainActivity.kt b/android/app/src/main/kotlin/com/example/portarius/MainActivity.kt new file mode 100644 index 0000000..8fd5068 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/portarius/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.portarius + +import io.flutter.embedding.android.FlutterFragmentActivity + +class MainActivity: FlutterFragmentActivity() { +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a9bf96f1ddd62fb4513c6303215d84fb3817a1c8 GIT binary patch literal 1895 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..f0134e4446255a09b027cb1e745e3e57a1689dd8 GIT binary patch literal 1311 zcmV+)1>pLLP)S2>^k--aT=D<(P9cl=1j~i z95A{6|Nk@m{qyVppI_e?fgWLCtJ4{{8*)hhJPpF1T#>F_=Mn=B36zy1HjCu?`ehGBfbbUb%&656J1b*+?jx z+rwS{|Nq}Z%ZoG5@9dwAru6H_S9f3Dzp!P`)x-1W`{i%g%F4mR50c>G6O$2FG%>h& zV#P*a$;F1O>D#AQZ$G_x@RV2&@CwOF;giSkfTne99FT&?`3UDkdSJAP*4hP;vog3~0N!z}VkDzI=RQLt_^)23>bA}xwt}9A4;P=f@V8Gd-xAWtD=a6WWE2nru=-WO$G9I5EVAG1(HF@qFU{f8Q2ku|owiR6+T|98}`10iz{`JkkLJ4f2 zIgKj{9&t()IGzI5^T&u^cB zT~z^~dQ$~mcW-8-kOzS)2WC(G`0CC>l02a2lu=BqxrMh zVv!v#4|K4ghGkS7gJl$6bKX9^dj9Z=(kg6nbV)*>PCu}T4QxukWZ>c#2X{M(1#h2R zJ-5Ec-Rt+yFW<4r)5!yj3`{I+K*vjf%5erz>R{#I#oJp0=8^B0_RW}lY)w`D@1I|P z;8KU;fnPs9e}47w{CO(69TPxCGQgOL1=#ImVg+L6|DX;X(4k*}<^28Uca9yseQM1* zU|Z}BA>&Xy@Z#>tGt26XjA-Y2>VuIrGB+5(s0RibjCx?8!KeoY8jN~ipuv#$000^K VqcG;GH|YQX002ovPDHLkV1fuNkB|TW literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..31321b07240580a0aabe8ceefc52630dd36e0ffa GIT binary patch literal 2642 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 t{&U2)%yXb=^S3JkP=;uOYCU|^{{Y)T;_v_f literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..de16c60f1f992def378a9a7668efec953a9f4a2b GIT binary patch literal 4011 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..948bbd0df839f0abed4f5f43e78616c0ed637520 GIT binary patch literal 5493 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 O3eeLw)~eTViT^)|OT%CQ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..0d467f4 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..83ae220 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cc5527d --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/icons/icon.png b/assets/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b66c642c3aa5089dcc4bc4608009efd946898139 GIT binary patch literal 182920 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%0DfB7vXR18a(9I*WZ8m&#YPf&VtG5USn5PrSy1T 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 JYj^(i{{YwaF|z;w literal 0 HcmV?d00001 diff --git a/lib/components/appbar/appbar.dart b/lib/components/appbar/appbar.dart new file mode 100644 index 0000000..9e3dac6 --- /dev/null +++ b/lib/components/appbar/appbar.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/components/text_components/double_text.dart'; +import 'package:portarius/models/portainer/endpoint.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:portarius/utils/style.dart'; +import 'package:provider/provider.dart'; + +class PortariusAppBar extends StatefulWidget { + const PortariusAppBar({Key? key, this.endpoint, this.title = 'portarius'}) + : super(key: key); + final Endpoint? endpoint; + final String title; + + @override + State createState() => PortariusAppBarState(); +} + +class PortariusAppBarState extends State { + @override + Widget build(BuildContext context) { + StorageManager storage = + Provider.of(context, listen: false); + StyleManager style = Provider.of(context, listen: false); + Size size = MediaQuery.of(context).size; + + /// Sort snapshots by timestamp. + List? endpoints = widget.endpoint?.snapshots; + + if (endpoints != null) { + endpoints.sort((a, b) => a.time!.compareTo(b.time!)); + } + + EndpointSnapshot? latestSnapshot = endpoints?.first; + + return SliverAppBar( + pinned: true, + primary: true, + expandedHeight: widget.endpoint == null ? null : size.height * .225, + title: widget.endpoint == null + ? Text( + widget.title, + style: Theme.of(context) + .textTheme + .headline5 + ?.copyWith(fontWeight: FontWeight.bold), + ) + : null, + centerTitle: true, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + actions: const [], + flexibleSpace: widget.endpoint == null + ? null + : FlexibleSpaceBar( + centerTitle: true, + expandedTitleScale: 1.5, + titlePadding: const EdgeInsets.only(bottom: 15), + background: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + flex: 3, + child: Container(), + ), + Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (latestSnapshot != null) ...[ + DoubleText( + value: latestSnapshot.runningContainerCount + .toString(), + label: 'Running'), + DoubleText( + value: latestSnapshot.stoppedContainerCount + .toString(), + label: 'Stopped'), + DoubleText( + value: latestSnapshot.imageCount.toString(), + label: 'Images'), + DoubleText( + value: latestSnapshot.volumeCount.toString(), + label: 'Volumes'), + ], + ], + ), + Flexible( + flex: 2, + child: Container(), + ), + ], + ), + ) + ], + ), + title: Text( + 'portarius', + style: Theme.of(context) + .textTheme + .headline5 + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ); + } +} diff --git a/lib/components/buttons/big_blue_button.dart b/lib/components/buttons/big_blue_button.dart new file mode 100644 index 0000000..3e456d8 --- /dev/null +++ b/lib/components/buttons/big_blue_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/models/portainer/user.dart'; + +class BigBlueButton extends StatefulWidget { + const BigBlueButton( + {Key? key, + required this.formKey, + required this.onClick, + required this.buttonTitle}) + : super(key: key); + + /// Form key + /// This key is used to validate the form + final GlobalKey formKey; + + /// On click function to be called when button is pressed + /// This function is called with the [User] object as parameter + /// If the form is invalid, the function is not called + final void Function() onClick; + + /// Text to be displayed on the button + final String buttonTitle; + + @override + State createState() => _BigBlueButtonState(); +} + +class _BigBlueButtonState extends State { + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () { + if (widget.formKey.currentState!.validate()) { + widget.onClick(); + } + }, + child: Text( + widget.buttonTitle, + style: Theme.of(context).textTheme.button, + ), + ); + } +} diff --git a/lib/components/cards/about_tile.dart b/lib/components/cards/about_tile.dart new file mode 100644 index 0000000..54cf621 --- /dev/null +++ b/lib/components/cards/about_tile.dart @@ -0,0 +1,95 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:portarius/components/cards/setting_tile.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +/// A tile that displays a aboutDialog that contains information about the app, +/// the app's author, and the app's source code. +class PortariusAboutTile extends StatelessWidget { + const PortariusAboutTile({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + StorageManager storage = + Provider.of(context, listen: false); + return PortariusSettingTile( + onTap: () { + showAboutDialog( + context: context, + applicationName: 'Portarius', + applicationVersion: storage.packageInfo.version, + routeSettings: const RouteSettings( + name: '/about', + ), + applicationIcon: Image.asset( + 'assets/icons/icon.png', + width: 100, + ), + applicationLegalese: '© ${DateTime.now().year} Zbejas', + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan(children: [ + TextSpan( + text: 'Portarius', + style: Theme.of(context) + .textTheme + .bodyText1! + .copyWith(color: Theme.of(context).primaryColor), + recognizer: TapGestureRecognizer() + ..onTap = () async { + const uri = 'https://github.com/zbejas/portarius'; + if (!await launchUrlString( + uri, + mode: LaunchMode.externalApplication, + )) { + throw 'Could not launch $uri'; + } + }, + ), + TextSpan( + text: ' is a free, open-source, ' + 'cross-platform mobile ' + 'application that allows you to ' + 'manage your Portainer sessions.', + style: Theme.of(context).textTheme.bodyText1, + ), + TextSpan( + text: '\n\nPortarius is developed and maintained by ', + style: Theme.of(context).textTheme.bodyMedium, + ), + TextSpan( + text: 'Zbejas.', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).primaryColor), + recognizer: TapGestureRecognizer() + ..onTap = () async { + const uri = 'https://github.com/zbejas/'; + if (!await launchUrlString( + uri, + mode: LaunchMode.externalApplication, + )) { + throw 'Could not launch $uri'; + } + }, + ), + TextSpan( + text: + '\n\nThis app is in no way or form related to the official Portainer project.', + style: Theme.of(context).textTheme.bodySmall, + ), + ]), + ), + ], + ); + }, + title: 'About Portarius', + subtitle: 'App info and legal jibberish', + trailing: const Icon(Icons.info), + ); + } +} diff --git a/lib/components/cards/docker_card.dart b/lib/components/cards/docker_card.dart new file mode 100644 index 0000000..48a5eb4 --- /dev/null +++ b/lib/components/cards/docker_card.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/models/portainer/endpoint.dart'; +import 'package:portarius/models/portainer/user.dart'; +import 'package:portarius/services/remote.dart'; +import 'package:provider/provider.dart'; + +import '../../models/docker/docker_container.dart'; + +class DockerContainerCard extends StatefulWidget { + const DockerContainerCard({ + Key? key, + required this.endpoint, + required this.onUpdate, + required this.container, + required this.onTap, + required this.onLongPress, + }) : super(key: key); + + /// The endpoint to which the container belongs. + /// This is used to know which API endpoint to use. + final Endpoint endpoint; + + /// Callback for when the user updates the container. + final Future Function()? onUpdate; + final void Function()? onTap; + final void Function()? onLongPress; + + final DockerContainer container; + + @override + State createState() => _DockerContainerCardState(); +} + +class _DockerContainerCardState extends State { + final List _isLoading = [false, false]; + + @override + Widget build(BuildContext context) { + User user = Provider.of(context, listen: true); + DockerContainer container = widget.container; + Endpoint endpoint = widget.endpoint; + Size size = MediaQuery.of(context).size; + + String name = container.names!.first.replaceRange(0, 1, ''); + + return Card( + elevation: 0, + child: GridTile( + header: Padding( + padding: const EdgeInsets.all(15), + child: Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 1, + child: Card( + child: Container( + width: 18, + height: 18, + color: container.state == 'running' + ? Theme.of(context).primaryColor + : Theme.of(context).secondaryHeaderColor, + ), + ), + ), + Flexible( + flex: 3, + fit: FlexFit.loose, + child: Text( + name, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + ), + Flexible( + flex: 1, + child: SizedBox( + width: size.height * .02, + ), + ), + ], + )), + footer: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (!_isLoading[0]) ...[ + IconButton( + tooltip: container.state == 'running' ? 'Stop' : 'Start', + icon: container.state == 'running' + ? const Icon(Icons.stop_circle_outlined) + : const Icon(Icons.play_circle_outlined), + onPressed: () async { + /// Pop up confirmation dialog. + /// If the user confirms, restart the container. + /// If the user cancels, do nothing. + if (container.state == 'running') { + if (await showDialog( + context: context, + builder: (BuildContext context) { + return _stopConfirmationDialog(container); + })) { + setState(() { + _isLoading[0] = true; + }); + bool result = await RemoteService() + .stopDockerContainer(user, endpoint, container.id); + + if (result) { + widget.onUpdate!(); + } + + setState(() { + _isLoading[0] = false; + }); + } + } else { + setState(() { + _isLoading[0] = true; + }); + bool result = await RemoteService() + .startDockerContainer(user, endpoint, container.id); + + if (result) { + widget.onUpdate!(); + } + + setState(() { + _isLoading[0] = false; + }); + } + }, + ), + ] else ...[ + Padding( + padding: EdgeInsets.all(size.height * 0.011), + child: SizedBox( + height: size.height * 0.035, + width: size.height * 0.035, + child: const CircularProgressIndicator(strokeWidth: 1.5), + ), + ), + ], + if (!_isLoading[1]) ...[ + IconButton( + tooltip: 'Restart', + icon: const Icon(Icons.restart_alt), + onPressed: () async { + /// Pop up confirmation dialog. + /// If the user confirms, restart the container. + /// If the user cancels, do nothing. + if (await showDialog( + context: context, + builder: (BuildContext context) { + return _restartConfirmationDialog(container); + })) { + setState(() { + _isLoading[1] = true; + }); + bool result = await RemoteService() + .restartDockerContainer(user, endpoint, container.id); + + if (result) { + widget.onUpdate!(); + } + + setState(() { + _isLoading[1] = false; + }); + } + }, + ), + ] else ...[ + Padding( + padding: EdgeInsets.all(size.height * 0.011), + child: SizedBox( + height: size.height * 0.035, + width: size.height * 0.035, + child: const CircularProgressIndicator(strokeWidth: 1.5), + ), + ), + ], + + /*IconButton( + icon: const Icon(Icons.keyboard_arrow_right), + onPressed: () async { + setState(() { + _isLoading[2] = true; + }); + }, + ),*/ + ], + ), + ), + child: InkWell( + onTap: () { + widget.onTap!(); + }, + onLongPress: () { + widget.onLongPress!(); + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 45), + child: Text( + '${container.status}', + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.caption, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _restartConfirmationDialog(DockerContainer container) => AlertDialog( + title: Text('Restart ${container.names?.first.replaceRange(0, 1, '')}'), + content: Text( + 'Are you sure you want to restart ${container.names?.first.replaceRange(0, 1, '')}?'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: const Text('Restart'), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ); + + Widget _stopConfirmationDialog(DockerContainer container) => AlertDialog( + title: Text('Stop ${container.names?.first.replaceRange(0, 1, '')}'), + content: Text( + 'Are you sure you want to stop ${container.names?.first.replaceRange(0, 1, '')}?'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: const Text('Stop'), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ); +} diff --git a/lib/components/cards/setting_tile.dart b/lib/components/cards/setting_tile.dart new file mode 100644 index 0000000..f8f44cd --- /dev/null +++ b/lib/components/cards/setting_tile.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:provider/provider.dart'; + +class PortariusSettingTile extends StatefulWidget { + const PortariusSettingTile( + {Key? key, + required this.onTap, + required this.title, + required this.subtitle, + required this.trailing, + this.enabled = true}) + : super(key: key); + final Function onTap; + final String title; + final String subtitle; + final Widget trailing; + final bool enabled; + + @override + State createState() => _PortariusSettingTileState(); +} + +class _PortariusSettingTileState extends State { + @override + Widget build(BuildContext context) { + StorageManager storage = + Provider.of(context, listen: false); + return Card( + margin: const EdgeInsets.only(left: 15, right: 15, top: 15), + child: InkWell( + onTap: !widget.enabled + ? null + : () { + widget.onTap(); + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: ListTile( + enabled: widget.enabled, + title: Text(widget.title), + subtitle: Text(widget.subtitle), + trailing: widget.trailing, + ), + ), + ), + ); + } +} diff --git a/lib/components/cards/status_card.dart b/lib/components/cards/status_card.dart new file mode 100644 index 0000000..72b2062 --- /dev/null +++ b/lib/components/cards/status_card.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/models/docker/detailed_container.dart'; + +class DockerStatusCard extends StatelessWidget { + DockerStatusCard({Key? key, required this.detailedContainer}) + : super(key: key); + DetailedDockerContainer detailedContainer; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: 'ID: ${detailedContainer.id}', + padding: const EdgeInsets.all(5.0), + child: Card( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status: ${detailedContainer.state!.status}', + ), + const Divider(), + Text( + 'Created: ${_parseDateTime(detailedContainer.created!.toLocal())}', + ), + const Divider(), + Text( + 'Start time: ${_parseDateTime(detailedContainer.state!.startedAt!.toLocal())}', + ), + ], + ), + ), + ), + ); + } + + String _parseDateTime(DateTime time) { + return '${time.day}.${time.month}.${time.year} ${time.hour}:${time.minute}:${time.second}'; + } +} diff --git a/lib/components/drawer/drawer.dart b/lib/components/drawer/drawer.dart new file mode 100644 index 0000000..287595e --- /dev/null +++ b/lib/components/drawer/drawer.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/models/portainer/endpoint.dart'; +import 'package:portarius/services/remote.dart'; +import 'package:portarius/utils/settings.dart'; +import 'package:portarius/utils/style.dart'; +import 'package:provider/provider.dart'; + +import '../../../models/portainer/user.dart'; +import '../../../services/storage.dart'; + +class PortariusDrawer extends StatefulWidget { + const PortariusDrawer({Key? key, required this.pageRoute}) : super(key: key); + final String pageRoute; + + @override + State createState() => _PortariusDrawerState(); +} + +class _PortariusDrawerState extends State { + @override + Widget build(BuildContext context) { + User user = Provider.of(context, listen: false); + StorageManager storage = + Provider.of(context, listen: false); + StyleManager style = Provider.of(context, listen: false); + SettingsManager settings = + Provider.of(context, listen: true); + return ClipRRect( + borderRadius: const BorderRadius.horizontal(right: Radius.circular(20)), + child: Drawer( + elevation: 0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.only(right: 20, top: 5), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Flexible( + flex: 1, + child: Padding( + padding: EdgeInsets.only(left: 15), + child: Text( + 'portarius', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.visible, + softWrap: false, + ), + ), + ), + Flexible( + flex: 1, + child: IconButton( + tooltip: 'Change Theme', + icon: Theme.of(context).brightness == Brightness.dark + ? const Icon(Icons.light_mode) + : const Icon(Icons.dark_mode), + onPressed: () { + style.setTheme( + Theme.of(context).brightness == Brightness.dark + ? ThemeMode.light + : ThemeMode.dark); + }, + ), + ), + ], + ), + const Flexible( + flex: 1, + child: Divider( + thickness: 0.75, + indent: 20, + ), + ), + Flexible( + flex: 15, + fit: FlexFit.loose, + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: ListView( + padding: EdgeInsets.zero, + children: [ + ListTile( + enabled: true, + selected: widget.pageRoute == '/home', + title: const Text('Home'), + subtitle: const Text('Home Page'), + trailing: const Icon( + Icons.home, + ), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushReplacementNamed('/home'); + }, + ), + ListTile( + title: const Text('User management'), + subtitle: const Text('Manage your saved userdata'), + trailing: const Icon(Icons.person), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushReplacementNamed('/users'); + }, + ), + ListTile( + enabled: true, + selected: widget.pageRoute == '/settings', + title: const Text('Settings'), + subtitle: const Text('Adjust your settings'), + trailing: const Icon( + Icons.settings, + ), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context) + .pushReplacementNamed('/settings'); + }, + ), + ListTile( + enabled: true, + title: const Text('Endpoints'), + subtitle: const Text('Select your endpoint'), + trailing: FutureBuilder>( + future: RemoteService().getEndpoints(user), + builder: (context, snapshot) { + if (snapshot.hasData) { + Endpoint? pickedEndpoint = + snapshot.data!.firstWhere( + (endpoint) => + endpoint.id == settings.selectedEndpointId, + ); + + if (snapshot.data!.isEmpty) { + return const Text('No endpoints'); + } else if (snapshot.data!.length == 1) { + return Text(pickedEndpoint.name ?? 'default'); + } else if (snapshot.data != null) { + return DropdownButton( + items: [ + ...snapshot.data! + .map( + (endpoint) => DropdownMenuItem( + value: endpoint.id, + child: Text( + endpoint.name ?? 'Unknown', + ), + ), + ) + .toList(), + ], + value: pickedEndpoint.name, + onChanged: (value) { + settings.selectedEndpointId = value as int; + storage.saveEndpointId(value); + }, + ); + } else { + return const Text('No endpoints'); + } + } else if (snapshot.hasError) { + return const Text('Error'); + } else { + return const Text('Loading'); + } + }, + ), + ), + ], + ), + ), + ), + Flexible( + flex: 0, + child: Padding( + padding: const EdgeInsets.only(left: 30, right: 30), + child: ListTile( + textColor: Theme.of(context).errorColor, + iconColor: Theme.of(context).errorColor, + title: const Text('Log Out'), + trailing: const Icon( + Icons.logout, + ), + onTap: () async { + await storage.clearEndpointId(); + // ignore: use_build_context_synchronously + await user.logOutUser(context); + // ignore: use_build_context_synchronously + Navigator.of(context).pushReplacementNamed('/'); + }, + ), + ), + ), + Flexible( + flex: 0, + child: Padding( + padding: const EdgeInsets.only(left: 15, top: 5), + child: Text('Portarius v${storage.packageInfo.version}'), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/components/lists/container_grid_list.dart b/lib/components/lists/container_grid_list.dart new file mode 100644 index 0000000..6553e25 --- /dev/null +++ b/lib/components/lists/container_grid_list.dart @@ -0,0 +1,185 @@ +// ignore_for_file: unused_import + +import 'package:flutter/material.dart'; +import 'package:portarius/components/cards/docker_card.dart'; +import 'package:portarius/models/portainer/endpoint.dart'; +import 'package:portarius/models/portainer/user.dart'; +import 'package:portarius/utils/settings.dart'; +import 'package:provider/provider.dart'; + +import '../../models/docker/docker_container.dart'; +import '../../models/portainer/token.dart'; +import '../../services/remote.dart'; + +class ContainerGrid extends StatefulWidget { + const ContainerGrid({ + Key? key, + required this.endpoint, + }) : super(key: key); + + final Endpoint endpoint; + + @override + State createState() => _ContainerGridState(); +} + +class _ContainerGridState extends State { + List _containers = []; + bool _refreshing = false; + bool _shouldRefresh = true; + + @override + Widget build(BuildContext context) { + User user = Provider.of(context, listen: true); + Size size = MediaQuery.of(context).size; + SettingsManager settingsManager = + Provider.of(context, listen: false); + Map> mappedContainers = {}; + List nonMappedContainers = []; + + if (settingsManager.autoRefresh && !_refreshing && _shouldRefresh) { + _refreshing = true; + Future.delayed(Duration(seconds: settingsManager.autoRefreshInterval), + () { + if (mounted) { + _refreshing = false; + _getContainers(user); + } + }); + } + + if (_containers.isEmpty) { + _getContainers(user); + + return const SliverFillRemaining( + child: Center( + child: CircularProgressIndicator(), + ), + ); + } else { + /// Add all 'com.docker.compose.project' containers and sort them by name. + for (DockerContainer container in _containers) { + if (container.labels != null && + container.labels!.labels + .containsKey('com.docker.compose.project')) { + if (mappedContainers[ + container.labels!.labels['com.docker.compose.project']] == + null) { + mappedContainers[ + container.labels!.labels['com.docker.compose.project']] = []; + } + mappedContainers[ + container.labels!.labels['com.docker.compose.project']] + ?.add(container); + } else { + nonMappedContainers.add(container); + } + } + + /// Add all non-mapped containers to the 'default' key. + if (nonMappedContainers.isNotEmpty) { + mappedContainers['default'] = nonMappedContainers; + } + + /// Sort the containers by name. + /// This is done to make the grid look better. + for (String key in mappedContainers.keys) { + mappedContainers[key] = mappedContainers[key]! + .map((DockerContainer container) => container) + .toList() + ..sort((DockerContainer a, DockerContainer b) => + a.names!.first.compareTo(b.names!.first)); + } + } + + return SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + if (mappedContainers.isEmpty) { + return const Center( + child: Text('No containers found.'), + ); + } + String project = mappedContainers.keys.toList()[index]; + List? containers = mappedContainers[project]; + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + left: 15, + top: index == 0 ? 0 : 15, + ), + child: Text( + project, + style: Theme.of(context).textTheme.headline4, + ), + ), + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.only(top: 20, left: 10, right: 10), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: size.width ~/ 150, + childAspectRatio: 1, + ), + itemCount: (containers as List).length, + itemBuilder: (BuildContext context, int index) { + return DockerContainerCard( + container: containers[index], + endpoint: widget.endpoint, + onUpdate: () async { + List newData = await RemoteService() + .getDockerContainerList(user, widget.endpoint); + Future.delayed(const Duration(milliseconds: 100), () { + setState(() { + _containers = newData; + }); + }); + return null; + }, + onTap: () async { + _shouldRefresh = false; + var result = await Navigator.pushNamed( + context, + '/home/container', + arguments: { + 'container': containers[index], + 'endpoint': widget.endpoint, + }, + ); + setState(() { + _shouldRefresh = true; + }); + }, + onLongPress: () { + // TODO something + }, + ); + }, + ), + ], + ); + }, + childCount: mappedContainers.keys.length, + ), + ); + } + + void _getContainers(User user) { + if (!_shouldRefresh) { + return; + } + + RemoteService().getDockerContainerList(user, widget.endpoint).then((value) { + if (value.isNotEmpty && mounted) { + setState(() { + _containers = value; + }); + } + }); + + //print('refresh: ${DateTime.now()}'); + } +} diff --git a/lib/components/text_components/double_text.dart b/lib/components/text_components/double_text.dart new file mode 100644 index 0000000..1f03e64 --- /dev/null +++ b/lib/components/text_components/double_text.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class DoubleText extends StatelessWidget { + const DoubleText({Key? key, required this.value, required this.label}) + : super(key: key); + final String value; + final String label; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(value, + style: Theme.of(context) + .textTheme + .headline5 + ?.copyWith(color: Theme.of(context).primaryColor)), + Text(label, style: Theme.of(context).textTheme.bodyText2), + ], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..21973b1 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,200 @@ +import 'dart:convert'; +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:portarius/models/portainer/token.dart'; +import 'package:portarius/pages/auth/authpage.dart'; +import 'package:portarius/pages/container/container_details.dart'; +import 'package:portarius/pages/home/home.dart'; +import 'package:portarius/pages/users/user_managment.dart'; +import 'package:portarius/pages/wrapper.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:portarius/utils/settings.dart'; +import 'package:portarius/utils/style.dart'; +import 'package:provider/provider.dart'; +import 'models/portainer/user.dart'; +import 'pages/settings/settings.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + Hive.init((await path_provider.getApplicationDocumentsDirectory()).path); + + Hive.registerAdapter(TokenAdapter()); + Hive.registerAdapter(UserAdapter()); + + const FlutterSecureStorage secureStorage = FlutterSecureStorage(); + var containsEncryptionKey = + await secureStorage.containsKey(key: 'encryptionKey'); + if (!containsEncryptionKey) { + var key = Hive.generateSecureKey(); + await secureStorage.write( + key: 'encryptionKey', value: base64UrlEncode(key)); + } + + String key = await secureStorage.read(key: 'encryptionKey') ?? ''; + + runApp(MultiProvider(providers: [ + ChangeNotifierProvider( + create: (_) => User(username: '', password: '', hostUrl: '')), + ChangeNotifierProvider( + create: (_) => StorageManager(key), + ), + ChangeNotifierProvider( + create: (_) => StyleManager(), + ), + ChangeNotifierProvider( + create: (_) => SettingsManager(), + ), + ], child: const MyApp())); +} + +class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + StorageManager storage = + Provider.of(context, listen: false); + StyleManager style = Provider.of(context, listen: true); + SettingsManager settings = + Provider.of(context, listen: false); + storage.init(context).then((_) { + settings.init(storage); + }); + + const Map _routes = { + '/users': UserManagerPage(), + '/settings': SettingsPage(), + '/home/container': ContainerDetailsPage(), + '/home': HomePage(), + '/auth': AuthPage(), + '/': Wrapper(), + }; + + return FutureBuilder( + future: style.getTheme(), + builder: (context, snapshot) { + return MaterialApp( + title: 'Portarius', + initialRoute: '/', + //routes: _routes, + onGenerateRoute: (settings) { + String pageName = settings.name ?? '/'; + Widget routeWidget = _routes[pageName] ?? const Wrapper(); + + if (pageName == '/home/container') { + return PageRouteBuilder( + settings: settings, + transitionDuration: const Duration(milliseconds: 250), + reverseTransitionDuration: const Duration(milliseconds: 150), + pageBuilder: (context, animation, secondaryAnimation) => + routeWidget, + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(animation), + child: child, + ); + }, + ); + } + + return PageRouteBuilder( + settings: + settings, // Pass this to make popUntil(), pushNamedAndRemoveUntil(), works + transitionDuration: const Duration(milliseconds: 150), + reverseTransitionDuration: const Duration(milliseconds: 150), + pageBuilder: (_, __, ___) => routeWidget, + transitionsBuilder: (_, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child), + ); + }, + themeMode: snapshot.data, + + /// Generated from https://rydmike.com/flexcolorscheme/ playground. + /// This is the color scheme used in the app. + theme: FlexThemeData.light( + colors: const FlexSchemeColor( + primary: Color(0xff70a1ff), + primaryContainer: Color(0xffd0e4ff), + secondary: Color(0xffac3306), + secondaryContainer: Color(0xffffdbcf), + tertiary: Color(0xff006875), + tertiaryContainer: Color(0xff95f0ff), + appBarColor: Color(0xffffdbcf), + error: Color(0xffb00020), + ), + usedColors: 1, + surfaceMode: FlexSurfaceMode.highScaffoldLowSurface, + blendLevel: 20, + appBarOpacity: 0.95, + tooltipsMatchBackground: true, + subThemesData: const FlexSubThemesData( + blendOnLevel: 20, + blendOnColors: false, + defaultRadius: 15.0, + textButtonRadius: 15.0, + elevatedButtonRadius: 40.0, + outlinedButtonRadius: 40.0, + inputDecoratorRadius: 40.0, + fabRadius: 20.0, + cardRadius: 15.0, + popupMenuRadius: 11.0, + dialogRadius: 20.0, + timePickerDialogRadius: 20.0, + ), + useMaterial3ErrorColors: true, + visualDensity: FlexColorScheme.comfortablePlatformDensity, + useMaterial3: true, + // To use the playground font, add GoogleFonts package and uncomment + // fontFamily: GoogleFonts.notoSans().fontFamily, + ), + darkTheme: FlexThemeData.dark( + colors: const FlexSchemeColor( + primary: Color(0xff9fc9ff), + primaryContainer: Color(0xff00325b), + secondary: Color(0xffffb59d), + secondaryContainer: Color(0xff872100), + tertiary: Color(0xff86d2e1), + tertiaryContainer: Color(0xff004e59), + appBarColor: Color(0xff872100), + error: Color(0xffcf6679), + ), + usedColors: 1, + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 20, + appBarStyle: FlexAppBarStyle.background, + appBarOpacity: 0.90, + appBarElevation: 2.0, + surfaceTint: const Color(0xff70a1ff), + tooltipsMatchBackground: true, + subThemesData: const FlexSubThemesData( + blendOnLevel: 30, + defaultRadius: 15.0, + textButtonRadius: 15.0, + elevatedButtonRadius: 40.0, + outlinedButtonRadius: 40.0, + inputDecoratorRadius: 40.0, + fabRadius: 20.0, + cardRadius: 15.0, + popupMenuRadius: 11.0, + dialogRadius: 20.0, + timePickerDialogRadius: 20.0, + ), + useMaterial3ErrorColors: true, + visualDensity: FlexColorScheme.comfortablePlatformDensity, + useMaterial3: true, + // To use the playground font, add GoogleFonts package and uncomment + // fontFamily: GoogleFonts.notoSans().fontFamily, + ), + ); + }); + } +} diff --git a/lib/models/docker/detailed_container.dart b/lib/models/docker/detailed_container.dart new file mode 100644 index 0000000..29a8f87 --- /dev/null +++ b/lib/models/docker/detailed_container.dart @@ -0,0 +1,839 @@ +// To parse this JSON data, do +// +// final detailedDockerContainer = detailedDockerContainerFromJson(jsonString); + +import 'dart:convert'; + +DetailedDockerContainer detailedDockerContainerFromJson(String str) => + DetailedDockerContainer.fromJson(json.decode(str)); + +String detailedDockerContainerToJson(DetailedDockerContainer data) => + json.encode(data.toJson()); + +class DetailedDockerContainer { + DetailedDockerContainer({ + required this.appArmorProfile, + required this.args, + required this.config, + required this.created, + required this.driver, + required this.execIDs, + required this.hostConfig, + required this.hostnamePath, + required this.hostsPath, + required this.logPath, + required this.id, + required this.image, + required this.mountLabel, + required this.name, + required this.networkSettings, + required this.path, + required this.processLabel, + required this.resolvConfPath, + required this.restartCount, + required this.state, + required this.mounts, + }); + + String? appArmorProfile; + List? args; + Config? config; + DateTime? created; + String? driver; + List? execIDs; + HostConfig? hostConfig; + String? hostnamePath; + String? hostsPath; + String? logPath; + String? id; + String? image; + String? mountLabel; + String? name; + NetworkSettings? networkSettings; + String? path; + String? processLabel; + String? resolvConfPath; + int? restartCount; + DockerState? state; + List? mounts; + + factory DetailedDockerContainer.fromJson(Map json) => + DetailedDockerContainer( + appArmorProfile: json["AppArmorProfile"], + args: List.from(json["Args"].map((x) => x)), + config: Config.fromJson(json["Config"]), + created: DateTime.parse(json["Created"]), + driver: json["Driver"], + execIDs: List.from(json["ExecIDs"]?.map((x) => x) ?? []), + hostConfig: HostConfig.fromJson(json["HostConfig"]), + hostnamePath: json["HostnamePath"], + hostsPath: json["HostsPath"], + logPath: json["LogPath"], + id: json["Id"], + image: json["Image"], + mountLabel: json["MountLabel"], + name: json["Name"].toString().replaceRange(0, 1, ''), + networkSettings: NetworkSettings.fromJson(json["NetworkSettings"]), + path: json["Path"], + processLabel: json["ProcessLabel"], + resolvConfPath: json["ResolvConfPath"], + restartCount: json["RestartCount"], + state: + json["State"] != null ? DockerState.fromJson(json["State"]) : null, + mounts: List.from(json["Mounts"].map((x) => Mount.fromJson(x))), + ); + + Map toJson() => { + "AppArmorProfile": appArmorProfile, + "Args": List.from(args?.map((x) => x) ?? []), + "Config": config?.toJson(), + "Created": created?.toIso8601String(), + "Driver": driver, + "ExecIDs": List.from(execIDs?.map((x) => x) ?? []), + "HostConfig": hostConfig?.toJson(), + "HostnamePath": hostnamePath, + "HostsPath": hostsPath, + "LogPath": logPath, + "Id": id, + "Image": image, + "MountLabel": mountLabel, + "Name": name, + "NetworkSettings": networkSettings?.toJson(), + "Path": path, + "ProcessLabel": processLabel, + "ResolvConfPath": resolvConfPath, + "RestartCount": restartCount, + "DockerState": state?.toJson(), + "Mounts": List.from(mounts?.map((x) => x.toJson()) ?? []), + }; +} + +class Config { + Config({ + required this.attachStderr, + required this.attachStdin, + required this.attachStdout, + required this.cmd, + required this.domainname, + required this.env, + required this.healthcheck, + required this.hostname, + required this.image, + required this.labels, + required this.macAddress, + required this.networkDisabled, + required this.openStdin, + required this.stdinOnce, + required this.tty, + required this.user, + required this.volumes, + required this.workingDir, + required this.stopSignal, + required this.stopTimeout, + }); + + bool? attachStderr; + bool? attachStdin; + bool? attachStdout; + List? cmd; + String? domainname; + List? env; + Healthcheck? healthcheck; + String? hostname; + String? image; + Labels? labels; + String? macAddress; + bool? networkDisabled; + bool? openStdin; + bool? stdinOnce; + bool? tty; + String? user; + Volumes? volumes; + String? workingDir; + String? stopSignal; + int? stopTimeout; + + factory Config.fromJson(Map json) => Config( + attachStderr: json["AttachStderr"], + attachStdin: json["AttachStdin"], + attachStdout: json["AttachStdout"], + cmd: List.from(json["Cmd"]?.map((x) => x) ?? []), + domainname: json["Domainname"], + env: List.from(json["Env"].map((x) => x)), + healthcheck: json["Healthcheck"] != null + ? Healthcheck.fromJson(json["Healthcheck"]) + : null, + hostname: json["Hostname"], + image: json["Image"], + labels: Labels.fromJson(json["Labels"]), + macAddress: json["MacAddress"], + networkDisabled: json["NetworkDisabled"], + openStdin: json["OpenStdin"], + stdinOnce: json["StdinOnce"], + tty: json["Tty"], + user: json["User"], + volumes: Volumes.fromJson(json["Volumes"]), + workingDir: json["WorkingDir"], + stopSignal: json["StopSignal"], + stopTimeout: json["StopTimeout"], + ); + + Map toJson() => { + "AttachStderr": attachStderr, + "AttachStdin": attachStdin, + "AttachStdout": attachStdout, + "Cmd": List.from(cmd?.map((x) => x) ?? []), + "Domainname": domainname, + "Env": List.from(env?.map((x) => x) ?? []), + "Healthcheck": healthcheck?.toJson(), + "Hostname": hostname, + "Image": image, + "Labels": labels?.toJson(), + "MacAddress": macAddress, + "NetworkDisabled": networkDisabled, + "OpenStdin": openStdin, + "StdinOnce": stdinOnce, + "Tty": tty, + "User": user, + "Volumes": volumes?.toJson(), + "WorkingDir": workingDir, + "StopSignal": stopSignal, + "StopTimeout": stopTimeout, + }; +} + +class Healthcheck { + Healthcheck({ + required this.test, + }); + + List test; + + factory Healthcheck.fromJson(Map json) => Healthcheck( + test: List.from(json["Test"].map((x) => x)), + ); + + Map toJson() => { + "Test": List.from(test.map((x) => x)), + }; +} + +class Labels { + /// This class maps all the labels that are used in the Docker image. + + Labels({ + required this.labels, + }); + + Map labels; + + factory Labels.fromJson(Map json) { + Map tempLabels = {}; + tempLabels.addAll(json); + + return Labels( + labels: tempLabels, + ); + } + + Map toJson() => { + 'Labels': labels, + }; +} + +class Volumes { + Volumes({ + required this.volumesData, + }); + + PortBindings volumesData; + + factory Volumes.fromJson(Map json) => Volumes( + volumesData: PortBindings.fromJson(json), + ); + + Map toJson() => { + "/volumes/data": volumesData.toJson(), + }; +} + +class PortBindings { + PortBindings(); + + factory PortBindings.fromJson(Map json) => PortBindings(); + + Map toJson() => {}; +} + +class HostConfig { + HostConfig({ + required this.maximumIOps, + required this.maximumIoBps, + required this.blkioWeight, + required this.blkioWeightDevice, + required this.blkioDeviceReadBps, + required this.blkioDeviceWriteBps, + required this.blkioDeviceReadIOps, + required this.blkioDeviceWriteIOps, + required this.containerIdFile, + required this.cpusetCpus, + required this.cpusetMems, + required this.cpuPercent, + required this.cpuShares, + required this.cpuPeriod, + required this.cpuRealtimePeriod, + required this.cpuRealtimeRuntime, + required this.devices, + required this.deviceRequests, + required this.ipcMode, + required this.memory, + required this.memorySwap, + required this.memoryReservation, + required this.kernelMemory, + required this.oomKillDisable, + required this.oomScoreAdj, + required this.networkMode, + required this.pidMode, + required this.portBindings, + required this.privileged, + required this.readonlyRootfs, + required this.publishAllPorts, + required this.restartPolicy, + required this.logConfig, + required this.sysctls, + required this.ulimits, + required this.volumeDriver, + required this.shmSize, + }); + + int? maximumIOps; + int? maximumIoBps; + int? blkioWeight; + List? blkioWeightDevice; + List? blkioDeviceReadBps; + List? blkioDeviceWriteBps; + List? blkioDeviceReadIOps; + List? blkioDeviceWriteIOps; + String? containerIdFile; + String? cpusetCpus; + String? cpusetMems; + int? cpuPercent; + int? cpuShares; + int? cpuPeriod; + int? cpuRealtimePeriod; + int? cpuRealtimeRuntime; + List? devices; + List? deviceRequests; + String? ipcMode; + int? memory; + int? memorySwap; + int? memoryReservation; + int? kernelMemory; + bool? oomKillDisable; + int? oomScoreAdj; + String? networkMode; + String? pidMode; + PortBindings? portBindings; + bool? privileged; + bool? readonlyRootfs; + bool? publishAllPorts; + RestartPolicy? restartPolicy; + LogConfig? logConfig; + Sysctls? sysctls; + List? ulimits; + String? volumeDriver; + int? shmSize; + + factory HostConfig.fromJson(Map json) => HostConfig( + maximumIOps: json["MaximumIOps"], + maximumIoBps: json["MaximumIOBps"], + blkioWeight: json["BlkioWeight"], + blkioWeightDevice: List.from( + json["BlkioWeightDevice"]?.map((x) => PortBindings.fromJson(x)) ?? + []), + blkioDeviceReadBps: List.from( + json["BlkioDeviceReadBps"]?.map((x) => PortBindings.fromJson(x)) ?? + []), + blkioDeviceWriteBps: List.from( + json["BlkioDeviceWriteBps"]?.map((x) => PortBindings.fromJson(x)) ?? + []), + blkioDeviceReadIOps: List.from( + json["BlkioDeviceReadIOps"]?.map((x) => PortBindings.fromJson(x)) ?? + []), + blkioDeviceWriteIOps: List.from( + json["BlkioDeviceWriteIOps"] + ?.map((x) => PortBindings.fromJson(x)) ?? + []), + containerIdFile: json["ContainerIDFile"], + cpusetCpus: json["CpusetCpus"], + cpusetMems: json["CpusetMems"], + cpuPercent: json["CpuPercent"], + cpuShares: json["CpuShares"], + cpuPeriod: json["CpuPeriod"], + cpuRealtimePeriod: json["CpuRealtimePeriod"], + cpuRealtimeRuntime: json["CpuRealtimeRuntime"], + devices: List.from(json["Devices"]?.map((x) => x) ?? []), + deviceRequests: List.from( + json["DeviceRequests"]?.map((x) => DeviceRequest.fromJson(x)) ?? + []), + ipcMode: json["IpcMode"], + memory: json["Memory"], + memorySwap: json["MemorySwap"], + memoryReservation: json["MemoryReservation"], + kernelMemory: json["KernelMemory"], + oomKillDisable: json["OomKillDisable"], + oomScoreAdj: json["OomScoreAdj"], + networkMode: json["NetworkMode"], + pidMode: json["PidMode"], + portBindings: PortBindings.fromJson(json["PortBindings"]), + privileged: json["Privileged"], + readonlyRootfs: json["ReadonlyRootfs"], + publishAllPorts: json["PublishAllPorts"], + restartPolicy: RestartPolicy.fromJson(json["RestartPolicy"]), + logConfig: LogConfig.fromJson(json["LogConfig"]), + sysctls: + json["Sysctls"] != null ? Sysctls.fromJson(json["Sysctls"]) : null, + ulimits: List.from( + json["Ulimits"]?.map((x) => PortBindings.fromJson(x)) ?? []), + volumeDriver: json["VolumeDriver"], + shmSize: json["ShmSize"], + ); + + Map toJson() => { + "MaximumIOps": maximumIOps, + "MaximumIOBps": maximumIoBps, + "BlkioWeight": blkioWeight, + "BlkioWeightDevice": + List.from(blkioWeightDevice?.map((x) => x.toJson()) ?? []), + "BlkioDeviceReadBps": List.from( + blkioDeviceReadBps?.map((x) => x.toJson()) ?? []), + "BlkioDeviceWriteBps": List.from( + blkioDeviceWriteBps?.map((x) => x.toJson()) ?? []), + "BlkioDeviceReadIOps": List.from( + blkioDeviceReadIOps?.map((x) => x.toJson()) ?? []), + "BlkioDeviceWriteIOps": List.from( + blkioDeviceWriteIOps?.map((x) => x.toJson()) ?? []), + "ContainerIDFile": containerIdFile, + "CpusetCpus": cpusetCpus, + "CpusetMems": cpusetMems, + "CpuPercent": cpuPercent, + "CpuShares": cpuShares, + "CpuPeriod": cpuPeriod, + "CpuRealtimePeriod": cpuRealtimePeriod, + "CpuRealtimeRuntime": cpuRealtimeRuntime, + "Devices": List.from(devices?.map((x) => x) ?? []), + "DeviceRequests": + List.from(deviceRequests?.map((x) => x.toJson()) ?? []), + "IpcMode": ipcMode, + "Memory": memory, + "MemorySwap": memorySwap, + "MemoryReservation": memoryReservation, + "KernelMemory": kernelMemory, + "OomKillDisable": oomKillDisable, + "OomScoreAdj": oomScoreAdj, + "NetworkMode": networkMode, + "PidMode": pidMode, + "PortBindings": portBindings?.toJson(), + "Privileged": privileged, + "ReadonlyRootfs": readonlyRootfs, + "PublishAllPorts": publishAllPorts, + "RestartPolicy": restartPolicy?.toJson(), + "LogConfig": logConfig?.toJson(), + "Sysctls": sysctls?.toJson(), + "Ulimits": List.from(ulimits?.map((x) => x.toJson()) ?? []), + "VolumeDriver": volumeDriver, + "ShmSize": shmSize, + }; +} + +class DeviceRequest { + DeviceRequest({ + required this.driver, + required this.count, + required this.deviceIDs, + required this.capabilities, + required this.options, + }); + + String? driver; + int? count; + List? deviceIDs; + List>? capabilities; + Options? options; + + factory DeviceRequest.fromJson(Map json) => DeviceRequest( + driver: json["Driver"], + count: json["Count"], + deviceIDs: List.from(json["DeviceIDs"].map((x) => x)), + capabilities: List>.from(json["Capabilities"] + .map((x) => List.from(x.map((x) => x)))), + options: Options.fromJson(json["Options"]), + ); + + Map toJson() => { + "Driver": driver, + "Count": count, + "DeviceIDs\"": List.from(deviceIDs?.map((x) => x) ?? []), + "Capabilities": List.from( + capabilities?.map((x) => List.from(x.map((x) => x))) ?? + []), + "Options": options?.toJson(), + }; +} + +class Options { + Options({ + required this.properties, + }); + + Map properties; + + factory Options.fromJson(Map json) => Options( + properties: json, + ); + + Map toJson() => properties; +} + +class LogConfig { + LogConfig({ + required this.type, + }); + + String type; + + factory LogConfig.fromJson(Map json) => LogConfig( + type: json["Type"], + ); + + Map toJson() => { + "Type": type, + }; +} + +class RestartPolicy { + RestartPolicy({ + required this.maximumRetryCount, + required this.name, + }); + + int maximumRetryCount; + String name; + + factory RestartPolicy.fromJson(Map json) => RestartPolicy( + maximumRetryCount: json["MaximumRetryCount"], + name: json["Name"], + ); + + Map toJson() => { + "MaximumRetryCount": maximumRetryCount, + "Name": name, + }; +} + +class Sysctls { + Sysctls({ + required this.netIpv4IpForward, + }); + + String netIpv4IpForward; + + factory Sysctls.fromJson(Map json) => Sysctls( + netIpv4IpForward: json["net.ipv4.ip_forward"], + ); + + Map toJson() => { + "net.ipv4.ip_forward": netIpv4IpForward, + }; +} + +class Mount { + Mount({ + required this.name, + required this.source, + required this.destination, + required this.driver, + required this.mode, + required this.rw, + required this.propagation, + }); + + String? name; + String? source; + String? destination; + String? driver; + String? mode; + bool? rw; + String? propagation; + + factory Mount.fromJson(Map json) => Mount( + name: json["Name"], + source: json["Source"], + destination: json["Destination"], + driver: json["Driver"], + mode: json["Mode"], + rw: json["RW"], + propagation: json["Propagation"], + ); + + Map toJson() => { + "Name": name, + "Source": source, + "Destination": destination, + "Driver": driver, + "Mode": mode, + "RW": rw, + "Propagation": propagation, + }; +} + +class NetworkSettings { + NetworkSettings({ + required this.bridge, + required this.sandboxId, + required this.hairpinMode, + required this.linkLocalIPv6Address, + required this.linkLocalIPv6PrefixLen, + required this.sandboxKey, + required this.endpointId, + required this.gateway, + required this.globalIPv6Address, + required this.globalIPv6PrefixLen, + required this.ipAddress, + required this.ipPrefixLen, + required this.iPv6Gateway, + required this.macAddress, + required this.networks, + }); + + String? bridge; + String? sandboxId; + bool? hairpinMode; + String? linkLocalIPv6Address; + int? linkLocalIPv6PrefixLen; + String? sandboxKey; + String? endpointId; + String? gateway; + String? globalIPv6Address; + int? globalIPv6PrefixLen; + String? ipAddress; + int? ipPrefixLen; + String? iPv6Gateway; + String? macAddress; + Networks? networks; + + factory NetworkSettings.fromJson(Map json) => + NetworkSettings( + bridge: json["Bridge"], + sandboxId: json["SandboxID"], + hairpinMode: json["HairpinMode"], + linkLocalIPv6Address: json["LinkLocalIPv6Address"], + linkLocalIPv6PrefixLen: json["LinkLocalIPv6PrefixLen"], + sandboxKey: json["SandboxKey"], + endpointId: json["EndpointID"], + gateway: json["Gateway"], + globalIPv6Address: json["GlobalIPv6Address"], + globalIPv6PrefixLen: json["GlobalIPv6PrefixLen"], + ipAddress: json["IPAddress"], + ipPrefixLen: json["IPPrefixLen"], + iPv6Gateway: json["IPv6Gateway"], + macAddress: json["MacAddress"], + networks: json["Networks"] != null + ? Networks.fromJson(json["Networks"]) + : null, + ); + + Map toJson() => { + "Bridge": bridge, + "SandboxID": sandboxId, + "HairpinMode": hairpinMode, + "LinkLocalIPv6Address": linkLocalIPv6Address, + "LinkLocalIPv6PrefixLen": linkLocalIPv6PrefixLen, + "SandboxKey": sandboxKey, + "EndpointID": endpointId, + "Gateway": gateway, + "GlobalIPv6Address": globalIPv6Address, + "GlobalIPv6PrefixLen": globalIPv6PrefixLen, + "IPAddress": ipAddress, + "IPPrefixLen": ipPrefixLen, + "IPv6Gateway": iPv6Gateway, + "MacAddress": macAddress, + "Networks": networks?.toJson(), + }; +} + +class Networks { + Networks({ + required this.bridge, + }); + + Bridge? bridge; + + factory Networks.fromJson(Map json) => Networks( + bridge: json["bridge"] != null ? Bridge.fromJson(json["bridge"]) : null, + ); + + Map toJson() => { + "bridge": bridge?.toJson(), + }; +} + +class Bridge { + Bridge({ + required this.networkId, + required this.endpointId, + required this.gateway, + required this.ipAddress, + required this.ipPrefixLen, + required this.iPv6Gateway, + required this.globalIPv6Address, + required this.globalIPv6PrefixLen, + required this.macAddress, + }); + + String? networkId; + String? endpointId; + String? gateway; + String? ipAddress; + int? ipPrefixLen; + String? iPv6Gateway; + String? globalIPv6Address; + int? globalIPv6PrefixLen; + String? macAddress; + + factory Bridge.fromJson(Map json) => Bridge( + networkId: json["NetworkID"], + endpointId: json["EndpointID"], + gateway: json["Gateway"], + ipAddress: json["IPAddress"], + ipPrefixLen: json["IPPrefixLen"], + iPv6Gateway: json["IPv6Gateway"], + globalIPv6Address: json["GlobalIPv6Address"], + globalIPv6PrefixLen: json["GlobalIPv6PrefixLen"], + macAddress: json["MacAddress"], + ); + + Map toJson() => { + "NetworkID": networkId, + "EndpointID": endpointId, + "Gateway": gateway, + "IPAddress": ipAddress, + "IPPrefixLen": ipPrefixLen, + "IPv6Gateway": iPv6Gateway, + "GlobalIPv6Address": globalIPv6Address, + "GlobalIPv6PrefixLen": globalIPv6PrefixLen, + "MacAddress": macAddress, + }; +} + +class DockerState { + DockerState({ + required this.error, + required this.exitCode, + required this.finishedAt, + required this.health, + required this.oomKilled, + required this.dead, + required this.paused, + required this.pid, + required this.restarting, + required this.running, + required this.startedAt, + required this.status, + }); + + String? error; + int? exitCode; + DateTime? finishedAt; + Health? health; + bool? oomKilled; + bool? dead; + bool? paused; + int? pid; + bool? restarting; + bool? running; + DateTime? startedAt; + String? status; + + factory DockerState.fromJson(Map json) => DockerState( + error: json["Error"], + exitCode: json["ExitCode"], + finishedAt: DateTime.parse(json["FinishedAt"]), + health: json["Health"] != null ? Health.fromJson(json["Health"]) : null, + oomKilled: json["OOMKilled"], + dead: json["Dead"], + paused: json["Paused"], + pid: json["Pid"], + restarting: json["Restarting"], + running: json["Running"], + startedAt: DateTime.parse(json["StartedAt"]), + status: json["Status"], + ); + + Map toJson() => { + "Error": error, + "ExitCode": exitCode, + "FinishedAt": finishedAt?.toIso8601String(), + "Health": health?.toJson(), + "OOMKilled": oomKilled, + "Dead": dead, + "Paused": paused, + "Pid": pid, + "Restarting": restarting, + "Running": running, + "StartedAt": startedAt?.toIso8601String(), + "Status": status, + }; +} + +class Health { + Health({ + required this.status, + required this.failingStreak, + required this.log, + }); + + String? status; + int? failingStreak; + List? log; + + factory Health.fromJson(Map json) => Health( + status: json["Status"], + failingStreak: json["FailingStreak"], + log: List.from(json["Log"].map((x) => Log.fromJson(x))), + ); + + Map toJson() => { + "Status": status, + "FailingStreak": failingStreak, + "Log": List.from(log?.map((x) => x.toJson()) ?? []), + }; +} + +class Log { + Log({ + required this.start, + required this.end, + required this.exitCode, + required this.output, + }); + + DateTime start; + DateTime end; + int exitCode; + String output; + + factory Log.fromJson(Map json) => Log( + start: DateTime.parse(json["Start"]), + end: DateTime.parse(json["End"]), + exitCode: json["ExitCode"], + output: json["Output"], + ); + + Map toJson() => { + "Start": start.toIso8601String(), + "End": end.toIso8601String(), + "ExitCode": exitCode, + "Output": output, + }; +} diff --git a/lib/models/docker/docker_container.dart b/lib/models/docker/docker_container.dart new file mode 100644 index 0000000..21f9128 --- /dev/null +++ b/lib/models/docker/docker_container.dart @@ -0,0 +1,290 @@ +// To parse this JSON data, do +// +// final token = tokenFromJson(jsonString); + +import 'dart:convert'; + +List dockerContainerFromJson(String str) => + List.from( + json.decode(str).map((x) => DockerContainer.fromJson(x))); + +String dockerContainerToJson(List data) => + json.encode(List.from(data.map((x) => x.toJson()))); + +/// Smaller representation of a container than inspecting a single container +class DockerContainer { + DockerContainer({ + required this.id, + required this.names, + required this.image, + required this.imageId, + required this.command, + required this.created, + required this.state, + required this.status, + required this.ports, + required this.labels, + required this.sizeRw, + required this.sizeRootFs, + required this.hostConfig, + required this.networkSettings, + required this.mounts, + }); + + String id; + List? names; + String? image; + String? imageId; + String? command; + int? created; + String? state; + String? status; + List? ports; + Labels? labels; + int? sizeRw; + int? sizeRootFs; + HostConfig? hostConfig; + NetworkSettings? networkSettings; + List? mounts; + + factory DockerContainer.fromJson(Map json) => + DockerContainer( + id: json["Id"], + names: List.from(json["Names"]?.map((x) => x) ?? []), + image: json["Image"], + imageId: json["ImageID"], + command: json["Command"], + created: json["Created"], + state: json["State"], + status: json["Status"], + ports: + List.from(json["Ports"]?.map((x) => Port.fromJson(x)) ?? []), + labels: Labels.fromJson(json["Labels"] ?? {}), + sizeRw: json["SizeRw"], + sizeRootFs: json["SizeRootFs"], + hostConfig: HostConfig.fromJson(json["HostConfig"] ?? {}), + networkSettings: NetworkSettings.fromJson(json["NetworkSettings"]), + mounts: List.from( + json["Mounts"]?.map((x) => Mount.fromJson(x)) ?? []), + ); + + Map toJson() => { + "Id": id, + "Names": List.from(names?.map((x) => x) ?? []), + "Image": image, + "ImageID": imageId, + "Command": command, + "Created": created, + "State": state, + "Status": status, + "Ports": List.from(ports?.map((x) => x.toJson()) ?? []), + "Labels": labels?.toJson(), + "SizeRw": sizeRw, + "SizeRootFs": sizeRootFs, + "HostConfig": hostConfig?.toJson(), + "NetworkSettings": networkSettings?.toJson(), + "Mounts": List.from(mounts?.map((x) => x.toJson()) ?? []), + }; + + void updateContainer(DockerContainer container) { + id = container.id; + names = container.names!.isEmpty ? names : container.names; + image = container.image ?? image; + imageId = container.imageId ?? imageId; + command = container.command ?? command; + created = container.created ?? created; + state = container.state ?? state; + status = container.status ?? status; + ports = container.ports!.isEmpty ? ports : container.ports; + labels = container.labels ?? labels; + sizeRw = container.sizeRw ?? sizeRw; + sizeRootFs = container.sizeRootFs ?? sizeRootFs; + hostConfig = container.hostConfig ?? hostConfig; + networkSettings = container.networkSettings ?? networkSettings; + mounts = container.mounts!.isEmpty ? mounts : container.mounts; + } +} + +class Bridge { + Bridge({ + required this.networkId, + required this.endpointId, + required this.gateway, + required this.ipAddress, + required this.ipPrefixLen, + required this.iPv6Gateway, + required this.globalIPv6Address, + required this.globalIPv6PrefixLen, + required this.macAddress, + }); + + String? networkId; + String? endpointId; + String? gateway; + String? ipAddress; + int? ipPrefixLen; + String? iPv6Gateway; + String? globalIPv6Address; + int? globalIPv6PrefixLen; + String? macAddress; + + factory Bridge.fromJson(Map json) => Bridge( + networkId: json["NetworkID"], + endpointId: json["EndpointID"], + gateway: json["Gateway"], + ipAddress: json["IPAddress"], + ipPrefixLen: json["IPPrefixLen"], + iPv6Gateway: json["IPv6Gateway"], + globalIPv6Address: json["GlobalIPv6Address"], + globalIPv6PrefixLen: json["GlobalIPv6PrefixLen"], + macAddress: json["MacAddress"], + ); + + Map toJson() => { + "NetworkID": networkId, + "EndpointID": endpointId, + "Gateway": gateway, + "IPAddress": ipAddress, + "IPPrefixLen": ipPrefixLen, + "IPv6Gateway": iPv6Gateway, + "GlobalIPv6Address": globalIPv6Address, + "GlobalIPv6PrefixLen": globalIPv6PrefixLen, + "MacAddress": macAddress, + }; +} + +class Port { + Port({ + required this.privatePort, + required this.publicPort, + required this.type, + }); + + int? privatePort; + int? publicPort; + String? type; + + factory Port.fromJson(Map json) => Port( + privatePort: json["PrivatePort"], + publicPort: json["PublicPort"], + type: json["Type"], + ); + + Map toJson() => { + "PrivatePort": privatePort, + "PublicPort": publicPort, + "Type": type, + }; +} + +class HostConfig { + HostConfig({ + required this.networkMode, + }); + + String networkMode; + + factory HostConfig.fromJson(Map json) => HostConfig( + networkMode: json["NetworkMode"], + ); + + Map toJson() => { + "NetworkMode": networkMode, + }; +} + +class Labels { + /// This class maps all the labels that are used in the Docker image. + + Labels({ + required this.labels, + }); + + Map labels; + + factory Labels.fromJson(Map json) { + Map tempLabels = {}; + tempLabels.addAll(json); + + return Labels( + labels: tempLabels, + ); + } + + Map toJson() => { + 'Labels': labels, + }; +} + +class Mount { + Mount({ + required this.name, + required this.source, + required this.destination, + required this.driver, + required this.mode, + required this.rw, + required this.propagation, + }); + + String? name; + String? source; + String? destination; + String? driver; + String? mode; + bool? rw; + String? propagation; + + factory Mount.fromJson(Map json) => Mount( + name: json["Name"], + source: json["Source"], + destination: json["Destination"], + driver: json["Driver"], + mode: json["Mode"], + rw: json["RW"], + propagation: json["Propagation"], + ); + + Map toJson() => { + "Name": name, + "Source": source, + "Destination": destination, + "Driver": driver, + "Mode": mode, + "RW": rw, + "Propagation": propagation, + }; +} + +class NetworkSettings { + NetworkSettings({ + required this.networks, + }); + + Networks networks; + + factory NetworkSettings.fromJson(Map json) => + NetworkSettings( + networks: Networks.fromJson(json["Networks"]), + ); + + Map toJson() => { + "Networks": networks.toJson(), + }; +} + +class Networks { + Networks({ + required this.bridge, + }); + + Bridge? bridge; + + factory Networks.fromJson(Map json) => Networks( + bridge: Bridge.fromJson(json["bridge"] ?? {}), + ); + + Map toJson() => { + "bridge": bridge?.toJson(), + }; +} diff --git a/lib/models/hive/token.g.dart b/lib/models/hive/token.g.dart new file mode 100644 index 0000000..ff6128d --- /dev/null +++ b/lib/models/hive/token.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../portainer/token.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class TokenAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + Token read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Token( + jwt: fields[0] as String, + ); + } + + @override + void write(BinaryWriter writer, Token obj) { + writer + ..writeByte(1) + ..writeByte(0) + ..write(obj.jwt); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TokenAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/hive/user.g.dart b/lib/models/hive/user.g.dart new file mode 100644 index 0000000..a2e2f2f --- /dev/null +++ b/lib/models/hive/user.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../portainer/user.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class UserAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + User read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return User( + username: fields[0] as String, + password: fields[1] as String, + hostUrl: fields[2] as String, + token: fields[3] as Token?, + ); + } + + @override + void write(BinaryWriter writer, User obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.username) + ..writeByte(1) + ..write(obj.password) + ..writeByte(2) + ..write(obj.hostUrl) + ..writeByte(3) + ..write(obj.token); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/portainer/endpoint.dart b/lib/models/portainer/endpoint.dart new file mode 100644 index 0000000..2350b40 --- /dev/null +++ b/lib/models/portainer/endpoint.dart @@ -0,0 +1,617 @@ +// To parse this JSON data, do +// +// final endpoint = endpointFromJson(jsonString); + +import 'dart:convert'; + +import 'package:portarius/models/portainer/user.dart'; +import 'package:portarius/services/remote.dart'; + +Endpoint endpointFromJson(String str) => Endpoint.fromJson(json.decode(str)); + +String endpointToJson(Endpoint data) => json.encode(data.toJson()); + +class Endpoint { + Endpoint({ + this.authorizedTeams, + this.authorizedUsers, + this.azureCredentials, + this.composeSyntaxMaxVersion, + this.edgeCheckinInterval, + this.edgeId, + this.edgeKey, + this.extensions, + this.groupId, + this.id, + this.kubernetes, + this.name, + this.publicUrl, + this.snapshots, + this.status, + this.tls, + this.tlscaCert, + this.tlsCert, + this.tlsConfig, + this.tlsKey, + this.tagIds, + this.tags, + this.teamAccessPolicies, + this.type, + this.url, + this.userAccessPolicies, + this.lastCheckInDate, + this.securitySettings, + }); + + List? authorizedTeams; + List? authorizedUsers; + AzureCredentials? azureCredentials; + String? composeSyntaxMaxVersion; + int? edgeCheckinInterval; + String? edgeId; + String? edgeKey; + List? extensions; + int? groupId; + int? id; + Kubernetes? kubernetes; + String? name; + String? publicUrl; + List? snapshots; + int? status; + bool? tls; + String? tlscaCert; + String? tlsCert; + TlsConfig? tlsConfig; + String? tlsKey; + List? tagIds; + List? tags; + AccessPolicies? teamAccessPolicies; + int? type; + String? url; + AccessPolicies? userAccessPolicies; + int? lastCheckInDate; + SecuritySettings? securitySettings; + + factory Endpoint.fromJson(Map json) => Endpoint( + authorizedTeams: + List.from(json["AuthorizedTeams"]?.map((x) => x) ?? []), + authorizedUsers: + List.from(json["AuthorizedUsers"]?.map((x) => x) ?? []), + azureCredentials: + AzureCredentials.fromJson(json["AzureCredentials"] ?? {}), + composeSyntaxMaxVersion: json["ComposeSyntaxMaxVersion"], + edgeCheckinInterval: json["EdgeCheckinInterval"], + edgeId: json["EdgeID"], + edgeKey: json["EdgeKey"], + extensions: List.from( + json["Extensions"]?.map((x) => Extension.fromJson(x)) ?? []), + groupId: json["GroupId"], + id: json["Id"], + kubernetes: Kubernetes.fromJson(json["Kubernetes"] ?? {}), + name: json["Name"], + publicUrl: json["PublicURL"], + snapshots: List.from( + json["Snapshots"]?.map((x) => EndpointSnapshot.fromJson(x)) ?? []), + status: json["Status"], + tls: json["TLS"], + tlscaCert: json["TLSCACert"], + tlsCert: json["TLSCert"], + tlsConfig: TlsConfig.fromJson(json["TLSConfig"] ?? {}), + tlsKey: json["TLSKey"], + tagIds: List.from(json["TagIds"]?.map((x) => x) ?? []), + tags: List.from(json["Tags"]?.map((x) => x) ?? []), + teamAccessPolicies: + AccessPolicies.fromJson(json["TeamAccessPolicies"] ?? {}), + type: json["Type"], + url: json["URL"], + userAccessPolicies: + AccessPolicies.fromJson(json["UserAccessPolicies"] ?? {}), + lastCheckInDate: json["lastCheckInDate"], + securitySettings: + SecuritySettings.fromJson(json["securitySettings"] ?? {}), + ); + + Map toJson() => { + "AuthorizedTeams": + List.from(authorizedTeams?.map((x) => x) ?? []), + "AuthorizedUsers": + List.from(authorizedUsers?.map((x) => x) ?? []), + "AzureCredentials": azureCredentials?.toJson(), + "ComposeSyntaxMaxVersion": composeSyntaxMaxVersion, + "EdgeCheckinInterval": edgeCheckinInterval, + "EdgeID": edgeId, + "EdgeKey": edgeKey, + "Extensions": + List.from(extensions?.map((x) => x.toJson()) ?? []), + "GroupId": groupId, + "Id": id, + "Kubernetes": kubernetes?.toJson(), + "Name": name, + "PublicURL": publicUrl, + "Snapshots": List.from(snapshots!.map((x) => x.toJson())), + "Status": status, + "TLS": tls, + "TLSCACert": tlscaCert, + "TLSCert": tlsCert, + "TLSConfig": tlsConfig?.toJson(), + "TLSKey": tlsKey, + "TagIds": List.from(tagIds?.map((x) => x) ?? []), + "Tags": List.from(tags?.map((x) => x) ?? []), + "TeamAccessPolicies": teamAccessPolicies?.toJson(), + "Type": type, + "URL": url, + "UserAccessPolicies": userAccessPolicies?.toJson(), + "lastCheckInDate": lastCheckInDate, + "securitySettings": securitySettings?.toJson(), + }; + + /// Refresh the endpoint's status. + Future refreshEndpoint(User user) async { + Endpoint newEndpint = (await RemoteService().getEndpoints(user)).firstWhere( + (x) => x.id == id, + orElse: () => throw Exception("Endpoint not found")); + authorizedTeams = newEndpint.authorizedTeams; + authorizedUsers = newEndpint.authorizedUsers; + azureCredentials = newEndpint.azureCredentials; + composeSyntaxMaxVersion = newEndpint.composeSyntaxMaxVersion; + edgeCheckinInterval = newEndpint.edgeCheckinInterval; + edgeId = newEndpint.edgeId; + edgeKey = newEndpint.edgeKey; + extensions = newEndpint.extensions; + groupId = newEndpint.groupId; + id = newEndpint.id; + kubernetes = newEndpint.kubernetes; + name = newEndpint.name; + publicUrl = newEndpint.publicUrl; + snapshots = newEndpint.snapshots; + status = newEndpint.status; + tls = newEndpint.tls; + tlscaCert = newEndpint.tlscaCert; + tlsCert = newEndpint.tlsCert; + tlsConfig = newEndpint.tlsConfig; + tlsKey = newEndpint.tlsKey; + tagIds = newEndpint.tagIds; + tags = newEndpint.tags; + teamAccessPolicies = newEndpint.teamAccessPolicies; + type = newEndpint.type; + url = newEndpint.url; + userAccessPolicies = newEndpint.userAccessPolicies; + lastCheckInDate = newEndpint.lastCheckInDate; + securitySettings = newEndpint.securitySettings; + } +} + +class AzureCredentials { + AzureCredentials({ + this.applicationId, + this.authenticationKey, + this.tenantId, + }); + + String? applicationId; + String? authenticationKey; + String? tenantId; + + factory AzureCredentials.fromJson(Map json) => + AzureCredentials( + applicationId: json["ApplicationID"], + authenticationKey: json["AuthenticationKey"], + tenantId: json["TenantID"], + ); + + Map toJson() => { + "ApplicationID": applicationId, + "AuthenticationKey": authenticationKey, + "TenantID": tenantId, + }; +} + +class Extension { + Extension({ + this.type, + this.url, + }); + + int? type; + String? url; + + factory Extension.fromJson(Map json) => Extension( + type: json["Type"], + url: json["URL"], + ); + + Map toJson() => { + "Type": type, + "URL": url, + }; +} + +class Kubernetes { + Kubernetes({ + this.configuration, + this.snapshots, + }); + + Configuration? configuration; + List? snapshots; + + factory Kubernetes.fromJson(Map json) => Kubernetes( + configuration: Configuration.fromJson(json["Configuration"]), + snapshots: List.from( + json["Snapshots"]?.map((x) => KubernetesSnapshot.fromJson(x)) ?? + []), + ); + + Map toJson() => { + "Configuration": configuration?.toJson(), + "Snapshots": + List.from(snapshots?.map((x) => x.toJson()) ?? []), + }; +} + +class Configuration { + Configuration({ + this.ingressClasses, + this.restrictDefaultNamespace, + this.storageClasses, + this.useLoadBalancer, + this.useServerMetrics, + }); + + List? ingressClasses; + bool? restrictDefaultNamespace; + List? storageClasses; + bool? useLoadBalancer; + bool? useServerMetrics; + + factory Configuration.fromJson(Map json) => Configuration( + ingressClasses: List.from( + json["IngressClasses"]?.map((x) => IngressClass.fromJson(x)) ?? []), + restrictDefaultNamespace: json["RestrictDefaultNamespace"], + storageClasses: List.from( + json["StorageClasses"]?.map((x) => StorageClass.fromJson(x)) ?? []), + useLoadBalancer: json["UseLoadBalancer"], + useServerMetrics: json["UseServerMetrics"], + ); + + Map toJson() => { + "IngressClasses": + List.from(ingressClasses?.map((x) => x.toJson()) ?? []), + "RestrictDefaultNamespace": restrictDefaultNamespace, + "StorageClasses": + List.from(storageClasses?.map((x) => x.toJson()) ?? []), + "UseLoadBalancer": useLoadBalancer, + "UseServerMetrics": useServerMetrics, + }; +} + +class IngressClass { + IngressClass({ + this.name, + this.type, + }); + + String? name; + String? type; + + factory IngressClass.fromJson(Map json) => IngressClass( + name: json["Name"], + type: json["Type"], + ); + + Map toJson() => { + "Name": name, + "Type": type, + }; +} + +class StorageClass { + StorageClass({ + this.accessModes, + this.allowVolumeExpansion, + this.name, + this.provisioner, + }); + + List? accessModes; + bool? allowVolumeExpansion; + String? name; + String? provisioner; + + factory StorageClass.fromJson(Map json) => StorageClass( + accessModes: List.from(json["AccessModes"].map((x) => x)), + allowVolumeExpansion: json["AllowVolumeExpansion"], + name: json["Name"], + provisioner: json["Provisioner"], + ); + + Map toJson() => { + "AccessModes": List.from(accessModes?.map((x) => x) ?? []), + "AllowVolumeExpansion": allowVolumeExpansion, + "Name": name, + "Provisioner": provisioner, + }; +} + +class KubernetesSnapshot { + KubernetesSnapshot({ + this.kubernetesVersion, + this.nodeCount, + this.time, + this.totalCpu, + this.totalMemory, + }); + + String? kubernetesVersion; + int? nodeCount; + int? time; + int? totalCpu; + int? totalMemory; + + factory KubernetesSnapshot.fromJson(Map json) => + KubernetesSnapshot( + kubernetesVersion: json["KubernetesVersion"], + nodeCount: json["NodeCount"], + time: json["Time"], + totalCpu: json["TotalCPU"], + totalMemory: json["TotalMemory"], + ); + + Map toJson() => { + "KubernetesVersion": kubernetesVersion, + "NodeCount": nodeCount, + "Time": time, + "TotalCPU": totalCpu, + "TotalMemory": totalMemory, + }; +} + +class SecuritySettings { + SecuritySettings({ + this.allowBindMountsForRegularUsers, + this.allowContainerCapabilitiesForRegularUsers, + this.allowDeviceMappingForRegularUsers, + this.allowHostNamespaceForRegularUsers, + this.allowPrivilegedModeForRegularUsers, + this.allowStackManagementForRegularUsers, + this.allowSysctlSettingForRegularUsers, + this.allowVolumeBrowserForRegularUsers, + this.enableHostManagementFeatures, + }); + + bool? allowBindMountsForRegularUsers; + bool? allowContainerCapabilitiesForRegularUsers; + bool? allowDeviceMappingForRegularUsers; + bool? allowHostNamespaceForRegularUsers; + bool? allowPrivilegedModeForRegularUsers; + bool? allowStackManagementForRegularUsers; + bool? allowSysctlSettingForRegularUsers; + bool? allowVolumeBrowserForRegularUsers; + bool? enableHostManagementFeatures; + + factory SecuritySettings.fromJson(Map json) => + SecuritySettings( + allowBindMountsForRegularUsers: json["allowBindMountsForRegularUsers"], + allowContainerCapabilitiesForRegularUsers: + json["allowContainerCapabilitiesForRegularUsers"], + allowDeviceMappingForRegularUsers: + json["allowDeviceMappingForRegularUsers"], + allowHostNamespaceForRegularUsers: + json["allowHostNamespaceForRegularUsers"], + allowPrivilegedModeForRegularUsers: + json["allowPrivilegedModeForRegularUsers"], + allowStackManagementForRegularUsers: + json["allowStackManagementForRegularUsers"], + allowSysctlSettingForRegularUsers: + json["allowSysctlSettingForRegularUsers"], + allowVolumeBrowserForRegularUsers: + json["allowVolumeBrowserForRegularUsers"], + enableHostManagementFeatures: json["enableHostManagementFeatures"], + ); + + Map toJson() => { + "allowBindMountsForRegularUsers": allowBindMountsForRegularUsers, + "allowContainerCapabilitiesForRegularUsers": + allowContainerCapabilitiesForRegularUsers, + "allowDeviceMappingForRegularUsers": allowDeviceMappingForRegularUsers, + "allowHostNamespaceForRegularUsers": allowHostNamespaceForRegularUsers, + "allowPrivilegedModeForRegularUsers": + allowPrivilegedModeForRegularUsers, + "allowStackManagementForRegularUsers": + allowStackManagementForRegularUsers, + "allowSysctlSettingForRegularUsers": allowSysctlSettingForRegularUsers, + "allowVolumeBrowserForRegularUsers": allowVolumeBrowserForRegularUsers, + "enableHostManagementFeatures": enableHostManagementFeatures, + }; +} + +class EndpointSnapshot { + EndpointSnapshot({ + this.dockerSnapshotRaw, + this.dockerVersion, + this.healthyContainerCount, + this.imageCount, + this.nodeCount, + this.runningContainerCount, + this.serviceCount, + this.stackCount, + this.stoppedContainerCount, + this.swarm, + this.time, + this.totalCpu, + this.totalMemory, + this.unhealthyContainerCount, + this.volumeCount, + }); + + DockerSnapshotRaw? dockerSnapshotRaw; + String? dockerVersion; + int? healthyContainerCount; + int? imageCount; + int? nodeCount; + int? runningContainerCount; + int? serviceCount; + int? stackCount; + int? stoppedContainerCount; + bool? swarm; + int? time; + int? totalCpu; + int? totalMemory; + int? unhealthyContainerCount; + int? volumeCount; + + factory EndpointSnapshot.fromJson(Map json) => + EndpointSnapshot( + dockerSnapshotRaw: + DockerSnapshotRaw.fromJson(json["DockerSnapshotRaw"]), + dockerVersion: json["DockerVersion"], + healthyContainerCount: json["HealthyContainerCount"], + imageCount: json["ImageCount"], + nodeCount: json["NodeCount"], + runningContainerCount: json["RunningContainerCount"], + serviceCount: json["ServiceCount"], + stackCount: json["StackCount"], + stoppedContainerCount: json["StoppedContainerCount"], + swarm: json["Swarm"], + time: json["Time"], + totalCpu: json["TotalCPU"], + totalMemory: json["TotalMemory"], + unhealthyContainerCount: json["UnhealthyContainerCount"], + volumeCount: json["VolumeCount"], + ); + + Map toJson() => { + "DockerSnapshotRaw": dockerSnapshotRaw?.toJson(), + "DockerVersion": dockerVersion, + "HealthyContainerCount": healthyContainerCount, + "ImageCount": imageCount, + "NodeCount": nodeCount, + "RunningContainerCount": runningContainerCount, + "ServiceCount": serviceCount, + "StackCount": stackCount, + "StoppedContainerCount": stoppedContainerCount, + "Swarm": swarm, + "Time": time, + "TotalCPU": totalCpu, + "TotalMemory": totalMemory, + "UnhealthyContainerCount": unhealthyContainerCount, + "VolumeCount": volumeCount, + }; +} + +class DockerSnapshotRaw { + DockerSnapshotRaw({ + this.containers, + this.images, + this.info, + this.networks, + this.version, + this.volumes, + }); + + Containers? containers; + Containers? images; + Containers? info; + Containers? networks; + Containers? version; + Containers? volumes; + + factory DockerSnapshotRaw.fromJson(Map json) => + DockerSnapshotRaw( + containers: Containers.fromJson(json["Containers"] ?? {}), + images: Containers.fromJson(json["Images"] ?? {}), + info: Containers.fromJson(json["Info"] ?? {}), + networks: Containers.fromJson(json["Networks"] ?? {}), + version: Containers.fromJson(json["Version"] ?? {}), + volumes: Containers.fromJson(json["Volumes"] ?? {}), + ); + + Map toJson() => { + "Containers": containers?.toJson(), + "Images": images?.toJson(), + "Info": info?.toJson(), + "Networks": networks?.toJson(), + "Version": version?.toJson(), + "Volumes": volumes?.toJson(), + }; +} + +class Containers { + Containers(); + + factory Containers.fromJson(Map json) => Containers(); + + Map toJson() => {}; +} + +class AccessPolicies { + AccessPolicies({ + this.additionalProp1, + this.additionalProp2, + this.additionalProp3, + }); + + AdditionalProp? additionalProp1; + AdditionalProp? additionalProp2; + AdditionalProp? additionalProp3; + + factory AccessPolicies.fromJson(Map json) => AccessPolicies( + additionalProp1: AdditionalProp.fromJson(json["additionalProp1"] ?? {}), + additionalProp2: AdditionalProp.fromJson(json["additionalProp2"] ?? {}), + additionalProp3: AdditionalProp.fromJson(json["additionalProp3"] ?? {}), + ); + + Map toJson() => { + "additionalProp1": additionalProp1?.toJson(), + "additionalProp2": additionalProp2?.toJson(), + "additionalProp3": additionalProp3?.toJson(), + }; +} + +class AdditionalProp { + AdditionalProp({ + this.roleId, + }); + + int? roleId; + + factory AdditionalProp.fromJson(Map json) => AdditionalProp( + roleId: json["RoleId"], + ); + + Map toJson() => { + "RoleId": roleId, + }; +} + +class TlsConfig { + TlsConfig({ + this.tls, + this.tlscaCert, + this.tlsCert, + this.tlsKey, + this.tlsSkipVerify, + }); + + bool? tls; + String? tlscaCert; + String? tlsCert; + String? tlsKey; + bool? tlsSkipVerify; + + factory TlsConfig.fromJson(Map json) => TlsConfig( + tls: json["TLS"], + tlscaCert: json["TLSCACert"], + tlsCert: json["TLSCert"], + tlsKey: json["TLSKey"], + tlsSkipVerify: json["TLSSkipVerify"], + ); + + Map toJson() => { + "TLS": tls, + "TLSCACert": tlscaCert, + "TLSCert": tlsCert, + "TLSKey": tlsKey, + "TLSSkipVerify": tlsSkipVerify, + }; +} diff --git a/lib/models/portainer/token.dart b/lib/models/portainer/token.dart new file mode 100644 index 0000000..2817987 --- /dev/null +++ b/lib/models/portainer/token.dart @@ -0,0 +1,36 @@ +// To parse this JSON data, do +// +// final token = tokenFromJson(jsonString); +import 'dart:convert'; + +import 'package:hive/hive.dart'; + +part '../hive/token.g.dart'; + +Token tokenFromJson(String str) => Token.fromJson(json.decode(str)); + +String tokenToJson(Token data) => json.encode(data.toJson()); + +@HiveType(typeId: 1, adapterName: 'TokenAdapter') +class Token { + Token({ + required this.jwt, + }); + + @HiveField(0) + String jwt; + + factory Token.fromJson(Map json) => Token( + jwt: json["jwt"], + ); + + Map toJson() => { + "jwt": jwt, + }; + + String getBearerToken() { + return 'Bearer $jwt'; + } + + bool get hasJwt => jwt.isNotEmpty; +} diff --git a/lib/models/portainer/user.dart b/lib/models/portainer/user.dart new file mode 100644 index 0000000..b9b7ed4 --- /dev/null +++ b/lib/models/portainer/user.dart @@ -0,0 +1,124 @@ +import 'package:flutter/cupertino.dart'; +import 'package:hive/hive.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:provider/provider.dart'; +import 'package:toast/toast.dart'; +import '../../services/remote.dart'; +import 'token.dart'; + +part '../hive/user.g.dart'; + +/// [User] model for Portainer. +/// +/// This model is used to store the username, password and host URL of the user. +/// [Token] is also stored in this model, but can be null. +/// +/// The token is used to access the Portainer API. +@HiveType(typeId: 0, adapterName: 'UserAdapter') +class User extends ChangeNotifier { + /// [username] of the user. + @HiveField(0) + String username; + + /// [password] of the user. + @HiveField(1) + String password; + + /// [hostUrl] of the portainer API + @HiveField(2) + String hostUrl; + + /// [token] for the API calls + @HiveField(3) + late Token? token; + + User( + {required this.username, + required this.password, + required this.hostUrl, + this.token}); + + /// Tries to auth [User] + /// + /// If [Token] is returned, it is stored in [User] + /// If [Token] is null, null is returned + Future authPortainer() async { + Token? newToken = await RemoteService().authPortainer( + username, + password, + hostUrl, + ); + + if (newToken == null) { + return null; + } + + if (newToken.jwt.isNotEmpty) { + print('Setting new token.'); + token = newToken; + notifyListeners(); + } + + return newToken; + } + + /// Checks if [Token] is valid + Future isTokenValid(Token token) async { + return await RemoteService().isTokenValid(this); + } + + /// Logs out the [User]. + /// It will remove [User] from Hive storage. + /// It will also remove [Token] from [User]. + /// [BuildContext] is required to get the [StorageManager] from the [Provider] + Future logOutUser(BuildContext context) async { + if (await RemoteService().logoutPortainer(this)) { + var storage = Provider.of(context, listen: false); + await storage.clearUser(); + + Future.delayed(const Duration(milliseconds: 100), () { + username = ''; + password = ''; + hostUrl = ''; + resetToken(); + }); + } else { + Toast.show('Logout failed.'); + } + } + + void setToken(Token newToken) { + token = newToken; + notifyListeners(); + } + + void resetToken() { + token = null; + notifyListeners(); + } + + void setNewUser(User user) { + username = user.username; + password = user.password; + + if (user.hostUrl.endsWith('/')) { + hostUrl = user.hostUrl.substring(0, user.hostUrl.length - 1); + } else { + hostUrl = user.hostUrl; + } + + token = user.token; + notifyListeners(); + } + + /// Operator == for [User] + /// Two [User] are equal if their [username] and [hostUrl] and [token] are equal. + @override + // ignore: hash_and_equals + bool operator ==(Object other) => + identical(this, other) || + other is User && + runtimeType == other.runtimeType && + username == other.username && + hostUrl == other.hostUrl; +} diff --git a/lib/pages/auth/authpage.dart b/lib/pages/auth/authpage.dart new file mode 100644 index 0000000..025ac79 --- /dev/null +++ b/lib/pages/auth/authpage.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:portarius/components/buttons/big_blue_button.dart'; +import 'package:portarius/models/portainer/user.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:portarius/utils/style.dart'; +import 'package:provider/provider.dart'; +import 'package:toast/toast.dart'; + +import '../../models/portainer/token.dart'; + +/// [AuthPage] for Portarius. +/// This page is used to authenticate with Portainer. +/// It will display a form to enter the host url, username and password. +/// Once the user has entered the credentials, it will send a request to Portainer +/// to authenticate. If the credentials are correct, it will store the token in +/// the local storage. +/// The token will be used to access the Portainer API. +class AuthPage extends StatefulWidget { + const AuthPage({Key? key}) : super(key: key); + + @override + State createState() => _AuthPageState(); +} + +class _AuthPageState extends State { + final TextEditingController _hostUrlController = TextEditingController(); + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + final _formKey = GlobalKey(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + + // Open the local storage. + _loadDataFromStorage(); + } + + @override + void dispose() { + super.dispose(); + _hostUrlController.dispose(); + _usernameController.dispose(); + _passwordController.dispose(); + } + + @override + Widget build(BuildContext context) { + User user = Provider.of(context, listen: true); + StorageManager storage = Provider.of(context, listen: true); + StyleManager style = Provider.of(context, listen: true); + Size size = MediaQuery.of(context).size; + + if (user.token != null && mounted) { + Future.delayed(const Duration(milliseconds: 150), () { + Navigator.pushReplacementNamed(context, '/'); + }); + } + + return Scaffold( + body: SingleChildScrollView( + child: SizedBox( + height: size.height, + width: size.width, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + flex: 4, + child: Container( + margin: EdgeInsets.only(top: size.height * 0.05), + child: + Image.asset('assets/icons/icon.png', fit: BoxFit.fill)), + ), + Flexible( + flex: 5, + child: Center( + child: SizedBox( + width: size.width * 0.85 > 500 ? 500 : size.width * 0.85, + child: Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Form( + autovalidateMode: AutovalidateMode.disabled, + key: _formKey, + child: Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + 'Authentication', + style: Theme.of(context) + .textTheme + .headline6 + ?.copyWith(fontWeight: FontWeight.w500), + ), + SizedBox( + height: 15, + width: size.width, + ), + TextFormField( + controller: _hostUrlController, + scrollPadding: + EdgeInsets.all(size.height * .15), + textInputAction: TextInputAction.next, + keyboardType: TextInputType.url, + decoration: const InputDecoration( + labelText: 'Host URL', + hintText: 'https://portainer.example.com', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a host URL'; + } + return null; + }, + ), + SizedBox( + height: 10, + width: size.width, + ), + TextFormField( + controller: _usernameController, + scrollPadding: EdgeInsets.all(size.height * .1), + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Username', + hintText: 'admin', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username'; + } + return null; + }, + ), + SizedBox( + height: 10, + width: size.width, + ), + TextFormField( + controller: _passwordController, + scrollPadding: + EdgeInsets.all(size.height * .12), + obscureText: true, + textInputAction: TextInputAction.done, + onEditingComplete: () => + _authenticateButton(user, storage), + decoration: const InputDecoration( + labelText: 'Password', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a password'; + } + return null; + }, + ), + SizedBox( + height: 15, + width: size.width, + ), + if (_isLoading) ...[ + const CircularProgressIndicator(), + ] else ...[ + BigBlueButton( + formKey: _formKey, + onClick: () => + _authenticateButton(user, storage), + buttonTitle: 'Log In', + ), + ], + ], + ), + ), + ), + ), + ), + ), + ), + Flexible( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + icon: const Icon(Icons.group), + onPressed: () { + Navigator.pushNamed(context, '/users', arguments: true); + }, + ), + IconButton( + icon: Theme.of(context).brightness == Brightness.dark + ? const Icon(Icons.light_mode) + : const Icon(Icons.dark_mode), + onPressed: () { + style.setTheme( + Theme.of(context).brightness == Brightness.dark + ? ThemeMode.light + : ThemeMode.dark); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + _loadDataFromStorage() async { + setState(() { + _isLoading = true; + }); + + // Load the data from the local storage. + var box = await Hive.openBox('portainer'); + + User? userData = box.get('user'); + + setState(() { + _hostUrlController.text = userData?.hostUrl ?? ''; + _usernameController.text = userData?.username ?? ''; + _passwordController.text = userData?.password ?? ''; + + _isLoading = false; + }); + } + + _authenticateButton(User user, StorageManager storage) async { + setState(() { + _isLoading = true; + }); + + user.username = _usernameController.text; + user.password = _passwordController.text; + user.hostUrl = _hostUrlController.text; + + Token? token = await user.authPortainer(); + + if (token != null) { + await storage.addUserToList(user); + await storage.saveUser(user); + user.setToken(token); + } else { + _showToast('Authentication failed.'); + } + + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + + _showToast(String message) { + ToastContext().init(context); + Toast.show(message, duration: Toast.lengthLong); + } +} diff --git a/lib/pages/container/container_details.dart b/lib/pages/container/container_details.dart new file mode 100644 index 0000000..399479e --- /dev/null +++ b/lib/pages/container/container_details.dart @@ -0,0 +1,319 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_speed_dial/flutter_speed_dial.dart'; +import 'package:portarius/components/appbar/appbar.dart'; +import 'package:portarius/components/cards/status_card.dart'; +import 'package:portarius/models/docker/detailed_container.dart'; +import 'package:portarius/models/docker/docker_container.dart'; +import 'package:portarius/models/portainer/endpoint.dart'; +import 'package:portarius/services/remote.dart'; +import 'package:portarius/utils/settings.dart'; +import 'package:provider/provider.dart'; +import 'package:toast/toast.dart'; + +import '../../models/portainer/user.dart'; + +class ContainerDetailsPage extends StatefulWidget { + const ContainerDetailsPage({Key? key}) : super(key: key); + + @override + State createState() => _ContainerDetailsPageState(); +} + +class _ContainerDetailsPageState extends State { + final ScrollController _scrollController = ScrollController(); + final ScrollController _logScrollController = ScrollController(); + bool _refreshing = false; + DetailedDockerContainer? detailedContainer; + bool _onLoad = true; + bool _showLogs = false; + bool _autoScroll = true; + + List _logs = []; + + @override + Widget build(BuildContext context) { + User user = Provider.of(context, listen: true); + SettingsManager settingsManager = + Provider.of(context, listen: false); + + Size size = MediaQuery.of(context).size; + + Map arguments = + ModalRoute.of(context)?.settings.arguments as Map; + DockerContainer container = arguments['container']; + Endpoint endpoint = arguments['endpoint']; + + if (_onLoad && mounted) { + _onLoad = false; + _getDetailedData(endpoint, container, user); + } + + if (settingsManager.autoRefresh && !_refreshing) { + _refreshing = true; + Future.delayed(Duration(seconds: settingsManager.autoRefreshInterval), + () { + if (mounted) { + _refreshing = false; + _getDetailedData(endpoint, container, user); + + if (_showLogs && mounted) { + _getLogs(endpoint, container, user); + } + } + }); + } + + if (detailedContainer == null) { + return Scaffold( + body: CustomScrollView( + shrinkWrap: true, + controller: _scrollController, + slivers: const [ + PortariusAppBar(), + SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), + ) + ], + ), + ); + } + + detailedContainer?.mounts + ?.sort((a, b) => a.destination!.compareTo(b.destination!)); + + return Scaffold( + floatingActionButton: SpeedDial( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + spaceBetweenChildren: 2.5, + spacing: 5, + children: [ + /*SpeedDialChild( + child: detailedContainer!.state!.status == 'running' + ? const Icon(Icons.stop_circle_outlined) + : const Icon(Icons.play_circle_outlined), + label: detailedContainer!.state!.status == 'running' + ? 'Stop' + : 'Start', + onTap: () { + + }, + ),*/ + SpeedDialChild( + child: const Icon(Icons.data_array), + label: 'Logs', + onTap: () { + setState(() { + _showLogs = !_showLogs; + if (_showLogs) { + // scroll to the bottom of page + Future.delayed(const Duration(milliseconds: 50), () { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: Duration(milliseconds: 500), + curve: Curves.easeOut); + }); + } + }); + }, + ), + /*SpeedDialChild( + child: const Icon(Icons.delete), + label: 'Delete', + onTap: () {}, + ),*/ + ], + child: const Icon(Icons.menu), + ), + body: CustomScrollView( + shrinkWrap: true, + controller: _scrollController, + slivers: [ + PortariusAppBar( + title: detailedContainer!.name ?? 'Unknown', + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Wrap( + clipBehavior: Clip.antiAlias, + alignment: WrapAlignment.start, + spacing: 10, + children: [ + DockerStatusCard(detailedContainer: detailedContainer!), + if (detailedContainer!.mounts!.isNotEmpty) + Card( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Text('Volumes', + style: Theme.of(context).textTheme.headline6), + ...detailedContainer!.mounts!.map((mount) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + Text( + '${mount.source} -> ${mount.destination}', + ), + ], + ); + }), + ], + ), + ), + ), + if (_showLogs) ...[ + Card( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 20, + right: 10, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('Logs', + style: Theme.of(context) + .textTheme + .headline6), + Row( + children: [ + const Text('Auto scroll'), + Switch( + value: _autoScroll, + onChanged: (value) { + setState(() { + _autoScroll = value; + }); + }, + ), + ], + ), + ], + ), + ), + const Divider(), + SizedBox( + height: size.height * 0.65, + child: _logs.isEmpty + ? const Center( + child: CircularProgressIndicator()) + : ListView( + controller: _logScrollController, + children: [ + ..._logs.map((line) { + return InkWell( + customBorder: + RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(18), + ), + onLongPress: () { + Clipboard.setData( + ClipboardData(text: line)); + ToastContext().init(context); + Toast.show( + 'Copied line to clipboard', + duration: Toast.lengthLong, + gravity: Toast.bottom); + }, + child: Padding( + padding: const EdgeInsets.only( + left: 10, + right: 10, + ), + child: Column( + children: [ + SizedBox( + height: 15, + ), + Text(line), + const Divider(), + ], + ), + ), + ); + }), + ], + ), + ), + ], + ), + ), + ), + ], + ], + ), + ), + ) + ], + ), + ); + } + + _getDetailedData( + Endpoint endpoint, DockerContainer container, User user) async { + DetailedDockerContainer? detailedContainer = + await RemoteService().getDockerContainer(user, endpoint, container.id); + if (detailedContainer != null && mounted) { + setState(() { + this.detailedContainer = detailedContainer; + }); + } + } + + void _getLogs(Endpoint endpoint, DockerContainer container, User user) async { + List logs = + await RemoteService().getContainerLogs(user, endpoint, container.id); + + // merged list + List mergedLogs = []; + mergedLogs.addAll(_logs); + + print('logs: ${mergedLogs.length}'); + + // find last line in old logs + String lastLine = ''; + + if (_logs.isNotEmpty) { + lastLine = _logs.last; + } + + if (lastLine != '') { + for (String line in logs) { + if (line.compareTo(lastLine) > 0) { + mergedLogs.add(line); + break; + } + } + } else { + mergedLogs.addAll(logs); + } + + print('logs: ${mergedLogs.length}'); + + setState(() { + _logs = mergedLogs; + Future.delayed(const Duration(milliseconds: 10), () { + if (_showLogs && + _autoScroll && + _logScrollController.positions.isNotEmpty && + mounted) { + _logScrollController.animateTo( + _logScrollController.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.easeIn); + } + }); + }); + } +} diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart new file mode 100644 index 0000000..3cf4648 --- /dev/null +++ b/lib/pages/home/home.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:portarius/components/appbar/appbar.dart'; +import 'package:portarius/components/drawer/drawer.dart'; +import 'package:portarius/models/portainer/user.dart'; +import 'package:portarius/services/remote.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:portarius/utils/settings.dart'; +import 'package:portarius/utils/style.dart'; +import 'package:provider/provider.dart'; + +import '../../components/lists/container_grid_list.dart'; +import '../../models/portainer/endpoint.dart'; + +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + List _endpoints = []; + Endpoint? _selectedEndpoint; + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + super.dispose(); + _scrollController.dispose(); + } + + @override + Widget build(BuildContext context) { + User user = Provider.of(context, listen: true); + StorageManager storage = Provider.of(context, listen: true); + SettingsManager settings = + Provider.of(context, listen: true); + // ignore: unused_local_variable + StyleManager style = Provider.of(context, listen: true); + Size size = MediaQuery.of(context).size; + + /// Get the endpoints from the API and store them in the [_endpoints] list. + /// Then select the first endpoint in the list as the selected endpoint. + if (_selectedEndpoint == null || _endpoints.isEmpty) { + RemoteService().getEndpoints(user).then((endpoints) { + if (mounted) { + setState(() { + /// Select the first endpoint. + if (endpoints.isNotEmpty) { + if (settings.selectedEndpointId == null) { + _selectedEndpoint = endpoints.first; + settings.selectedEndpointId = endpoints.first.id; + storage.saveEndpointId(endpoints.first.id ?? 1); + } else { + _selectedEndpoint = endpoints.firstWhere( + (endpoint) => endpoint.id == settings.selectedEndpointId, + orElse: () => endpoints.first, + ); + } + } + _endpoints = endpoints; + }); + } + }); + } + + return WillPopScope( + onWillPop: () async { + if (_scrollController.hasClients) { + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + + if (_scrollController.offset == 0) { + bool result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Are you sure?'), + content: const Text('Do you want to exit the application?'), + actions: [ + TextButton( + child: const Text('No'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: const Text('Yes'), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ), + ); + if (result) { + SystemNavigator.pop(); + } + } else { + return false; + } + } + return false; + }, + child: Scaffold( + drawer: const Padding( + padding: EdgeInsets.only( + top: 45, + bottom: 10, + ), + child: PortariusDrawer( + pageRoute: '/home', + )), + body: RefreshIndicator( + onRefresh: () async { + _endpoints = await RemoteService().getEndpoints(user); + setState(() { + _selectedEndpoint = null; + }); + }, + edgeOffset: size.height * .25, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + PortariusAppBar(endpoint: _selectedEndpoint), + _selectedEndpoint == null + ? const SliverFillRemaining( + child: Center( + child: CircularProgressIndicator(), + ), + ) + : SliverPadding( + padding: EdgeInsets.only( + bottom: size.height * 0.05, + ), + sliver: ContainerGrid( + endpoint: _selectedEndpoint!, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/loading/loading.dart b/lib/pages/loading/loading.dart new file mode 100644 index 0000000..173cb6b --- /dev/null +++ b/lib/pages/loading/loading.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:provider/provider.dart'; + +import '../../utils/settings.dart'; + +class LoadingPage extends StatefulWidget { + const LoadingPage({Key? key}) : super(key: key); + + @override + State createState() => _LoadingPageState(); +} + +class _LoadingPageState extends State { + bool _timeout = false; + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + StorageManager storage = Provider.of(context, listen: true); + SettingsManager settings = + Provider.of(context, listen: true); + + if (!_timeout) { + Future.delayed(const Duration(seconds: 10), () { + if (mounted) { + setState(() { + _timeout = true; + }); + } + }); + } + + return Scaffold( + body: Center( + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + flex: 45, + child: Container( + margin: EdgeInsets.only(top: size.height * 0.05), + child: + Image.asset('assets/icons/icon.png', fit: BoxFit.fill)), + ), + Flexible( + flex: 15, + child: Container(), + ), + Flexible( + flex: 40, + child: _timeout + ? Center( + child: TextButton( + onPressed: () async { + setState(() { + _timeout = false; + }); + await storage.init(context); + await settings.init(storage); + }, + child: const Text( + 'Try Again', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ), + ) + : Column( + children: const [ + CircularProgressIndicator(), + SizedBox(height: 12.5), + Text('Loading...'), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart new file mode 100644 index 0000000..b0df2f6 --- /dev/null +++ b/lib/pages/settings/settings.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/components/cards/about_tile.dart'; +import 'package:portarius/services/local_auth.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:portarius/utils/settings.dart'; +import 'package:provider/provider.dart'; + +import '../../components/appbar/appbar.dart'; +import '../../components/drawer/drawer.dart'; +import '../../models/portainer/user.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({Key? key}) : super(key: key); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + super.dispose(); + _scrollController.dispose(); + } + + @override + Widget build(BuildContext context) { + SettingsManager settings = + Provider.of(context, listen: true); + StorageManager storage = Provider.of(context, listen: true); + User user = Provider.of(context, listen: true); + + return WillPopScope( + onWillPop: () async { + Navigator.of(context).pushReplacementNamed( + '/home', + ); + return true; + }, + child: Scaffold( + drawer: const Padding( + padding: EdgeInsets.only( + top: 45, + bottom: 10, + ), + child: PortariusDrawer( + pageRoute: '/settings', + )), + body: CustomScrollView( + controller: _scrollController, + shrinkWrap: true, + slivers: [ + const PortariusAppBar(), + SliverToBoxAdapter( + child: Column( + children: [ + Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 15, + top: 20, + ), + child: Text( + 'Page Refresh', + style: Theme.of(context).textTheme.headline4, + ), + ), + ), + Card( + margin: + const EdgeInsets.only(left: 15, right: 15, top: 20), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: ListTile( + title: const Text('Auto Refresh'), + subtitle: const Text('Toggles the auto refresh.'), + trailing: Switch( + value: settings.autoRefresh, + onChanged: (value) async { + await storage.saveAutoRefresh(value); + await settings.refreshSettings(storage); + }, + ), + ), + ), + ), + Card( + margin: + const EdgeInsets.only(left: 15, right: 15, top: 10), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: ListTile( + enabled: settings.autoRefresh, + title: const Text('Auto Refresh Interval'), + subtitle: const Text( + 'Sets the auto refresh interval in seconds.'), + trailing: DropdownButton( + value: settings.autoRefreshInterval, + items: [ + ...[1, 3, 5, 10, 15, 30] + .map( + (interval) => DropdownMenuItem( + value: interval, + child: Text( + '${interval}s', + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ) + .toList(), + ], + onChanged: !settings.autoRefresh + ? null + : (int? value) { + storage + .saveAutoRefreshInterval(value ?? 10); + settings.refreshSettings(storage); + }, + ), + ), + ), + ), + ], + ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 15, + top: 20, + ), + child: Text( + 'App Lock', + style: Theme.of(context).textTheme.headline4, + ), + ), + ), + Card( + margin: const EdgeInsets.only(left: 15, right: 15, top: 20), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: ListTile( + title: const Text('Use biometric authentication'), + subtitle: + const Text('Toggles biometric authentication.'), + trailing: Switch( + value: settings.biometricEnabled, + onChanged: (value) async { + if (await LocalAuthManager.deviceSupported()) { + await storage.saveBiometric(value); + await settings.refreshSettings(storage); + } else { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text( + 'Biometric authentication'), + content: const Text( + 'Your device does not support biometric authentication.' + '\n\nThis may be because you have not enabled it in your device settings.'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }); + } + }, + ), + ), + ), + ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 15, + top: 20, + ), + child: Text( + 'About', + style: Theme.of(context).textTheme.headline4, + ), + ), + ), + const PortariusAboutTile(), + const SizedBox( + height: 20, + ), + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/users/user_managment.dart b/lib/pages/users/user_managment.dart new file mode 100644 index 0000000..7a557db --- /dev/null +++ b/lib/pages/users/user_managment.dart @@ -0,0 +1,339 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/components/cards/setting_tile.dart'; +import 'package:portarius/services/remote.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:provider/provider.dart'; +import 'package:toast/toast.dart'; + +import '../../components/appbar/appbar.dart'; +import '../../components/drawer/drawer.dart'; +import '../../models/portainer/token.dart'; +import '../../models/portainer/user.dart'; + +class UserManagerPage extends StatefulWidget { + const UserManagerPage({ + Key? key, + }) : super(key: key); + + @override + State createState() => _UserManagerPageState(); +} + +class _UserManagerPageState extends State { + final ScrollController _scrollController = ScrollController(); + bool _areUsersLoaded = false; + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + _scrollController.dispose(); + } + + @override + Widget build(BuildContext context) { + StorageManager storage = Provider.of(context, listen: true); + User user = Provider.of(context, listen: true); + + List userList = storage.savedUsers; + bool? fromAuthPage = + ModalRoute.of(context)?.settings.arguments as bool? ?? false; + + if (!_areUsersLoaded) { + storage.refreshUsers().then((value) => setState(() { + _areUsersLoaded = true; + })); + } + + return WillPopScope( + onWillPop: () async { + if (fromAuthPage) { + Navigator.of(context).pushReplacementNamed('/'); + } else { + Navigator.of(context).pushReplacementNamed('/home'); + } + return false; + }, + child: Scaffold( + floatingActionButton: FloatingActionButton( + tooltip: 'Add User', + onPressed: () async { + await _showAddUserDialog(storage); + }, + child: const Icon(Icons.add), + ), + drawer: fromAuthPage + ? null + : const Padding( + padding: EdgeInsets.only( + top: 45, + bottom: 10, + ), + child: PortariusDrawer( + pageRoute: '/users', + )), + body: CustomScrollView( + controller: _scrollController, + shrinkWrap: true, + slivers: [ + const PortariusAppBar(), + SliverToBoxAdapter( + child: Column( + children: [ + Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 20, + top: 20, + ), + child: Text( + 'Users', + style: Theme.of(context).textTheme.headline4, + ), + ), + ), + ListBody( + children: [ + ...userList.map((u) { + return Padding( + padding: const EdgeInsets.all(0.0), + child: PortariusSettingTile( + enabled: fromAuthPage ? true : user != u, + title: u.username, + subtitle: u.hostUrl, + trailing: user != u + ? const Icon(Icons.person) + : const Icon( + Icons.check, + size: 30, + ), + onTap: () async { + await _showUserDialog( + user, u, storage, fromAuthPage); + }, + ), + ); + }), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + + _validateUser(String username, String hostUrl, String password, + StorageManager storage) async { + Token? token = + await RemoteService().authPortainer(username, password, hostUrl); + + if (token == null && mounted) { + ToastContext().init(context); + Toast.show( + 'Invalid credentials.', + ); + return null; + } + + if (hostUrl.endsWith('/')) { + hostUrl = hostUrl.substring(0, hostUrl.length - 1); + } + + if (token != null) { + User user = User( + username: username, + hostUrl: hostUrl, + password: password, + token: token, + ); + await storage.addUserToList(user); + if (mounted) { + Navigator.pop(context); + } + } + } + + _showUserDialog(User loggedUser, User clickedUser, StorageManager storage, + bool fromAuthPage) async { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(clickedUser.username), + content: Text( + 'Viewing user\n${clickedUser.username}@${clickedUser.hostUrl}', + textAlign: TextAlign.center, + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + child: const Text('Select'), + onPressed: () async { + loggedUser.setNewUser(clickedUser); + await storage.saveUser(clickedUser); + if (mounted) { + storage.initUser(context); + if (fromAuthPage) { + Navigator.pushNamedAndRemoveUntil( + context, '/auth', (route) => false); + } else { + Navigator.pop(context); + } + } + }, + ), + TextButton( + child: const Text('Delete'), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete User'), + content: const Text( + 'Are you sure you want to delete this user?', + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + child: const Text('Delete'), + onPressed: () async { + await storage.removeUserFromList(clickedUser); + if (mounted) { + Navigator.pop(context); + Navigator.pop(context); + } + }, + ), + ], + ); + }, + ); + }, + ), + ], + ); + }, + ); + } + + _showAddUserDialog(StorageManager storage) { + TextEditingController usernameController = TextEditingController(); + TextEditingController passwordController = TextEditingController(); + TextEditingController hostUrlController = TextEditingController(); + + Size size = MediaQuery.of(context).size; + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add User'), + content: Form( + child: Wrap( + children: [ + TextFormField( + controller: hostUrlController, + scrollPadding: EdgeInsets.all(size.height * .15), + textInputAction: TextInputAction.next, + keyboardType: TextInputType.url, + decoration: const InputDecoration( + labelText: 'Host URL', + hintText: 'https://portainer.example.com', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a host URL'; + } + return null; + }, + ), + SizedBox( + height: 10, + width: size.width, + ), + TextFormField( + controller: usernameController, + scrollPadding: EdgeInsets.all(size.height * .1), + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Username', + hintText: 'admin', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username'; + } + return null; + }, + ), + SizedBox( + height: 10, + width: size.width, + ), + TextFormField( + controller: passwordController, + scrollPadding: EdgeInsets.all(size.height * .12), + obscureText: true, + textInputAction: TextInputAction.done, + onEditingComplete: () { + _validateUser( + usernameController.text, + hostUrlController.text, + passwordController.text, + storage); + }, + decoration: const InputDecoration( + labelText: 'Password', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a password'; + } + return null; + }, + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Add'), + onPressed: () { + _validateUser(usernameController.text, hostUrlController.text, + passwordController.text, storage); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/pages/wrapper.dart b/lib/pages/wrapper.dart new file mode 100644 index 0000000..61d7e4a --- /dev/null +++ b/lib/pages/wrapper.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:portarius/models/portainer/user.dart'; +import 'package:portarius/pages/loading/loading.dart'; +import 'package:portarius/services/local_auth.dart'; +import 'package:portarius/services/storage.dart'; +import 'package:portarius/utils/settings.dart'; +import 'package:provider/provider.dart'; +import 'auth/authpage.dart'; +import 'home/home.dart'; + +/// [Wrapper] for switching between [AuthPage] and [HomePage]. +/// This is the main entry point of the app. +/// It uses [Provider] to provide the [User] model to the [HomePage]. +/// The [User] model is used to store the username, password and host URL of the user. +/// [Token] is also stored in this model, but can be null. +/// The token is used to access the Portainer API. +/// With no token, the user is redirected to the [AuthPage]. +/// With a token, the user is redirected to the [HomePage]. +class Wrapper extends StatefulWidget { + const Wrapper({Key? key}) : super(key: key); + + @override + State createState() => _WrapperState(); +} + +class _WrapperState extends State { + bool isAuthing = false; + @override + Widget build(BuildContext context) { + User user = Provider.of(context, listen: true); + StorageManager storage = Provider.of(context, listen: true); + SettingsManager settings = + Provider.of(context, listen: true); + bool isAuthenticated = settings.isAuthenticated; + + if (!storage.isInitialized && !settings.isInitialized) { + return const LoadingPage(); + } + + print('Biometrics: ${settings.biometricEnabled}'); + print('Authenticated: ${isAuthenticated}'); + print('User: ${user.token?.jwt}'); + + if (settings.biometricEnabled && !isAuthenticated) { + if (mounted && !isAuthing) { + _runBiometrics(settings); + } + return const LoadingPage(); + } + + if (user.token != null && settings.biometricEnabled && isAuthenticated) { + return const HomePage(); + } + + if (user.token != null && !settings.biometricEnabled) { + return const HomePage(); + } + + return const AuthPage(); + } + + _runBiometrics(SettingsManager settings) async { + isAuthing = true; + final result = await LocalAuthManager.authenticate(); + setState(() { + settings.isAuthenticated = result; + isAuthing = false; + }); + } +} diff --git a/lib/services/local_auth.dart b/lib/services/local_auth.dart new file mode 100644 index 0000000..b56fd07 --- /dev/null +++ b/lib/services/local_auth.dart @@ -0,0 +1,46 @@ +import 'package:local_auth/local_auth.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; + +class LocalAuthManager { + static final LocalAuthentication _instance = LocalAuthentication(); + + static Future hasBiometrics() { + return _instance.canCheckBiometrics; + } + + static Future deviceSupported() async { + try { + return await _instance.isDeviceSupported(); + } catch (e) { + return false; + } + } + + static Future authenticate() async { + final isAvailable = await hasBiometrics(); + if (!isAvailable) { + return false; + } + + try { + return await _instance.authenticate( + localizedReason: 'Authenticate to access Portarius', + options: const AuthenticationOptions( + stickyAuth: true, + useErrorDialogs: true, + ), + authMessages: [ + const AndroidAuthMessages( + signInTitle: 'Oops! Biometric authentication required!', + cancelButton: 'No thanks', + ), + const IOSAuthMessages( + cancelButton: 'No thanks', + ), + ]); + } catch (e) { + return false; + } + } +} diff --git a/lib/services/remote.dart b/lib/services/remote.dart new file mode 100644 index 0000000..d3ae896 --- /dev/null +++ b/lib/services/remote.dart @@ -0,0 +1,285 @@ +import 'dart:convert'; +import 'dart:ffi'; + +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'package:portarius/models/docker/detailed_container.dart'; +import 'package:portarius/models/portainer/endpoint.dart'; + +import '../models/docker/docker_container.dart'; +import '../models/portainer/token.dart'; +import '../models/portainer/user.dart'; + +class RemoteService { + /// Authenticates the user with the given credentials and returns a [Token]. + /// The [Token] will be used to access the Portainer API. + Future authPortainer( + String username, String password, String hostUrl) async { + http.Client client = http.Client(); + + // * If there is a '/' at the end of the hostUrl, remove it. + if (hostUrl.endsWith('/')) { + hostUrl = hostUrl.substring(0, hostUrl.length - 1); + } + + Uri uri = Uri.parse("$hostUrl/api/auth"); + + http.Response response = await client.post( + uri, + body: jsonEncode({ + "username": username, + "password": password, + }), + headers: { + "Content-Type": "application/json", + "Charset": "utf-8", + }, + ).timeout(const Duration(seconds: 10)); + + if (response.statusCode == 200) { + Token token = tokenFromJson(response.body); + Box box = await Hive.openBox('portainer'); + box.put('jwt', token.jwt); + + return token; + } else { + debugPrint('authPortainer: ${response.statusCode}'); + return null; + } + } + + /// Send logout request to portainer API. + /// Function will return true or false depending on if the request was successful + Future logoutPortainer(User user) async { + http.Client client = http.Client(); + + // * If there is a '/' at the end of the hostUrl, remove it. + if (user.hostUrl.endsWith('/')) { + user.hostUrl = user.hostUrl.substring(0, user.hostUrl.length - 1); + } + + Uri uri = Uri.parse("${user.hostUrl}/api/auth/logout"); + + http.Response response = await client.post( + uri, + headers: { + "Authorization": user.token?.getBearerToken() ?? '', + }, + ); + + if (response.statusCode == 204) { + return true; + } else { + debugPrint('logoutPortainer: ${response.statusCode}'); + return false; + } + } + + /// Check if currently stored [Token] is valid. + /// If the token is valid, it will return true. + /// If the token is not valid, it will return false. + /// If there is no token stored, it will return false. + /// If the token is stored, but it is not valid, it will remove the token from the local storage. + Future isTokenValid(User user) async { + if (user.token == null) { + return false; + } + + http.Client client = http.Client(); + Uri uri = Uri.parse("${user.hostUrl}/api/motd"); + http.Response response = await client.get( + uri, + headers: {"Authorization": user.token?.getBearerToken() ?? ''}, + ); + + if (response.statusCode == 200) { + return true; + } else { + debugPrint('isTokenValid: ${response.statusCode}'); + return false; + } + } + + /// Returns a [List] containing all containers. + /// The [Token] will be used to access the Portainer API. + Future> getDockerContainerList( + User user, Endpoint endpoint) async { + http.Client client = http.Client(); + + Uri uri = Uri.parse( + "${user.hostUrl}/api/endpoints/${endpoint.id}/docker/containers/json?all=true", + ); + + http.Response response = await client.get(uri, headers: { + "Authorization": user.token?.getBearerToken() ?? '', + }); + + if (response.statusCode == 200) { + return dockerContainerFromJson(response.body); + } else { + debugPrint('getDockerContainerList: ${response.statusCode}'); + throw Exception('Failed to load post'); + } + } + + /// Get a [DockerContainer] by its id. + /// The [Token] will be used to access the Portainer API. + /// The [DockerContainer] will be returned. + /// If the container does not exist, it will return null. + Future getDockerContainer( + User user, Endpoint endpoint, String containerId) async { + http.Client client = http.Client(); + + Uri uri = Uri.parse( + "${user.hostUrl}/api/endpoints/${endpoint.id}/docker/containers/$containerId/json"); + + http.Response response = await client.get(uri, headers: { + "Authorization": user.token?.getBearerToken() ?? '', + }); + + if (response.statusCode == 200) { + return detailedDockerContainerFromJson(response.body); + } else { + debugPrint('getDockerContainer: ${response.statusCode}'); + throw Exception('Failed to load post'); + } + } + + /// Restart a [DockerContainer] with the given id. + /// The [Token] will be used to access the Portainer API. + /// Returns true if the request was successful. + /// Returns false if the request was not successful. + Future restartDockerContainer( + User user, Endpoint endpoint, String containerId) async { + http.Client client = http.Client(); + + Uri uri = Uri.parse( + "${user.hostUrl}/api/endpoints/${endpoint.id}/docker/containers/$containerId/restart"); + + http.Response response = await client.post(uri, headers: { + "Authorization": user.token?.getBearerToken() ?? '', + }); + + if (response.statusCode == 204) { + return true; + } else { + debugPrint('restartDockerContainer: ${response.statusCode}'); + return false; + } + } + + /// Start a [DockerContainer] with the given id. + /// The [Token] will be used to access the Portainer API. + /// Returns true if the request was successful. + /// Returns false if the request was not successful. + Future startDockerContainer( + User user, Endpoint endpoint, String containerId) async { + http.Client client = http.Client(); + + Uri uri = Uri.parse( + "${user.hostUrl}/api/endpoints/${endpoint.id}/docker/containers/$containerId/start"); + + http.Response response = await client.post(uri, headers: { + "Authorization": user.token?.getBearerToken() ?? '', + }); + + if (response.statusCode == 204) { + return true; + } else { + debugPrint('startDockerContainer: ${response.statusCode}'); + return false; + } + } + + /// Stop a [DockerContainer] with the given id. + /// The [Token] will be used to access the Portainer API. + /// Returns true if the request was successful. + /// Returns false if the request was not successful. + Future stopDockerContainer( + User user, Endpoint endpoint, String containerId) async { + http.Client client = http.Client(); + + Uri uri = Uri.parse( + "${user.hostUrl}/api/endpoints/${endpoint.id}/docker/containers/$containerId/stop"); + + http.Response response = await client.post(uri, headers: { + "Authorization": user.token?.getBearerToken() ?? '', + }); + + if (response.statusCode == 204) { + return true; + } else { + debugPrint('stopDockerContainer: ${response.statusCode}'); + return false; + } + } + + /// Get stacks from portainer API. + /// The [Token] will be used to access the Portainer API. + Future getStacks(User user) async { + http.Client client = http.Client(); + + Uri uri = Uri.parse("${user.hostUrl}/api/stacks"); + + http.Response response = await client.get( + uri, + headers: {"Authorization": user.token?.getBearerToken() ?? ''}, + ); + + if (response.statusCode == 200) { + return response.body; + } else { + debugPrint('getStacks: ${response.statusCode}'); + throw Exception('Failed to load post'); + } + } + + /// Get endpoints from portainer API. + Future> getEndpoints(User user) async { + http.Client client = http.Client(); + + Uri uri = Uri.parse("${user.hostUrl}/api/endpoints"); + + http.Response response = await client.get( + uri, + headers: {"Authorization": user.token?.getBearerToken() ?? ''}, + ); + + if (response.statusCode == 200) { + return List.from( + json.decode(response.body).map((x) => Endpoint.fromJson(x))); + } else { + debugPrint('getEndpoints: ${response.statusCode}'); + throw Exception('Failed to load post'); + } + } + + Future> getContainerLogs( + User user, Endpoint endpoint, String containerId) async { + http.Client client = http.Client(); + + Uri uri = Uri.parse( + "${user.hostUrl}/api/endpoints/${endpoint.id}/docker/containers/$containerId/logs?tail=100&stdout=true&stderr=true"); + + http.Response response = await client.get(uri, headers: { + "Authorization": user.token?.getBearerToken() ?? '', + }); + + if (response.statusCode == 200) { + String splitStr = response.body.replaceRange(7, response.body.length, ''); + + List returnList = response.body.split(splitStr); + returnList.removeAt(0); + for (int i = 0; i < returnList.length; i++) { + returnList[i] = returnList[i].replaceRange(0, 1, ''); + } + + return returnList; + } else { + debugPrint('getLogs: ${response.statusCode}'); + debugPrint('getLogs: ${response.body}'); + throw Exception('Failed to load post'); + } + } +} diff --git a/lib/services/storage.dart b/lib/services/storage.dart new file mode 100644 index 0000000..e69d5c2 --- /dev/null +++ b/lib/services/storage.dart @@ -0,0 +1,225 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:portarius/services/remote.dart'; +import 'package:provider/provider.dart'; + +import '../models/portainer/token.dart'; +import '../models/portainer/user.dart'; + +class StorageManager extends ChangeNotifier { + /// Encryption key used to encrypt and decrypt the data. + final String encryptionKey; + + /// Stores the token for the API calls, username, password and host URL. + late Box? _storageBox; + bool isInitialized = false; + + /// PackkageInfo + late PackageInfo _packageInfo; + get packageInfo => _packageInfo; + + /// Saved user list + List _savedUsers = []; + get savedUsers => _savedUsers; + + StorageManager(this.encryptionKey) : super(); + + /// Initializes the [StorageManager]. + /// This method is called when the app is started. + /// It opens the local storage. + /// If the storage is empty, it will create a new one. + Future init(BuildContext context) async { + _storageBox = await Hive.openBox('portarius', + encryptionCipher: HiveAesCipher(base64Decode(encryptionKey))); + _packageInfo = await PackageInfo.fromPlatform(); + + if (_savedUsers.isEmpty) { + List tempUserStorage = + await _storageBox!.get('savedUsers', defaultValue: []); + _savedUsers = tempUserStorage + .map((user) => User( + username: user.username, + hostUrl: user.hostUrl, + password: user.password, + token: user.token)) + .toList(); + } + + // ignore: use_build_context_synchronously + await initUser(context); + + isInitialized = true; + notifyListeners(); + } + + /// Load user data + /// This method is called when the user is authenticated. + /// It loads the user data from the local storage. + /// It will also check if the token is valid. + /// If the token is not valid, it will try to auth the user again. + /// If the token is valid, it will return the user. + /// If the token is null, it will return null. + Future initUser(BuildContext context) async { + User providedUser = Provider.of(context, listen: false); + User? user = await _storageBox!.get('user'); + + if (user == null || user.hostUrl.isEmpty) { + return; + } + + if (!(await RemoteService().isTokenValid(user)) && + user.password.isNotEmpty && + user.username.isNotEmpty) { + Token? token = await RemoteService().authPortainer( + user.username, + user.password, + user.hostUrl, + ); + + if (token == null) { + return; + } + + if (!_savedUsers.contains(user)) { + _savedUsers.add(user); + } + user.setToken(token); + notifyListeners(); + } + + if (!_savedUsers.contains(user)) { + _savedUsers.add(user); + } + providedUser.setNewUser(user); + saveUser(user); + } + + /// Save user data + Future saveUser(User user) async { + /// If hostUrl ends with '/', remove it. + if (user.hostUrl.endsWith('/')) { + user.hostUrl = user.hostUrl.substring(0, user.hostUrl.length - 1); + } + + await _storageBox!.put('user', user); + } + + /// Clear user data + Future clearUser() async { + await _storageBox!.delete('user'); + } + + /// Save selected endpoint id + Future saveEndpointId(int id) async { + await _storageBox!.put('endpoint', id); + notifyListeners(); + } + + /// Load selected endpoint id + Future loadEndpointId() async { + return await _storageBox!.get('endpoint'); + } + + /// Clear selected endpoint id + Future clearEndpointId() async { + await _storageBox!.delete('endpoint'); + notifyListeners(); + } + + /// Save auto-refresh data + Future saveAutoRefresh(bool autoRefresh) async { + await _storageBox!.put('autoRefresh', autoRefresh); + notifyListeners(); + } + + /// Load auto-refresh data + Future loadAutoRefresh() async { + return await _storageBox!.get('autoRefresh') ?? true; + } + + /// Load auto-refresh interval data + Future loadAutoRefreshInterval() async { + return await _storageBox!.get('autoRefreshInterval') ?? 10; + } + + /// Save auto-refresh interval data + Future saveAutoRefreshInterval(int interval) async { + await _storageBox!.put('autoRefreshInterval', interval); + notifyListeners(); + } + + /// Load user list + Future loadUsers() async { + List tempUserStorage = + await _storageBox!.get('savedUsers', defaultValue: []); + _savedUsers = tempUserStorage + .map((user) => User( + username: user.username, + hostUrl: user.hostUrl, + password: user.password, + token: user.token)) + .toList(); + } + + /// Save user list + Future saveUsers(List userList) async { + await _storageBox!.put('savedUsers', userList); + notifyListeners(); + } + + /// Add to user list + Future addUserToList(User user) async { + if (!_savedUsers.contains(user)) { + _savedUsers.add(user); + } + + await saveUsers(_savedUsers); + } + + /// Remove from user list + Future removeUserFromList(User user) async { + _savedUsers.removeWhere((element) => + element.username == user.username && element.hostUrl == user.hostUrl); + await saveUsers(_savedUsers); + } + + /// Replace user in user list with new user + Future replaceUserInList(User oldUser, User newUser) async { + _savedUsers.removeWhere((element) => + element.username == oldUser.username && + element.hostUrl == oldUser.hostUrl); + _savedUsers.add(newUser); + await saveUsers(_savedUsers); + } + + /// Clear user list + Future clearUsers() async { + await _storageBox!.delete('savedUsers'); + notifyListeners(); + } + + /// Refresh user list + Future refreshUsers() async { + _savedUsers = []; + await loadUsers(); + notifyListeners(); + } + + /// Load biometric data + Future loadBiometric() async { + return await _storageBox!.get('biometric', defaultValue: false); + } + + /// Save biometric data + Future saveBiometric(bool biometric) async { + await _storageBox!.put('biometric', biometric); + notifyListeners(); + } + + /// Returns the [Box] + Box? get storageBox => _storageBox; +} diff --git a/lib/utils/settings.dart b/lib/utils/settings.dart new file mode 100644 index 0000000..c49f5c0 --- /dev/null +++ b/lib/utils/settings.dart @@ -0,0 +1,59 @@ +import 'package:flutter/cupertino.dart'; +import 'package:portarius/models/portainer/endpoint.dart'; +import 'package:portarius/services/storage.dart'; + +class SettingsManager extends ChangeNotifier { + /// This is the [Endpoint] that is used for the Portainer API. + /// If the [Endpoint] is null, it will be set to the first endpoint in the list. + int? _selectedEndpointId; + int? get selectedEndpointId => _selectedEndpointId; + + late bool _autoRefresh; + bool get autoRefresh => _autoRefresh; + + late int _autoRefreshInterval; + int get autoRefreshInterval => _autoRefreshInterval; + + bool _isInitialized = false; + bool get isInitialized => _isInitialized; + + bool _biometricEnabled = false; + bool get biometricEnabled => _biometricEnabled; + + bool _isAuthenticated = false; + bool get isAuthenticated => _isAuthenticated; + set isAuthenticated(bool value) { + _isAuthenticated = value; + notifyListeners(); + } + + /// This is the [Endpoint] that is used for the Portainer API. + /// Get the selected [Endpoint] + + /// This is the [Endpoint] that is used for the Portainer API. + /// Set the [Endpoint] + set selectedEndpointId(int? value) { + _selectedEndpointId = value; + notifyListeners(); + } + + /// Init the [SettingsManager] before running this. + /// This will load the [Endpoint] from the [StorageManager]. + Future init(StorageManager storage) async { + _selectedEndpointId = await storage.loadEndpointId(); + _autoRefresh = await storage.loadAutoRefresh(); + _autoRefreshInterval = await storage.loadAutoRefreshInterval(); + _biometricEnabled = await storage.loadBiometric(); + + _isInitialized = true; + notifyListeners(); + } + + Future refreshSettings(StorageManager storage) async { + _selectedEndpointId = await storage.loadEndpointId(); + _autoRefresh = await storage.loadAutoRefresh(); + _autoRefreshInterval = await storage.loadAutoRefreshInterval(); + _biometricEnabled = await storage.loadBiometric(); + notifyListeners(); + } +} diff --git a/lib/utils/style.dart b/lib/utils/style.dart new file mode 100644 index 0000000..d729676 --- /dev/null +++ b/lib/utils/style.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; + +class StyleManager extends ChangeNotifier { + /// Change theme in the local storage. + Future setTheme(ThemeMode theme) async { + var box = await Hive.openBox('style'); + await box.put('theme', theme == ThemeMode.dark ? 'dark' : 'light'); + notifyListeners(); + } + + Future getTheme() async { + var box = await Hive.openBox('style'); + + String? themeString = await box.get('theme'); + + if (themeString == null) { + return null; + } + + return themeString == 'light' ? ThemeMode.light : ThemeMode.dark; + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..cb6ea7e --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,831 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "41.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + build_runner: + dependency: "direct main" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flex_color_scheme: + dependency: "direct main" + description: + name: flex_color_scheme + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.3" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + flutter_speed_dial: + dependency: "direct main" + description: + name: flutter_speed_dial + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + hive: + dependency: "direct main" + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct main" + description: + name: hive_generator + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + local_auth: + dependency: "direct main" + description: + name: local_auth + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.6" + local_auth_ios: + dependency: transitive + description: + name: local_auth_ios + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.7" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + package_info_plus_linux: + dependency: transitive + description: + name: package_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_info_plus_macos: + dependency: transitive + description: + name: package_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + package_info_plus_web: + dependency: transitive + description: + name: package_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_info_plus_windows: + dependency: transitive + description: + name: package_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.16" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + toast: + dependency: "direct main" + description: + name: toast + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.5" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.17.1 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..9760597 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,90 @@ +name: portarius +description: Portainer companion app + +# 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 used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0 + +environment: + sdk: ">=2.17.1 <3.0.0" + +# 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: + build_runner: ^2.2.0 + flex_color_scheme: ^5.1.0 + flutter: + sdk: flutter + flutter_launcher_icons: ^0.9.3 + flutter_secure_storage: ^5.0.2 + flutter_speed_dial: ^6.0.0 + hive: ^2.2.2 + hive_generator: ^1.1.3 + http: ^0.13.4 + local_auth: ^2.1.0 + package_info_plus: ^1.4.2 + path_provider: ^2.0.11 + provider: ^6.0.3 + toast: ^0.3.0 + url_launcher: ^6.1.5 + +dev_dependencies: + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + +flutter_icons: + android: true + ios: true + image_path: "assets/icons/icon.png" + +# 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/icons/icon.png" + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + # 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/custom-fonts/#from-packages