diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml deleted file mode 100644 index dc1e9c7..0000000 --- a/.github/workflows/android.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: "Build APK" - -on: - push: - branches: ["main"] - tags: ["*"] - workflow_dispatch: - -jobs: - build-apk: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - submodules: recursive - - - name: Lix GHA Installer Action - uses: samueldr/lix-gha-installer-action@v2026-02-22 - with: - extra_nix_config: experimental-features = nix-command flakes flake-self-attrs - - - name: Decode keystore - run: echo "$KEYSTORE_CONTENT" | base64 --decode > keystore.jks - env: - KEYSTORE_CONTENT: ${{ secrets.KEYSTORE_CONTENT }} - - - name: Build app - run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build apk --release" - env: - KEYSTORE_PATH: ../../keystore.jks - KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} - - - name: Upload installer artifact - uses: actions/upload-artifact@v6 - with: - name: APK - path: build/app/outputs/flutter-apk/app-release.apk \ No newline at end of file diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml deleted file mode 100644 index d280bc6..0000000 --- a/.github/workflows/flatpak.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: "Build Flatpaks" - -on: - push: - branches: ["main"] - tags: ["*"] - workflow_dispatch: - -jobs: - build-flatpak: - strategy: - fail-fast: false - matrix: - include: - - arch: x86_64 - runner: ubuntu-latest - - arch: aarch64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Lix GHA Installer Action - uses: samueldr/lix-gha-installer-action@v2026-02-22 - with: - extra_nix_config: experimental-features = nix-command flakes flake-self-attrs - - - name: Build app - run: nix build .#flatpak - - - name: Upload installer artifact - uses: actions/upload-artifact@v6 - with: - name: flatpak-${{ matrix.arch }} - path: result/nexus.federated.nexus.flatpak \ No newline at end of file diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index d9c8dd5..0000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: "Build EXE" - -on: - push: - branches: ["main"] - tags: ["*"] - workflow_dispatch: - -jobs: - build-exe: - runs-on: windows-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - submodules: recursive - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.44.4 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version-file: gomuks/go.mod - - - name: Setup MSYS2 - uses: msys2/setup-msys2@v2 - with: - msystem: MINGW64 - install: >- - mingw-w64-x86_64-gcc - - - name: Go build - run: | - cd gomuks/pkg/ffi - go build -tags goolm,sqlite_fts5 -o ../../../libgomuks.dll -buildmode=c-shared - - - name: Build with Flutter - run: | - flutter pub get - dart scripts/generate.dart - flutter pub run build_runner build - flutter build windows --release - - - name: Copy MinGW runtime DLLs - shell: msys2 {0} - run: | - cp /mingw64/bin/libgcc_s_seh-1.dll build/windows/x64/runner/Release/ - cp /mingw64/bin/libwinpthread-1.dll build/windows/x64/runner/Release/ - cp /mingw64/bin/libstdc++-6.dll build/windows/x64/runner/Release/ - - - name: Install Inno Setup - run: choco install innosetup -y - - - name: Build Inno Setup installer - run: iscc windows/installer.iss - - - name: Upload installer artifact - uses: actions/upload-artifact@v6 - with: - name: windows-installer - path: windows/dist/Nexus-Setup.exe \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2bec583..dfaf03d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,4 @@ key.properties *.freezed.dart # Devel Password -password.txt - -# Nix -/result \ No newline at end of file +password.txt \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 145276a..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "gomuks"] - path = gomuks - url = https://github.com/gomuks/gomuks - branch = main diff --git a/.metadata b/.metadata index 1be1841..6651909 100644 --- a/.metadata +++ b/.metadata @@ -15,7 +15,7 @@ migration: - platform: root create_revision: nixpkgs000000000000000000000000000000000 base_revision: nixpkgs000000000000000000000000000000000 - - platform: macos + - platform: windows create_revision: nixpkgs000000000000000000000000000000000 base_revision: nixpkgs000000000000000000000000000000000 diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ff533e..30f4254 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,16 +1,3 @@ { - "cSpell.words": [ - "Appbar", - "Displayname", - "fluttertagger", - "Gomuks", - "Homeserver", - "Linkified", - "localpart", - "msgtype", - "muks", - "prefs", - "unban", - "unredact" - ] + "cSpell.words": ["Appbar", "Displayname", "prefs"] } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index fb69a56..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,70 +0,0 @@ -# Development Documentation - -## Build instructions - -Build instructions can be found in [README.md](./README.md#build-it-yourself). - -## Updating Gomuks - -You can run the following command to update the Gomuks submodule: - -```sh -git submodule update --remote -``` - -## Code Style - -See [Effective Dart: Style](https://dart.dev/effective-dart/style) for general rules. There are some extra rules detailed below: - -### Controllers and Helpers ([Riverpod](https://pub.dev/packages/riverpod)) - -Controllers live in `lib/controllers/` and provide a source that exposes data and logic via Riverpod providers, allowing other parts of the code to watch state changes with ref.watch (`ref.watch(MyController.provider)`), access the current value with ref.read (`ref.read(MyController.provider)`), and run helper methods on those classes using the notifier: - -```dart -ref.watch(MyController.provider.notifier).helperMethod() -``` - -We use an object oriented style for controllers, where `provider` is a static member on the controller class. E.g. - -```dart -class MyController extends AsyncNotifier { - final SomeInputType input; - MyController(this.input); - - @override - Future build() async { - return input.foo; - } - - static final provider = - AsyncNotifierProvider.family( - AuthorController.new, - ); -} -``` - -Providers which are not controllers, e.g. they expose no data, only methods, should instead live in `lib/helpers/`. For an example, see `lib/helpers/launch_helper.dart`. Other, non-provider helpers, like extensions or helper methods can also go in `lib/helpers/`. - -### Don't use StatefulWidgets ([Flutter Hooks](https://pub.dev/packages/flutter_hooks)) - -This project uses Flutter Hooks to help with boilerplate that StatefulWidgets create. Instead of using a StatefulWidget, we just use hooks like `useState` or `useEffect` in the build method of a `HookWidget`, which is a drop in replacement for `StatelessWidget`. If you need both a `WidgetRef` to watch providers, and access to hooks, use `HookConsumerWidget`. - -### Models ([Freezed](https://pub.dev/packages/freezed)) - -We use Freezed for our models to avoid boilerplate and enforce an immutable style of state and data modeling throughout the code. See their documentation for more info, or see our existing models in `lib/models/`. - -### Immutable Data Collections ([Fast Immutable Collections](https://pub.dev/packages/fast_immutable_collections)) - -When possible, use immutable collections instead of the mutable equivalent. For example, use `IMap` over `Map`, `IList` over `List`, `ISet` over `Set`. This matches the immutable style of Riverpod and Freezed. - -### Don't create globals - -When possible, we prefer not to create global variables or methods. You can usually replace a global variable with a Riverpod controller, and a global method with an extension method. - -## LLM/AI Assisted Contributions - -LLM generated code is NOT allowed. All contributions should be written by humans. Using LLMs for interacting with others, e.g. for Comments, PRs, etc, is also not allowed. - -## Code of Conduct - -All contributions must follow the [Federated Nexus Code of Conduct](https://federated.nexus/code/). diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 496acdb..0000000 --- a/LICENSE +++ /dev/null @@ -1,675 +0,0 @@ -# GNU GENERAL PUBLIC LICENSE - -Version 3, 29 June 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. - - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - -## Preamble - -The GNU General Public License is a free, copyleft license for -software and other kinds of works. - -The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom -to share and change all versions of a program--to make sure it remains -free software for all its users. We, the Free Software Foundation, use -the GNU General Public License for most of our software; it applies -also to any other work released this way by its authors. You can apply -it to your programs, too. - -When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you -have certain responsibilities if you distribute copies of the -software, or if you modify it: responsibilities to respect the freedom -of others. - -For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - -Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - -Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the -manufacturer can do so. This is fundamentally incompatible with the -aim of protecting users' freedom to change the software. The -systematic pattern of such abuse occurs in the area of products for -individuals to use, which is precisely where it is most unacceptable. -Therefore, we have designed this version of the GPL to prohibit the -practice for those products. If such problems arise substantially in -other domains, we stand ready to extend this provision to those -domains in future versions of the GPL, as needed to protect the -freedom of users. - -Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish -to avoid the special danger that patents applied to a free program -could make it effectively proprietary. To prevent this, the GPL -assures that patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and -modification follow. - -## TERMS AND CONDITIONS - -### 0. Definitions. - -"This License" refers to version 3 of the GNU General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds -of works, such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of -an exact copy. The resulting work is called a "modified version" of -the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based -on the Program. - -To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user -through a computer network, with no transfer of a copy, is not -conveying. - -An interactive user interface displays "Appropriate Legal Notices" to -the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - -### 1. Source Code. - -The "source code" for a work means the preferred form of the work for -making modifications to it. "Object code" means any non-source form of -a work. - -A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can -regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same -work. - -### 2. Basic Permissions. - -All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, -without conditions so long as your license otherwise remains in force. -You may convey covered works to others for the sole purpose of having -them make modifications exclusively for you, or provide you with -facilities for running those works, provided that you comply with the -terms of this License in conveying all material for which you do not -control copyright. Those thus making or running the covered works for -you must do so exclusively on your behalf, under your direction and -control, on terms that prohibit them from making any copies of your -copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the -conditions stated below. Sublicensing is not allowed; section 10 makes -it unnecessary. - -### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - -When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such -circumvention is effected by exercising rights under this License with -respect to the covered work, and you disclaim any intention to limit -operation or modification of the work as a means of enforcing, against -the work's users, your or third parties' legal rights to forbid -circumvention of technological measures. - -### 4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - -### 5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these -conditions: - -- a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. -- b) The work must carry prominent notices stating that it is - released under this License and any conditions added under - section 7. This requirement modifies the requirement in section 4 - to "keep intact all notices". -- c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. -- d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - -A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - -### 6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms of -sections 4 and 5, provided that you also convey the machine-readable -Corresponding Source under the terms of this License, in one of these -ways: - -- a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. -- b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the Corresponding - Source from a network server at no charge. -- c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. -- d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. -- e) Convey the object code using peer-to-peer transmission, - provided you inform other peers where the object code and - Corresponding Source of the work are being offered to the general - public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, -family, or household purposes, or (2) anything designed or sold for -incorporation into a dwelling. In determining whether a product is a -consumer product, doubtful cases shall be resolved in favor of -coverage. For a particular product received by a particular user, -"normally used" refers to a typical or common use of that class of -product, regardless of the status of the particular user or of the way -in which the particular user actually uses, or expects or is expected -to use, the product. A product is a consumer product regardless of -whether the product has substantial commercial, industrial or -non-consumer uses, unless such uses represent the only significant -mode of use of the product. - -"Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to -install and execute modified versions of a covered work in that User -Product from a modified version of its Corresponding Source. The -information must suffice to ensure that the continued functioning of -the modified object code is in no case prevented or interfered with -solely because modification has been made. - -If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - -The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or -updates for a work that has been modified or installed by the -recipient, or for the User Product in which it has been modified or -installed. Access to a network may be denied when the modification -itself materially and adversely affects the operation of the network -or violates the rules and protocols for communication across the -network. - -Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - -### 7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders -of that material) supplement the terms of this License with terms: - -- a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or -- b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or -- c) Prohibiting misrepresentation of the origin of that material, - or requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or -- d) Limiting the use for publicity purposes of names of licensors - or authors of the material; or -- e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or -- f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions - of it) with contractual assumptions of liability to the recipient, - for any liability that these contractual assumptions directly - impose on those licensors and authors. - -All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; the -above requirements apply either way. - -### 8. Termination. - -You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - -However, if you cease all violation of this License, then your license -from a particular copyright holder is reinstated (a) provisionally, -unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder -fails to notify you of the violation by some reasonable means prior to -60 days after the cessation. - -Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - -Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - -### 9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or run -a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - -### 10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - -### 11. Patents. - -A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - -A contributor's "essential patent claims" are all patent claims owned -or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - -If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - -A patent license is "discriminatory" if it does not include within the -scope of its coverage, prohibits the exercise of, or is conditioned on -the non-exercise of one or more of the rights that are specifically -granted under this License. You may not convey a covered work if you -are a party to an arrangement with a third party that is in the -business of distributing software, under which you make payment to the -third party based on the extent of your activity of conveying the -work, and under which the third party grants, to any of the parties -who would receive the covered work from you, a discriminatory patent -license (a) in connection with copies of the covered work conveyed by -you (or copies made from those copies), or (b) primarily for and in -connection with specific products or compilations that contain the -covered work, unless you entered into that arrangement, or that patent -license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - -### 12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under -this License and any other pertinent obligations, then as a -consequence you may not convey it at all. For example, if you agree to -terms that obligate you to collect a royalty for further conveying -from those to whom you convey the Program, the only way you could -satisfy both those terms and this License would be to refrain entirely -from conveying the Program. - -### 13. Use with the GNU Affero General Public License. - -Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - -### 14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions -of the GNU General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in -detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies that a certain numbered version of the GNU General Public -License "or any later version" applies to it, you have the option of -following the terms and conditions either of that numbered version or -of any later version published by the Free Software Foundation. If the -Program does not specify a version number of the GNU General Public -License, you may choose any version ever published by the Free -Software Foundation. - -If the Program specifies that a proxy can decide which future versions -of the GNU General Public License can be used, that proxy's public -statement of acceptance of a version permanently authorizes you to -choose that version for the Program. - -Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - -### 15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT -WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND -PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE -DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR -CORRECTION. - -### 16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR -CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT -NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR -LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM -TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER -PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -### 17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -## How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these -terms. - -To do so, attach the following notices to the program. It is safest to -attach them to the start of each source file to most effectively state -the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper -mail. - -If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands \`show w' and \`show c' should show the -appropriate parts of the General Public License. Of course, your -program's commands might be different; for a GUI interface, you would -use an "about box". - -You should also get your employer (if you work as a programmer) or -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. For more information on this, and how to apply and follow -the GNU GPL, see . - -The GNU General Public License does not permit incorporating your -program into proprietary programs. If your program is a subroutine -library, you may consider it more useful to permit linking proprietary -applications with the library. If this is what you want to do, use the -GNU Lesser General Public License instead of this License. But first, -please read . diff --git a/README.md b/README.md index d706c9c..670e20e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Nexus Client > [!WARNING] -> Nexus Client is still in development, and doesn't support everything needed for daily use. +> Nexus Client is still heavily in development, and is not ready for use! ## Description -A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. +A simple and user-friendly Matrix client made with Flutter and the Matrix Dart SDK. ## Screenshots @@ -15,196 +15,107 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. ## Progress -- [ ] Platform Support - - [x] Linux - - [x] Windows - - [x] Android - - [ ] MacOS ([#50](https://git.federated.nexus/Nexus/nexus/issues/50)) - - [ ] iOS - - [ ] Web (may not be possible) -- [x] Login (via OAuth) -- [x] Rooms / Spaces - - [x] Displaying and choosing - - [x] Reading, showing unread - - [x] Mark as read button on rooms and spaces - - [ ] Searching - - [ ] Creating (Rooms, Spaces, and DMs) - - [x] Joining - - [x] Parse vias - - [x] Using a text/uri/link - - [x] Plain text - - [x] `matrix:` Uri - - [x] Matrix.to link - - [ ] From space - - [ ] From directory - - [x] Leaving - - [x] Subspaces -- [x] Messages - - [x] Encryption - - [x] Restoring crypto identity from a recovery passphrase/key - - [x] Sending - - [x] Plain text - - [x] HTML/Markdown - - [x] Replies - - [x] Choose ping on/off - - [x] Per message profiles - - [x] Attachments - - [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391) - - [x] Mentions - - [x] Users - - [x] Rooms - - [ ] Inline emoji picker (Putting this here since it'll be implemented the same way as mentions) - - [ ] Custom emojis/stickers - - [ ] GIFs using Gomuks' GIF proxies - - [x] Receiving - - [x] Plain text - - [x] Per message profiles - - [x] HTML - - [x] URL Previews - - [x] Replies - - [x] Viewing - - [ ] Jump to original message - - [x] In loaded timeline - - [ ] Out of loaded timeline - - [x] Edits - - [x] Attachments - - [x] Unencrypted - - [ ] Encrypted - - [x] Blurhashing - - [ ] Downloading attachments - - [x] Opening attachments in their own view - - [ ] Polls - - [x] Mentions - - [x] Users - - [x] Clickable - - [x] Rooms - - [ ] Clickable - - [x] Matrix URIs - - [x] Matrix.to links - - [x] Events - - [ ] Render more nicely - - [ ] Clickable - - [x] Custom emojis/stickers - - [x] History loading - - [x] Editing - - [x] Deleting -- [x] Reactions -- [x] Pins - - [x] Displaying - - [x] Pinning/Unpinning -- [ ] Threads -- [x] Profile popouts - - [x] Working actions -- [x] Copy link to: - - [x] Room - - [x] Space - - [x] Message -- [ ] Reporting - - [x] Events - - [ ] Rooms -- [x] Member list - - [x] Sort by power level - - [ ] Colors based off of power level -- [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35)) -- [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195) -- [ ] Invites -- [x] Settings +- [ ] Platform Support + - [x] Linux + - [x] Windows (untested, if you are interested in helping to test, open an issue) + - [ ] MacOS + - [ ] Android + - [ ] iOS + - [ ] Web (may not be possible) +- [x] Login + - [x] Username / password auth + - [ ] OAuth / OIDC +- [x] Rooms / Spaces + - [x] Displaying and choosing + - [x] Reading, showing unread + - [ ] Mark as read button on rooms and spaces + - [ ] Searching + - [ ] Creating (Rooms, Spaces, and DMs) + - [ ] Joining + - [ ] Using alias + - [ ] From space + - [ ] Exploring + - [x] Leaving + - [x] Subspaces +- [x] Messages + - [x] Sending + - [x] Plain text + - [x] HTML/Markdown + - [x] Replies + - [ ] Attachments + - [x] Mentions + - [x] Users + - [x] Rooms + - [ ] Custom emojis/stickers + - [ ] GIFs, maybe through Tenor or something + - [ ] Encrypted messages + - [x] Recieving + - [x] Plain text + - [x] HTML + - [x] Replies + - [x] Viewing + - [ ] Jump to original message + - [x] Edits + - [x] Attachments + - [ ] Downloading attachments + - [ ] Opening attachments in their own view + - [x] Mentions + - [x] Users + - [x] Rooms + - [ ] Plain text + - [x] Matrix URIs + - [x] Matrix.to links + - [x] Custom emojis/stickers + - [ ] Encrypted messages + - [x] History loading + - [x] Backwards + - [ ] Forwards + - [ ] Editing + - [x] Deleting +- [ ] Reactions: Waiting on https://github.com/flyerhq/flutter_chat_ui/pull/838 +- [ ] Pins + - [ ] Displaying + - [ ] Creating +- [ ] Threads +- [ ] Profile popouts +- [x] Copy link to [room, space] +- [x] Reporting +- [ ] Notifications using UnifiedPush +- [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195) +- [ ] Invites + - [ ] Viewing / accepting + - [ ] Spam filtering +- [ ] Settings + - [ ] Light/Dark mode + - [ ] Dynamic Theming + - [ ] Devices + - [ ] Viewing devices + - [ ] Verifying devices + - [ ] URL preview: Server / Client / None + - [ ] Account changes + - [ ] Display name + - [ ] Profile picture + - [ ] Timezone + - [ ] Pronouns + - [ ] Password + - [ ] About + - [x] Log Out -## Try it out +## Development -If you want to try out Nexus, grab one of the following artifacts from CI: +Fork and clone the project, then: -- [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip) -- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip) -- Flatpak - - [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip) - - [x86_64/AMD64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-x86_64.zip) - -## Build it yourself - -### Prerequisites - -#### Linux - -- With Nix: Either use direnv and `direnv allow`, or `nix flake develop` -- Without Nix: Install Flutter, Go, Git, Libclang, and Glibc. Do not use any Snap packages, they cause various compilation issues. - -#### Windows - -You will need: - -- Flutter -- Android SDK + NDK -- Git -- Go -- Visual Studio 2022 (Desktop development with C++) -- [MSYS2/MinGW-w64 GCC](https://www.msys2.org/) (for CGO) -- [LLVM/Clang + libclang](https://clang.llvm.org/get_started.html) (for `ffigen`) - -On Windows, make sure these are available in your shell `PATH`: - -- `C:\msys64\ucrt64\bin` (or your MinGW bin path containing `x86_64-w64-mingw32-gcc.exe`) -- `C:\Program Files\LLVM\bin` (contains `clang.exe` and `libclang.dll`) - -For `dart scripts/generate.dart`, you may also need: - -```powershell -$env:CPATH = "C:\msys64\ucrt64\include" -``` - -#### MacOS - -Similar prerequisites apply (Flutter, Git, Go, C toolchain, LLVM/libclang), but exact setup has not been fully documented yet. - -### Clone repo - -First, clone and open the repo: - -```sh -git clone --recurse-submodules https://git.federated.nexus/Nexus/nexus -cd nexus -``` - -### Set up Flutter - -Get dependencies: - -```sh -flutter pub get -``` - -Generate Gomuks bindings: - -```sh -dart scripts/generate.dart -``` - -> [!NOTE] -> If you are having issues with `stddef.h` not being found, try setting CPATH manually: -> -> ```sh -> export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" -> ``` +- With Nix: Either use direnv, or `nix flake develop` +- Without Nix: Install Flutter, Rust, the libsecret dev package for your distro (must be in `PKG_CONFIG_PATH`), and sqlite (must be in `LD_LIBRARY_PATH`). Build generated files, and watch for new changes: ```sh -flutter pub run build_runner watch +flutter pub run build_runner watch --delete-conflicting-outputs ``` -Run the app: - -```sh -flutter run -``` - -Development instructions can be found in [DEVELOPMENT.md](./DEVELOPMENT.md). +Run `flutter run` to run the app. ## Community -Join the [Nexus Client Matrix Room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client. - -# Credits - -Thank you Hylke Bons (https://planetpeanut.studio) for making the amazing icon for Nexus! -Thank you Tulir Asokan for making [Gomuks](https://github.com/gomuks/gomuks), and helping us integrate it into Nexus! +Come chat in the [Federated Nexus Community](https://matrix.to/#/#space:federated.nexus) for questions or help with developing or using Nexus Client. diff --git a/analysis_options.yaml b/analysis_options.yaml index a8b1078..c2aaaa0 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,7 +1,6 @@ analyzer: errors: invalid_annotation_target: ignore - avoid_print: ignore exclude: - "build/**" - "**/*.g.dart" diff --git a/android/app/build.gradle b/android/app/build.gradle index 2e7fb67..ce5f465 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -30,7 +30,7 @@ if (keystorePropertiesFile.exists()) { } android { - namespace = "nexus.federated.nexus" + namespace = "nexus.federated.Nexus" ndkVersion = flutter.ndkVersion compileSdk = 34 @@ -39,12 +39,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } - defaultConfig { - applicationId = "nexus.federated.nexus" + applicationId = "nexus.federated.Nexus" minSdk = 29 targetSdkVersion flutter.targetSdkVersion versionCode = flutterVersionCode.toInteger() @@ -54,8 +50,7 @@ android { signingConfigs { release { keyAlias "key" - def storePath = keystoreProperties['path'] ?: System.getenv("KEYSTORE_PATH") - storeFile storePath ? file(storePath) : null + storeFile keystoreProperties['path'] ? file(keystoreProperties['path']) : file(System.getenv("KEYSTORE_PATH")) keyPassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD") storePassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD") } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a0a0bcc..1c369c9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ android:label="Nexus" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" - android:roundIcon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/nexus_round" android:allowBackup="false" android:fullBackupContent="false"> - - - - - - - - - + - - - diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 2721cd8..80efd04 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 4e9192d..b02e5ef 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index f18b718..54aed69 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 2f6a559..eb2221d 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 0118074..c5ac464 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/gradle.properties b/android/gradle.properties index 1551eb0..3b5b324 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,7 +1,3 @@ org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true -# This builtInKotlin flag was added automatically by Flutter migrator -android.builtInKotlin=false -# This newDsl flag was added automatically by Flutter migrator -android.newDsl=false diff --git a/assets/background.png b/assets/background.png deleted file mode 100644 index 9f1d8e7..0000000 Binary files a/assets/background.png and /dev/null differ diff --git a/assets/background.svg b/assets/background.svg deleted file mode 100644 index 749e03a..0000000 --- a/assets/background.svg +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/fonts/NotoColorEmoji.ttf b/assets/fonts/NotoColorEmoji.ttf deleted file mode 100644 index b652015..0000000 Binary files a/assets/fonts/NotoColorEmoji.ttf and /dev/null differ diff --git a/assets/fonts/Roboto.ttf b/assets/fonts/Roboto.ttf deleted file mode 100644 index 01656a3..0000000 Binary files a/assets/fonts/Roboto.ttf and /dev/null differ diff --git a/assets/foreground.png b/assets/foreground.png index a98eb11..4249989 100644 Binary files a/assets/foreground.png and b/assets/foreground.png differ diff --git a/assets/foreground.svg b/assets/foreground.svg index 9aad561..4f2f2b2 100644 --- a/assets/foreground.svg +++ b/assets/foreground.svg @@ -1,19 +1,20 @@ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + inkscape:current-layer="layer1" /> diff --git a/assets/icon.png b/assets/icon.png index d6d4906..04b75cb 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/icon.svg b/assets/icon.svg index b36fa26..0effd9a 100644 --- a/assets/icon.svg +++ b/assets/icon.svg @@ -1,22 +1,21 @@ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="stop11" /> diff --git a/assets/mobile.png b/assets/mobile.png deleted file mode 100644 index 6b1b81c..0000000 Binary files a/assets/mobile.png and /dev/null differ diff --git a/assets/mobile.svg b/assets/mobile.svg deleted file mode 100644 index 7ca0a7d..0000000 --- a/assets/mobile.svg +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/monochrome.png b/assets/monochrome.png deleted file mode 100644 index 941c706..0000000 Binary files a/assets/monochrome.png and /dev/null differ diff --git a/assets/monochrome.svg b/assets/monochrome.svg deleted file mode 100644 index a86f36e..0000000 --- a/assets/monochrome.svg +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/screenshotDark.png b/assets/screenshotDark.png index 322a64e..dec983f 100644 Binary files a/assets/screenshotDark.png and b/assets/screenshotDark.png differ diff --git a/assets/screenshotLight.png b/assets/screenshotLight.png index 8772bcf..003f6b4 100644 Binary files a/assets/screenshotLight.png and b/assets/screenshotLight.png differ diff --git a/build.yaml b/build.yaml deleted file mode 100644 index 5d6aeda..0000000 --- a/build.yaml +++ /dev/null @@ -1,6 +0,0 @@ -targets: - $default: - builders: - json_serializable: - options: - field_rename: snake diff --git a/flake.lock b/flake.lock index 41cfb9d..b627cd3 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1782949081, - "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "lastModified": 1759362264, + "narHash": "sha256-wfG0S7pltlYyZTM+qqlhJ7GMw2fTF4mLKCIVhLii/4M=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "rev": "758cf7296bee11f1706a574c77d072b8a7baa881", "type": "github" }, "original": { @@ -18,54 +18,17 @@ "type": "github" } }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nix2flatpak": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1774860670, - "narHash": "sha256-YjJkQrvxrErXtfDi3obUn6rNmkA+CIAZ3f5NgL5xuYE=", - "owner": "neobrain", - "repo": "nix2flatpak", - "rev": "61d68e21e3fbc2d57590051f48736bea271f4aba", - "type": "github" - }, - "original": { - "owner": "neobrain", - "repo": "nix2flatpak", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1773389992, - "narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=", - "owner": "NixOS", + "lastModified": 1759381078, + "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", + "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" @@ -73,11 +36,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1782614948, - "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=", + "lastModified": 1754788789, + "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c", + "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", "type": "github" }, "original": { @@ -86,42 +49,10 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1784497964, - "narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { "flake-parts": "flake-parts", - "nix2flatpak": "nix2flatpak", - "nixpkgs": "nixpkgs_2" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 2b13a08..027db59 100644 --- a/flake.nix +++ b/flake.nix @@ -2,10 +2,8 @@ description = "Nexus Flutter Flake"; inputs = { - self.submodules = true; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; - nix2flatpak.url = "github:neobrain/nix2flatpak"; }; outputs = @@ -25,7 +23,6 @@ perSystem = { - lib, pkgs, system, ... @@ -40,38 +37,35 @@ }; }; - packages = + devShells.default = let - default = pkgs.callPackage ./linux/nix/pkg { - src = self; - }; + # android = pkgs.callPackage ./nix/android.nix { }; in - { - inherit default; + pkgs.mkShell { + packages = with pkgs; [ + # jdk17 + cargo + (flutter.override { extraPkgConfigPackages = [ pkgs.libsecret ]; }) - flatpak = inputs.nix2flatpak.lib.${system}.mkFlatpak { - appName = "Nexus"; - developer = "QuadRadical"; - appId = "nexus.federated.nexus"; - package = default; - runtime = "org.gnome.Platform/49"; - permissions = { - share = [ "network" ]; - sockets = [ - "pulseaudio" - "fallback-x11" - "wayland" - ]; - devices = [ "dri" ]; - }; - }; + # android.platform-tools + (pkgs.writeShellScriptBin "rustup" (builtins.readFile ./nix/fake-rustup.sh)) + ]; - gomuks = pkgs.callPackage ./linux/nix/pkg/gomuks.nix { - src = self; + env = rec { + LD_LIBRARY_PATH = "${ + pkgs.lib.makeLibraryPath ([ + pkgs.sqlite + ]) + }:./build/linux/x64/debug/plugins/flutter_vodozemac"; + + # ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk"; + # ANDROID_SDK_ROOT = ANDROID_HOME; + # JAVA_HOME = pkgs.jdk17; + + # TOOLS = "${ANDROID_HOME}/build-tools/${"36.0.0"}"; + # GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${TOOLS}/aapt2"; }; }; - - devShells.default = pkgs.callPackage ./linux/nix/devshell.nix { }; }; }; } diff --git a/gomuks b/gomuks deleted file mode 160000 index c06eb1c..0000000 --- a/gomuks +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c06eb1cbda2d49ff199a351e2d55df6ff7ab602b diff --git a/hook/build.dart b/hook/build.dart deleted file mode 100644 index 625201d..0000000 --- a/hook/build.dart +++ /dev/null @@ -1,143 +0,0 @@ -import "dart:io"; -import "package:collection/collection.dart"; -import "package:hooks/hooks.dart"; -import "package:code_assets/code_assets.dart"; -import "package:nexus/helpers/extensions/get_xcode_sdk.dart"; -import "package:path/path.dart"; - -Future main(List args) => build(args, (input, output) async { - if (!input.config.buildCodeAssets) return; - final codeConfig = input.config.code; - final targetOS = codeConfig.targetOS; - final targetArch = codeConfig.targetArchitecture; - - String libFileName; - Map extraEnv = {}; - switch (targetOS) { - case OS.linux: - libFileName = "libgomuks.so"; - break; - case OS.macOS: - libFileName = "libgomuks.dylib"; - extraEnv = { - "SDKROOT": await getXCodeSDK(), - "CGO_ENABLED": "1", - "GOARCH": switch (targetArch) { - Architecture.arm64 => "arm64", - Architecture.x64 => "amd64", - _ => throw UnsupportedError( - "Unsupported macOS architecture: $targetArch", - ), - }, - }; - break; - case OS.windows: - libFileName = "libgomuks.dll"; - break; - case OS.android: - libFileName = "libgomuks.so"; - - final targetNdkApi = codeConfig.android.targetNdkApi; - - Future findNdkFromSdk() async { - final androidHome = - Platform.environment["ANDROID_HOME"] ?? - Platform.environment["ANDROID_SDK_ROOT"]; - if (androidHome == null) return null; - - final ndkDir = Directory(join(androidHome, "ndk")); - if (!await ndkDir.exists()) return null; - - final versions = await ndkDir.list().toList(); - return versions.sortedBy((file) => file.path).lastOrNull?.path; - } - - final ndkHome = - Platform.environment["ANDROID_NDK_HOME"] ?? - Platform.environment["ANDROID_NDK_ROOT"] ?? - Platform.environment["NDK_HOME"] ?? - await findNdkFromSdk(); - - if (ndkHome == null) { - throw Exception( - "Could not find Android NDK. Set ANDROID_NDK_HOME or install via sdkmanager.", - ); - } - - // TODO: Someone please give me a way to detect host architecture so I can change this - final hostTag = - Platform.environment["ANDROID_HOST_TAG"] ?? - "${Platform.operatingSystem}-x86_64"; - final ccTriple = switch (targetArch) { - Architecture.arm64 => "aarch64-linux-android", - Architecture.arm => "armv7a-linux-androideabi", - Architecture.x64 => "x86_64-linux-android", - Architecture.ia32 => "i686-linux-android", - _ => throw UnsupportedError( - "Unsupported Android architecture: $targetArch", - ), - }; - final cc = - "$ndkHome/toolchains/llvm/prebuilt/$hostTag/bin/$ccTriple$targetNdkApi-clang"; - - extraEnv = {"GOOS": "android", "CC": cc}; - break; - default: - throw UnsupportedError("Unsupported OS: $targetOS"); - } - - var libFile = input.packageRoot.resolve(libFileName); - final gomuksBuildDir = input.packageRoot.resolve("gomuks/"); - - if (!(await File.fromUri(libFile).exists())) { - final buildDir = input.packageRoot.resolve("build/"); - libFile = buildDir.resolve("${targetArch.name}/$libFileName"); - - final tags = [ - "sqlite_fts5", - "goolm", - // goheif/dav1d is not supported on Android, would need to be fixed upstream - if (targetOS == OS.android) "noheic", - ].join(","); - print( - "Building Gomuks shared library $libFileName (${targetOS.name}/${targetArch.name}) to ${libFile.path}...", - ); - final result = await Process.run( - "go", - ["build", "-tags", tags, "-o", libFile.path, "-buildmode=c-shared"], - workingDirectory: gomuksBuildDir.resolve("pkg/ffi/").toFilePath(), - environment: { - "CGO_ENABLED": "1", - "GOARCH": switch (targetArch) { - Architecture.arm64 => "arm64", - Architecture.arm => "arm", - Architecture.x64 => "amd64", - Architecture.ia32 => "386", - _ => throw UnsupportedError("Unsupported architecture: $targetArch"), - }, - ...extraEnv, - }, - ); - - if (result.exitCode != 0) { - throw Exception( - "Failed to build Gomuks shared library\n${result.stderr}", - ); - } - } - - final generatedFile = "src/third_party/gomuks.g.dart"; - print("Adding $libFileName as asset..."); - output - ..assets.code.add( - CodeAsset( - package: "nexus", - name: generatedFile, - linkMode: DynamicLoadingBundled(), - file: libFile, - ), - ) - ..dependencies.add(libFile) - ..dependencies.add(gomuksBuildDir); - print("Done!"); -}); diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6f637da --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + nexus.federated.Nexus + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 15.5 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..9c6fa1f --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,55 @@ +platform :ios, 16 +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] == '8.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + end + end + flutter_additional_ios_build_settings target + end + + ################ Awesome Notifications pod modification 1 ################### + awesome_pod_file = File.expand_path(File.join('plugins', 'awesome_notifications', 'ios', 'Scripts', 'AwesomePodFile'), '.symlinks') + require awesome_pod_file + update_awesome_pod_build_settings(installer) + ################ Awesome Notifications pod modification 1 ################### +end + +################ Awesome Notifications pod modification 2 ################### +awesome_pod_file = File.expand_path(File.join('plugins', 'awesome_notifications', 'ios', 'Scripts', 'AwesomePodFile'), '.symlinks') +require awesome_pod_file +update_awesome_main_target_settings('Runner', File.dirname(File.realpath(__FILE__)), flutter_root) +################ Awesome Notifications pod modification 2 ################### diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d4af5a1 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,582 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3965FF1D950A2B2E051F6219 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E598BBDD2ED0112E344B590 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5E598BBDD2ED0112E344B590 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6A2D47C573B065AD356C7183 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CA00A9F43BB1AB7D908E180A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + DFFCB3F45BF495277AE4D3EE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3965FF1D950A2B2E051F6219 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7226BEAE3D25CA120A434CC9 /* Pods */ = { + isa = PBXGroup; + children = ( + DFFCB3F45BF495277AE4D3EE /* Pods-Runner.debug.xcconfig */, + CA00A9F43BB1AB7D908E180A /* Pods-Runner.release.xcconfig */, + 6A2D47C573B065AD356C7183 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 7226BEAE3D25CA120A434CC9 /* Pods */, + E396C277672D83FCFDB51CCB /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + E396C277672D83FCFDB51CCB /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5E598BBDD2ED0112E344B590 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + A3B5CB395514F4BF9CDF304E /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + C3C8545BDAC52D360D298A29 /* [CP] Embed Pods Frameworks */, + F46F66CC32F585BBF2FB675E /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + A3B5CB395514F4BF9CDF304E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C3C8545BDAC52D360D298A29 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F46F66CC32F585BBF2FB675E /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = P3ZD9TNSKZ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = P3ZD9TNSKZ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = P3ZD9TNSKZ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 71% rename from macos/Runner.xcworkspace/contents.xcworkspacedata rename to ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/macos/Runner/Release.entitlements b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 51% rename from macos/Runner/Release.entitlements rename to ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index 741903e..f9b0d7c 100644 --- a/macos/Runner/Release.entitlements +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -2,11 +2,7 @@ - com.apple.security.app-sandbox - - com.apple.security.network.client - - com.apple.security.files.user-selected.read-only - + PreviewsEnabled + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 83% rename from macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ff47eeb..e3773d4 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -14,8 +14,8 @@ buildForAnalyzing = "YES"> @@ -26,12 +26,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -42,7 +43,7 @@ parallelizable = "YES"> @@ -54,6 +55,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -65,8 +67,8 @@ runnableDebuggingMode = "0"> @@ -82,8 +84,8 @@ runnableDebuggingMode = "0"> diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..b636303 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..2b21522 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..8471cd6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..c145b15 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..5da5679 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..cd2b74f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..68cbdbf Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..306efe8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..c145b15 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..959cc28 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..d86b69c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..3a5c49b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..e563327 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..30ae8c6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..2fb68c4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..d86b69c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..151862a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..c5ca065 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..a5880bd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..6ea8156 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..657cf77 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..87d1ce7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..b89f32a --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,69 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Nexus + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + nexus + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSSupportsOpeningDocumentsInPlace + + LSApplicationQueriesSchemes + + file + https + http + mailto + tel + + UIFileSharingEnabled + + ITSAppUsesNonExemptEncryption + + LSRequiresIPhoneOS + + NSCameraUsageDescription + This app needs camera access to scan asset QR codes + NSMicrophoneUsageDescription + This app doesn't use your microphone. + NSPhotoLibraryUsageDescription + This app doesn't use your photo library. + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/lib/controllers/account_data.dart b/lib/controllers/account_data.dart deleted file mode 100644 index 0926e08..0000000 --- a/lib/controllers/account_data.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/account_data.dart"; - -class AccountDataController extends Notifier> { - @override - IMap build() => .new(); - - void update(IMap newData) => - state = .new({...state.unlock, ...newData.unlock}); - - static final provider = - NotifierProvider>( - AccountDataController.new, - ); -} diff --git a/lib/controllers/attachment.dart b/lib/controllers/attachment.dart deleted file mode 100644 index e33493e..0000000 --- a/lib/controllers/attachment.dart +++ /dev/null @@ -1,40 +0,0 @@ -import "package:file_selector/file_selector.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/content/message.dart"; -import "package:path/path.dart"; - -class AttachmentController extends Notifier<(String, MessageContent?)?> { - final String roomId; - AttachmentController(this.roomId); - - @override - Null build() => null; - - Future add(XFile file) async { - final filename = basename(file.path); - state = (filename, null); - - final room = ref.watch( - RoomsController.provider.select((value) => value[roomId]), - ); - - final content = await ref - .watch(ClientController.provider.notifier) - .uploadMedia( - .new( - path: file.path, - encrypt: room?.state[EventType.encryption.type]?.isNotEmpty == true, - ), - ); - - state = (filename, content); - } - - static final provider = NotifierProvider.family - .autoDispose( - AttachmentController.new, - ); -} diff --git a/lib/controllers/auth_url.dart b/lib/controllers/auth_url.dart deleted file mode 100644 index 156eb81..0000000 --- a/lib/controllers/auth_url.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_id.dart"; -import "package:nexus/models/oauth_auth_code_response.dart"; -import "package:nexus/models/requests/oauth/get_auth_url.dart"; - -class AuthUrlController extends AsyncNotifier { - final Uri homeserver; - AuthUrlController(this.homeserver); - - @override - Future build() async => ref - .watch(ClientController.provider.notifier) - .getAuthUrl( - .new( - homeserverUrl: homeserver, - redirectUri: .new(scheme: "nexus.federated.nexus", path: "/"), - responseMode: .query, - scopes: .new([Scope.clientApi, Scope.device]), - clientId: await ref.watch( - ClientIdController.provider(homeserver).future, - ), - ), - ); - - static final provider = AsyncNotifierProvider.family - .autoDispose( - AuthUrlController.new, - ); -} diff --git a/lib/controllers/author.dart b/lib/controllers/author.dart deleted file mode 100644 index bbbe068..0000000 --- a/lib/controllers/author.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "dart:async"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/user.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/event.dart"; - -class AuthorController extends AsyncNotifier { - final Event event; - AuthorController(this.event); - - @override - Future build() async { - final member = await ref.watch( - UserController.provider( - .new(roomId: event.roomId, userId: event.sender), - ).future, - ); - - return .new( - status: member.status, - avatarUrl: event.pmp?.avatarUrl ?? member.avatarUrl, - displayName: event.pmp?.displayName ?? member.displayName, - ); - } - - static final provider = - AsyncNotifierProvider.family( - AuthorController.new, - ); -} diff --git a/lib/controllers/avatar_controller.dart b/lib/controllers/avatar_controller.dart new file mode 100644 index 0000000..1bb4c72 --- /dev/null +++ b/lib/controllers/avatar_controller.dart @@ -0,0 +1,17 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/client_controller.dart"; + +class AvatarController extends AsyncNotifier { + final String mxc; + AvatarController(this.mxc); + @override + Future build() async => Uri.parse(mxc).getThumbnailUri( + await ref.watch(ClientController.provider.future), + width: 24, + height: 24, + ); + + static final provider = AsyncNotifierProvider.family + .autoDispose(AvatarController.new); +} diff --git a/lib/controllers/client.dart b/lib/controllers/client.dart deleted file mode 100644 index 10e153d..0000000 --- a/lib/controllers/client.dart +++ /dev/null @@ -1,316 +0,0 @@ -import "dart:ffi"; -import "dart:io"; -import "dart:isolate"; -import "dart:math"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:ffi/ffi.dart"; -import "package:flutter/foundation.dart"; -import "package:nexus/controllers/account_data.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/init_complete.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/space_edges.dart"; -import "package:nexus/controllers/sync_status.dart"; -import "package:nexus/controllers/top_level_spaces.dart"; -import "package:nexus/helpers/extensions/gomuks_buffer.dart"; -import "package:nexus/main.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/oauth_auth_code_response.dart"; -import "package:nexus/models/paginate.dart"; -import "package:nexus/models/requests/get_event.dart"; -import "package:nexus/models/requests/get_related_events.dart"; -import "package:nexus/models/requests/get_room_state.dart"; -import "package:nexus/models/requests/join_room.dart"; -import "package:nexus/models/profile.dart"; -import "package:nexus/models/requests/oauth/exchange_token.dart"; -import "package:nexus/models/requests/oauth/get_auth_url.dart"; -import "package:nexus/models/requests/oauth/register_client.dart"; -import "package:nexus/models/requests/paginate.dart"; -import "package:nexus/models/requests/redact_event.dart"; -import "package:nexus/models/requests/report.dart"; -import "package:nexus/models/requests/send_event.dart"; -import "package:nexus/models/requests/send_message.dart"; -import "package:nexus/models/requests/set_membership.dart"; -import "package:nexus/models/requests/set_state.dart"; -import "package:nexus/models/requests/upload_media.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/sync_data.dart"; -import "package:nexus/src/third_party/gomuks.g.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:path_provider/path_provider.dart"; - -class ClientController extends AsyncNotifier { - @override - Future build() async { - final Pointer root; - if (Platform.isAndroid) { - final dir = await getApplicationSupportDirectory(); - root = "${dir.path}/gomuks".toNativeUtf8().cast(); - } else { - root = nullptr.cast(); - } - - final handle = GomuksInit(root); - - final callable = - NativeCallable< - Void Function(Pointer, Int64, GomuksOwnedBuffer) - >.listener(( - Pointer command, - int requestId, - GomuksOwnedBuffer data, - ) { - try { - final muksEventType = command.cast().toDartString(); - debugPrint("Handling $muksEventType..."); - final decodedMuksEvent = data.toJson(); - - switch (muksEventType) { - case "client_state": - ref - .watch(ClientStateController.provider.notifier) - .set(.fromJson(decodedMuksEvent)); - break; - case "sync_status": - ref - .watch(SyncStatusController.provider.notifier) - .set(.fromJson(decodedMuksEvent)); - break; - case "init_complete": - ref.watch(InitCompleteController.provider.notifier).complete(); - break; - case "send_complete": - final event = Event.fromJson(decodedMuksEvent["event"]); - ref - .watch(RoomsController.provider.notifier) - .update( - .new({ - event.roomId: .new(events: .new({event.rowId: event})), - }), - .new(), - ); - - break; - case "sync_complete": - final syncData = SyncData.fromJson(decodedMuksEvent); - final roomProvider = RoomsController.provider; - final accountDataProvider = AccountDataController.provider; - - if (syncData.clearState) { - ref.invalidate(roomProvider); - ref.invalidate(accountDataProvider); - } - - ref - .watch(roomProvider.notifier) - .update(syncData.rooms, syncData.leftRooms); - ref - .watch(accountDataProvider.notifier) - .update(syncData.accountData); - - if (syncData.topLevelSpaces != null) { - ref - .watch(TopLevelSpacesController.provider.notifier) - .set(syncData.topLevelSpaces!); - } - - if (syncData.spaceEdges != null) { - ref - .watch(SpaceEdgesController.provider.notifier) - .set(syncData.spaceEdges!); - } - - // ref - // .watch(SyncStatusController.provider.notifier) - // .set(SyncStatus.fromJson(decodedMuksEvent)); - break; - default: - debugPrint("Unhandled event: $muksEventType"); - } - debugPrint("Finished handling $muksEventType..."); - } catch (error, stackTrace) { - if (kDebugMode) { - debugPrintStack(stackTrace: stackTrace, label: error.toString()); - rethrow; - } else { - showError(error, stackTrace); - } - } - }); - - ref.onDispose(() => GomuksDestroy(handle)); - ref.onDispose(callable.close); - - final errorCode = GomuksStart(handle, callable.nativeFunction); - - if (errorCode == 0) return handle; - throw Exception("GomuksStart returned error code $errorCode"); - } - - Future _sendCommand( - String command, [ - Map data = const {}, - ]) async { - final bufferPointer = data.toGomuksBufferPtr(); - final handle = await future; - final response = await Isolate.run( - () => GomuksSubmitCommand( - handle, - command.toNativeUtf8().cast(), - bufferPointer.ref, - ), - ); - - calloc.free(bufferPointer); - - final json = response.buf.toJson(); - if (response.command.cast().toDartString() == "error") { - throw json; - } - - return json; - } - - Future redactEvent(RedactEventRequest report) => - _sendCommand("redact_event", report.toJson()); - - Future sendMessage(SendMessageRequest request) async => - Event.fromJson(await _sendCommand("send_message", request.toJson())); - - Future sendEvent(SendEventRequest request) async { - final json = request.toJson(); - final content = request.content.toJson(); - - return Event.fromJson( - await _sendCommand("send_event", { - ...json, - "content": { - ...content, - "m.relates_to": { - ...((content["m.relates_to"] as Map?) ?? {}), - "event_id": request.relatesTo, - "rel_type": request.relationType, - }, - }, - }), - ); - } - - Future setState(SetStateRequest request) async => - await _sendCommand("set_state", request.toJson()); - - Future verify(String recoveryKey) async { - try { - await _sendCommand("verify", {"recovery_key": recoveryKey}); - return null; - } catch (error) { - return error.toString(); - } - } - - Future joinRoom(JoinRoomRequest request) async { - final response = await _sendCommand("join_room", request.toJson()); - return response["room_id"]; - } - - Future getAccessToken() async { - final response = await _sendCommand("get_account_info", {}); - return response?["access_token"]; - } - - Future leaveRoom(Room room) async { - if (room.metadata == null) return; - await _sendCommand("leave_room", {"room_id": room.metadata!.id}); - } - - // (await _sendCommand("get_event_context", { - // "room_id": request.roomId, - // "event_id": r"$OqZT4NuTj0J1-771IOEEWRI4XdumRNu6ighlvO3K3gc", - // })); - - Future> getRoomState(GetRoomStateRequest request) async { - Future getState(GetRoomStateRequest request) async => - (await _sendCommand("get_room_state", request.toJson())) as List?; - final response = await getState(request); - - return .new( - (response ?? await getState(request.copyWith(refetch: true)) ?? []).map( - (event) => .fromJson(event), - ), - ); - } - - Future?> getRelatedEvents( - GetRelatedEventsRequest request, - ) async { - final response = - (await _sendCommand("get_related_events", request.toJson())) as List?; - return .new(response?.map((event) => .fromJson(event))); - } - - Future getEvent(GetEventRequest request) async { - final json = await _sendCommand("get_event", request.toJson()); - return json == null ? null : .fromJson(json); - } - - Future paginate(PaginateRequest request) async => - .fromJson(await _sendCommand("paginate", request.toJson())); - - Future getProfile(String userId) async { - final json = await _sendCommand("get_profile", {"user_id": userId}); - return .fromJsonWithCatch({...json, "id": userId}); - } - - Future reportEvent(ReportRequest request) => - _sendCommand("report_event", request.toJson()); - - Future setMembership(SetMembershipRequest request) => - _sendCommand("set_membership", request.toJson()); - - Future uploadMedia(UploadMediaRequest request) async => - .fromJson(await _sendCommand("upload_media", request.toJson())); - - Future logout() => _sendCommand("logout"); - - Future markRead(Room room) async { - final eventRowId = room.timeline[room.timeline.keys.reduce(max)]; - final event = eventRowId == null ? null : room.events[eventRowId]; - if (event == null || room.metadata == null) return; - - await _sendCommand("mark_read", { - "room_id": room.metadata!.id, - "receipt_type": "m.read", - "event_id": event.eventId, - }); - } - - Future registerClient(OAuthRegisterClientRequest request) async => - (await _sendCommand( - "oauth_register_client", - request.toJson(), - ))["client_id"]; - - Future getAuthUrl(OAuthGetAuthUrl request) async => - .fromJson( - await _sendCommand("oauth_get_authorization_url", request.toJson()), - ); - - Future exchangeToken(OAuthExchangeTokenRequest request) async => - await _sendCommand("oauth_exchange_token", request.toJson()); - - Future discoverHomeserver(Uri homeserver) async { - try { - final response = await _sendCommand("discover_homeserver", { - "user_id": "@fake-user:${homeserver.host}", - }); - return Uri.parse(response["m.homeserver"]?["base_url"]); - } catch (error) { - return null; - } - } - - static final provider = AsyncNotifierProvider( - ClientController.new, - ); -} diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart new file mode 100644 index 0000000..1a88526 --- /dev/null +++ b/lib/controllers/client_controller.dart @@ -0,0 +1,100 @@ +import "dart:convert"; +import "dart:io"; +import "package:flutter/foundation.dart"; +import "package:nexus/controllers/database_controller.dart"; +import "package:flutter_vodozemac/flutter_vodozemac.dart"; +import "package:matrix/matrix.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/secure_storage_controller.dart"; +import "package:nexus/models/session_backup.dart"; + +class ClientController extends AsyncNotifier { + @override + bool updateShouldNotify( + AsyncValue previous, + AsyncValue next, + ) => previous.hasValue != next.hasValue; + static const sessionBackupKey = "sessionBackup"; + + @override + Future build() async { + final client = Client( + "nexus", + logLevel: kReleaseMode ? Level.warning : Level.verbose, + importantStateEvents: {"im.ponies.room_emotes"}, + supportedLoginTypes: {AuthenticationTypes.password}, + database: await MatrixSdkDatabase.init( + "nexus", + database: await ref.watch(DatabaseController.provider.future), + ), + nativeImplementations: NativeImplementationsIsolate( + compute, + vodozemacInit: init, + ), + ); + + final backupJson = await ref + .watch(SecureStorageController.provider.notifier) + .get(sessionBackupKey); + + if (backupJson != null) { + final backup = SessionBackup.fromJson(json.decode(backupJson)); + + await client.init( + waitForFirstSync: false, + newToken: backup.accessToken, + newHomeserver: backup.homeserver, + newUserID: backup.userID, + newDeviceID: backup.deviceID, + newDeviceName: backup.deviceName, + ); + } + + return client; + } + + Future setHomeserver(Uri homeserverUrl) async { + final client = await future; + try { + await client.checkHomeserver(homeserverUrl); + return true; + } catch (_) { + return false; + } + } + + Future login(String username, String password) async { + final client = await future; + try { + final deviceName = "Nexus Client login on ${Platform.localHostname}"; + final details = await MatrixApi(homeserver: client.homeserver).login( + LoginType.mLoginPassword, + initialDeviceDisplayName: deviceName, + identifier: AuthenticationUserIdentifier(user: username), + password: password, + ); + await ref + .watch(SecureStorageController.provider.notifier) + .set( + sessionBackupKey, + json.encode( + SessionBackup( + accessToken: details.accessToken, + homeserver: client.homeserver!, + userID: details.userId, + deviceID: details.deviceId, + deviceName: deviceName, + ).toJson(), + ), + ); + ref.invalidateSelf(asReload: true); + return true; + } catch (_) { + return false; + } + } + + static final provider = AsyncNotifierProvider( + ClientController.new, + ); +} diff --git a/lib/controllers/client_id.dart b/lib/controllers/client_id.dart deleted file mode 100644 index ac8f5cb..0000000 --- a/lib/controllers/client_id.dart +++ /dev/null @@ -1,33 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; - -class ClientIdController extends AsyncNotifier { - final Uri homeserver; - ClientIdController(this.homeserver); - - @override - Future build() => ref - .watch(ClientController.provider.notifier) - .registerClient( - .new( - clientName: "Nexus", - applicationType: .native, - grantTypes: .new([.authorizationCode, .refreshToken]), - responseTypes: .new([.code]), - logoUri: Uri.https( - "nexus.federated.nexus", - "raw/branch/main/assets/mobile.svg", - ), - homeserverUrl: homeserver, - clientUri: Uri.https("nexus.federated.nexus"), - redirectUris: .new([ - .new(scheme: "nexus.federated.nexus", path: "/"), - ]), - ), - ); - - static final provider = - AsyncNotifierProvider.family( - ClientIdController.new, - ); -} diff --git a/lib/controllers/client_state.dart b/lib/controllers/client_state.dart deleted file mode 100644 index 1b77ecb..0000000 --- a/lib/controllers/client_state.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/client_state.dart"; - -class ClientStateController extends Notifier { - @override - Null build() => null; - - void set(ClientState newState) => state = newState; - - static final provider = NotifierProvider( - ClientStateController.new, - ); -} diff --git a/lib/controllers/cross_cache.dart b/lib/controllers/cross_cache.dart deleted file mode 100644 index 4d5611a..0000000 --- a/lib/controllers/cross_cache.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -class CrossCacheController extends Notifier { - @override - CrossCache build() => .new(); - - static final provider = NotifierProvider( - CrossCacheController.new, - ); -} diff --git a/lib/controllers/database_controller.dart b/lib/controllers/database_controller.dart new file mode 100644 index 0000000..706560b --- /dev/null +++ b/lib/controllers/database_controller.dart @@ -0,0 +1,18 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:path/path.dart"; +import "package:path_provider/path_provider.dart"; +import "package:sqflite_common_ffi/sqflite_ffi.dart"; + +class DatabaseController extends AsyncNotifier { + @override + Future build() async { + databaseFactory = databaseFactoryFfi; + return databaseFactoryFfi.openDatabase( + join((await getApplicationSupportDirectory()).path, "database.db"), + ); + } + + static final provider = AsyncNotifierProvider( + DatabaseController.new, + ); +} diff --git a/lib/controllers/emoji.dart b/lib/controllers/emoji.dart deleted file mode 100644 index caea3de..0000000 --- a/lib/controllers/emoji.dart +++ /dev/null @@ -1,84 +0,0 @@ -import "dart:convert"; -import "package:emoji_text_field/models/emoji_category.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:http/http.dart"; -import "package:nexus/models/emoji.dart"; - -typedef EmojiTuple = (IMap, IMap>); - -class EmojiController extends AsyncNotifier { - @override - Future build() async { - final response = await get( - .https("github.com", "github/gemoji/raw/refs/heads/master/db/emoji.json"), - ); - - if (response.statusCode != 200) { - throw Exception("Failed to load emoji data"); - } - - final data = json.decode(response.body); - - final entries = (data as List) - .cast>() - .map(Emoji.fromJson) - .toIList(); - - final categoryMap = entries.fold>>( - .new(), - (acc, entry) => acc.update( - entry.category, - (list) => list.add(entry.emoji), - ifAbsent: () => .new([entry.emoji]), - ), - ); - - final keywordMap = entries.fold>>( - .new(), - (acc, entry) => acc.add( - entry.emoji, - .new([...entry.tags, ...entry.aliases, entry.description]), - ), - ); - - final customCategories = IMap.fromEntries( - categoryMap.entries.map( - (entry) => MapEntry( - entry.key, - EmojiCategory( - name: entry.key, - icon: switch (entry.key) { - "Smileys & Emotion" => Icons.emoji_emotions, - "People & Body" => Icons.emoji_people, - "Animals & Nature" => Icons.emoji_nature, - "Food & Drink" => Icons.emoji_food_beverage, - "Travel & Places" => Icons.travel_explore, - "Activities" => Icons.sports_soccer, - "Objects" => Icons.emoji_objects, - "Symbols" => Icons.emoji_symbols, - "Flags" => Icons.emoji_flags, - _ => Icons.category, - }, - emojis: entry.value.toList(growable: false), - ), - ), - ), - ); - - final customKeywords = IMap( - .fromEntries( - keywordMap.entries.map( - (e) => .new(e.key, e.value.toList(growable: false)), - ), - ), - ); - - return (customCategories, customKeywords); - } - - static final provider = AsyncNotifierProvider( - EmojiController.new, - ); -} diff --git a/lib/controllers/event.dart b/lib/controllers/event.dart deleted file mode 100644 index b7b417a..0000000 --- a/lib/controllers/event.dart +++ /dev/null @@ -1,32 +0,0 @@ -import "package:collection/collection.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/requests/get_event.dart"; - -class EventController extends AsyncNotifier { - final GetEventRequest request; - EventController(this.request); - - @override - Future build() async { - final room = ref.watch( - RoomsController.provider.select((value) => value[request.roomId]), - ); - final event = room?.events.values.firstWhereOrNull( - (event) => event.eventId == request.eventId, - ); - - return event ?? - await ref - .watch(ClientController.provider.notifier) - .getEvent(request) - .onError((_, _) => null); - } - - static final provider = AsyncNotifierProvider.family - .autoDispose( - EventController.new, - ); -} diff --git a/lib/controllers/events_controller.dart b/lib/controllers/events_controller.dart new file mode 100644 index 0000000..37b9ff2 --- /dev/null +++ b/lib/controllers/events_controller.dart @@ -0,0 +1,30 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/from_controller.dart"; + +class EventsController extends AsyncNotifier { + EventsController(this.room); + final Room room; + + @override + Future build({String? from}) async { + final response = await room.client.getRoomEvents( + room.id, + Direction.b, + from: from, + limit: 32, + ); + if (ref.mounted) { + ref.watch(FromController.provider(room).notifier).set(response.end); + } + return response; + } + + Future prev() async => + build(from: ref.read(FromController.provider(room))); + + static final provider = AsyncNotifierProvider.autoDispose + .family( + EventsController.new, + ); +} diff --git a/lib/controllers/from_controller.dart b/lib/controllers/from_controller.dart new file mode 100644 index 0000000..54c850a --- /dev/null +++ b/lib/controllers/from_controller.dart @@ -0,0 +1,15 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; + +class FromController extends Notifier { + FromController(_); + @override + String? build() => null; + + void set(String? value) => state = value; + + static final provider = + NotifierProvider.family( + FromController.new, + ); +} diff --git a/lib/controllers/header.dart b/lib/controllers/header.dart deleted file mode 100644 index 3e173ad..0000000 --- a/lib/controllers/header.dart +++ /dev/null @@ -1,20 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; - -class HeaderController extends AsyncNotifier> { - @override - Future> build() async { - if (ref.watch(ClientStateController.provider)?.isLoggedIn != true) { - return {}; - } - final client = ref.watch(ClientController.provider.notifier); - final accessToken = await client.getAccessToken(); - return {"authorization": "Bearer $accessToken"}; - } - - static final provider = - AsyncNotifierProvider>( - HeaderController.new, - ); -} diff --git a/lib/controllers/image_picker.dart b/lib/controllers/image_picker.dart deleted file mode 100644 index 797c997..0000000 --- a/lib/controllers/image_picker.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:image_picker/image_picker.dart"; - -class ImagePickerController extends Notifier { - @override - ImagePicker build() => .new(); - - static final provider = NotifierProvider( - ImagePickerController.new, - ); -} diff --git a/lib/controllers/init_complete.dart b/lib/controllers/init_complete.dart deleted file mode 100644 index c011472..0000000 --- a/lib/controllers/init_complete.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; - -class InitCompleteController extends Notifier { - @override - bool build() => false; - void complete() => state = true; - - static final provider = NotifierProvider( - InitCompleteController.new, - ); -} diff --git a/lib/controllers/key.dart b/lib/controllers/key_controller.dart similarity index 77% rename from lib/controllers/key.dart rename to lib/controllers/key_controller.dart index eff3ab2..946892e 100644 --- a/lib/controllers/key.dart +++ b/lib/controllers/key_controller.dart @@ -1,5 +1,5 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/shared_prefs.dart"; +import "package:nexus/controllers/shared_prefs_controller.dart"; class KeyController extends Notifier { final String key; @@ -12,14 +12,14 @@ class KeyController extends Notifier { String? build() => ref.watch(SharedPrefsController.provider).requireValue.getString(key); - Future set(String? value) async { + Future set(String? id) async { final prefs = ref.watch(SharedPrefsController.provider).requireValue; - state = value; + state = id; - if (value == null) { + if (id == null) { prefs.remove(key); } else { - prefs.setString(key, value); + prefs.setString(key, id); } } diff --git a/lib/controllers/members.dart b/lib/controllers/members.dart deleted file mode 100644 index 8566b40..0000000 --- a/lib/controllers/members.dart +++ /dev/null @@ -1,46 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/requests/get_room_state.dart"; - -class MembersController extends AsyncNotifier> { - final String roomId; - MembersController(this.roomId); - - @override - Future> build() async { - final room = ref.watch( - RoomsController.provider.select((value) => value[roomId]), - ); - - if (room == null) return .new(); - - if (!room.hasFetchedMembers) { - final fetchedState = await ref - .watch(ClientController.provider.notifier) - .getRoomState( - GetRoomStateRequest( - roomId: roomId, - fetchMembers: !(room.metadata?.hasMemberList ?? false), - includeMembers: true, - ), - ); - - await ref - .read(RoomsController.provider.notifier) - .addState(roomId, fetchedState, isMembers: true); - } - - return room.state[EventType.membership.type]?.values - .map((rowId) => room.events[rowId]) - .nonNulls - .toISet() ?? - .new(); - } - - static final provider = AsyncNotifierProvider.autoDispose - .family, String>(MembersController.new); -} diff --git a/lib/controllers/members_by_status.dart b/lib/controllers/members_by_status.dart deleted file mode 100644 index 613f7b1..0000000 --- a/lib/controllers/members_by_status.dart +++ /dev/null @@ -1,32 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/members.dart"; -import "package:nexus/models/configs/members_by_status.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/event.dart"; - -class MembersByStatusController extends AsyncNotifier> { - final MembersByStatusConfig config; - MembersByStatusController(this.config); - - @override - Future> build() => ref.watch( - MembersController.provider(config.roomId).selectAsync( - (members) => members - .where( - (membership) => switch (membership.content) { - MembershipContent(:final status) => config.status == status, - _ => false, - }, - ) - .toISet(), - ), - ); - - static final provider = - AsyncNotifierProvider.family< - MembersByStatusController, - ISet, - MembersByStatusConfig - >(MembersByStatusController.new); -} diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart new file mode 100644 index 0000000..fae5433 --- /dev/null +++ b/lib/controllers/members_controller.dart @@ -0,0 +1,22 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; + +class MembersController extends AsyncNotifier> { + final Room room; + MembersController(this.room); + + @override + Future> build() async => IList( + (await room.client.getMembersByRoom( + room.id, + notMembership: Membership.leave, + )) ?? + [], + ); + + static final provider = + AsyncNotifierProvider.family, Room>( + MembersController.new, + ); +} diff --git a/lib/controllers/members_grouped.dart b/lib/controllers/members_grouped.dart deleted file mode 100644 index 6f41e9d..0000000 --- a/lib/controllers/members_grouped.dart +++ /dev/null @@ -1,64 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/members_by_status.dart"; -import "package:nexus/controllers/room_creators.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/configs/members_by_status.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/content/power_levels.dart"; -import "package:nexus/models/event.dart"; - -class MembersGroupedController - extends AsyncNotifier>>> { - final MembersByStatusConfig config; - MembersGroupedController(this.config); - - @override - Future>>> build() async { - final room = ref.watch( - RoomsController.provider.select((value) => value[config.roomId]), - ); - - final roomCreators = room == null - ? null - : ref.watch((RoomCreatorsController.provider(room))); - - final powerLevelsRowId = room?.state[EventType.powerLevels.type]?[""]; - final powerLevelsEvent = powerLevelsRowId == null - ? null - : room?.events[powerLevelsRowId]; - - final content = switch (powerLevelsEvent?.content) { - PowerLevelsContent content => content, - _ => PowerLevelsContent(), - }; - - final members = await ref.watch( - MembersByStatusController.provider(config).future, - ); - - return members - .fold>>(.new(), (result, event) { - final groupKey = roomCreators?.contains(event.stateKey!) == true - ? null - : content.users[event.stateKey!] ?? content.usersDefault; - - return result.update( - groupKey, - (value) => value.add(event), - ifAbsent: () => .new({event}), - ); - }) - .toEntryIList( - compare: (a, b) => - (b?.key ?? double.infinity).compareTo(a?.key ?? double.infinity), - ); - } - - static final provider = - AsyncNotifierProvider.family< - MembersGroupedController, - IList>>, - MembersByStatusConfig - >(MembersGroupedController.new); -} diff --git a/lib/controllers/multi_provider.dart b/lib/controllers/multi_provider.dart deleted file mode 100644 index 52dd8d9..0000000 --- a/lib/controllers/multi_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "dart:async"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -class MultiProviderController extends AsyncNotifier { - MultiProviderController(this.providers); - final IList providers; - - @override - Future build() => - .wait(providers.map((provider) => ref.watch(provider.future))); - - static final provider = - AsyncNotifierProvider.family< - MultiProviderController, - void, - IList - >(MultiProviderController.new); -} diff --git a/lib/controllers/pinned_events.dart b/lib/controllers/pinned_events.dart deleted file mode 100644 index 914a301..0000000 --- a/lib/controllers/pinned_events.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/event.dart"; -import "package:nexus/controllers/pinned_ids.dart"; -import "package:nexus/models/event.dart"; - -class PinnedEventsController extends AsyncNotifier> { - final String roomId; - PinnedEventsController(this.roomId); - - @override - Future> build() async { - final pinIds = ref.watch(PinnedIdsController.provider(roomId)); - - return (await Future.wait( - pinIds.map( - (eventId) => ref.watch( - EventController.provider( - .new(eventId: eventId, roomId: roomId), - ).future, - ), - ), - )).nonNulls.toIList(); - } - - static final provider = AsyncNotifierProvider.family - .autoDispose, String>( - PinnedEventsController.new, - ); -} diff --git a/lib/controllers/pinned_ids.dart b/lib/controllers/pinned_ids.dart deleted file mode 100644 index 3d1e5da..0000000 --- a/lib/controllers/pinned_ids.dart +++ /dev/null @@ -1,53 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/content/pinned_events.dart"; - -class PinnedIdsController extends Notifier> { - final String roomId; - PinnedIdsController(this.roomId); - - @override - IList build() { - final room = ref.watch( - RoomsController.provider.select((rooms) => rooms[roomId]), - ); - - if (room == null) return .new(); - - final pinnedRowId = room.state[EventType.pinnedEvents.type]?[""]; - final pinnedStateEvent = pinnedRowId == null - ? null - : room.events[pinnedRowId]; - - if (pinnedStateEvent?.content case PinnedEventsContent content) { - return content.pinnedEvents; - } - - return .new(); - } - - Future addPin(String eventId) async => - setPinned(.new(pinnedEvents: .new(state.add(eventId)))); - - Future removePin(String eventId) async => - setPinned(.new(pinnedEvents: .new(state.remove(eventId)))); - - Future setPinned(PinnedEventsContent content) => ref - .read(ClientController.provider.notifier) - .setState( - .new( - roomId: roomId, - type: EventType.pinnedEvents.type, - stateKey: "", - content: content, - ), - ); - - static final provider = NotifierProvider.family - .autoDispose, String>( - PinnedIdsController.new, - ); -} diff --git a/lib/controllers/power_level.dart b/lib/controllers/power_level.dart deleted file mode 100644 index 2f0c72e..0000000 --- a/lib/controllers/power_level.dart +++ /dev/null @@ -1,81 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/room_creators.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/configs/power_level.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/content/power_levels.dart"; - -class PowerLevelController extends Notifier { - final PowerLevelConfig config; - PowerLevelController(this.config); - - @override - bool build() { - if (config case EventPowerLevelConfig(:final eventType)) { - assert( - eventType != .redaction, - "Checking power level for a redaction should use [PowerLevelConfig.redaction].", - ); - } - - final room = ref.watch( - RoomsController.provider.select((value) => value[config.roomId]), - ); - - final roomCreators = room == null - ? null - : ref.watch(RoomCreatorsController.provider(room)); - - final eventRowId = room?.state[EventType.powerLevels.type]?[""]; - - final event = eventRowId == null ? null : room?.events[eventRowId]; - final content = event?.content is PowerLevelsContent - ? event!.content - : PowerLevelsContent(); - - final user = ref.watch( - ClientStateController.provider.select((value) => value?.userId), - ); - if (user == null || content is! PowerLevelsContent) return false; - - double powerLevelOf(String userId) => roomCreators?.contains(userId) == true - ? double.infinity - : (content.users[userId] ?? content.usersDefault).toDouble(); - - final userLevel = powerLevelOf(user); - - return switch (config) { - EventPowerLevelConfig(:final eventType) => - userLevel >= (content.events[eventType.type] ?? content.eventsDefault), - - MembershipActionPowerLevelConfig(:final action, :final targetUser) => - switch (action) { - .invite => userLevel >= content.invite, - - .kick => - userLevel >= content.kick && userLevel > powerLevelOf(targetUser), - - .ban => - userLevel >= content.ban && userLevel > powerLevelOf(targetUser), - - .unban => userLevel >= content.ban, - }, - - StatePowerLevelConfig(:final eventType) => - userLevel >= (content.events[eventType.type] ?? content.stateDefault), - - RedactionPowerLevelConfig(:final targetUser) => - userLevel >= - (targetUser == user - ? (content.events[EventType.redaction.type] ?? - content.eventsDefault) - : content.redact), - }; - } - - static final provider = NotifierProvider.autoDispose - .family( - PowerLevelController.new, - ); -} diff --git a/lib/controllers/profile.dart b/lib/controllers/profile.dart deleted file mode 100644 index 32525a3..0000000 --- a/lib/controllers/profile.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/models/profile.dart"; - -class ProfileController extends AsyncNotifier { - final String userId; - ProfileController(this.userId); - - @override - Future build() { - final client = ref.watch(ClientController.provider.notifier); - return client.getProfile(userId); - } - - static final provider = AsyncNotifierProvider.family - .autoDispose(ProfileController.new); -} diff --git a/lib/controllers/reactions.dart b/lib/controllers/reactions.dart deleted file mode 100644 index 615a59d..0000000 --- a/lib/controllers/reactions.dart +++ /dev/null @@ -1,55 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/configs/reactions.dart"; -import "package:nexus/models/content/reaction.dart"; - -class ReactionsController extends AsyncNotifier>> { - final ReactionsConfig config; - ReactionsController(this.config); - - @override - Future>> build() async { - final eventInfo = ref.watch( - RoomsController.provider.select((value) { - final event = value[config.roomId]?.events[config.eventRowId]; - return event == null ? null : (event.eventId, event.reactions); - }), - ); - - final reactionEvents = eventInfo?.$2.isNotEmpty == true - ? await ref - .watch(ClientController.provider.notifier) - .getRelatedEvents( - .new( - roomId: config.roomId, - eventId: eventInfo!.$1, - relationType: "m.annotation", - ), - ) - : null; - - return reactionEvents - ?.where((event) => event.redactedBy == null) - .fold>>(.new(), (acc, event) { - if (event.content case ReactionContent(:final key?)) { - return acc.update( - key, - (list) => list.add(event.sender), - ifAbsent: () => .new([event.sender]), - ); - } - - return acc; - }) ?? - .new(); - } - - static final provider = - AsyncNotifierProvider.family< - ReactionsController, - IMap>, - ReactionsConfig - >(ReactionsController.new); -} diff --git a/lib/controllers/room_chat.dart b/lib/controllers/room_chat.dart deleted file mode 100644 index ac979e0..0000000 --- a/lib/controllers/room_chat.dart +++ /dev/null @@ -1,229 +0,0 @@ -import "dart:async"; -import "dart:math"; -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:fluttertagger/fluttertagger.dart"; -import "package:nexus/controllers/attachment.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/content/reaction.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/requests/redact_event.dart"; -import "package:nexus/models/relation_type.dart"; -import "package:nexus/models/requests/send_message.dart"; -import "package:nexus/models/room.dart"; - -class RoomChatController extends AsyncNotifier?> { - final String roomId; - RoomChatController(this.roomId); - - @override - Future?> build() async { - final client = ref.watch(ClientController.provider.notifier); - final room = ref.watch( - RoomsController.provider.select((rooms) => rooms[roomId]), - ); - - if (room == null) return null; - - if (!room.hasFetchedState) { - final state = await client.getRoomState(.new(roomId: roomId)); - - await ref.read(RoomsController.provider.notifier).addState(roomId, state); - } - - // While there are under 20 events, try to load more - // until there's no more or the conditions are met. - if (room.hasMore && room.timeline.length < 20) { - loadOlder(); - } - - return room.timeline - .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) - .map((element) => element.value) - .toIList() - .addAll(room.sticky) - .map((entry) { - final foundEvent = entry == null ? null : room.events[entry]; - - final editedEvent = - foundEvent == null || foundEvent.lastEditRowId == 0 - ? null - : room.events[foundEvent.lastEditRowId]; - - return editedEvent == null - ? foundEvent - : foundEvent?.copyWith( - content: editedEvent.content, - localContent: editedEvent.localContent, - ); - }) - .nonNulls - .toIList(); - } - - Future deleteMessage(Event event, {String? reason}) => ref - .watch(ClientController.provider.notifier) - .redactEvent( - RedactEventRequest( - eventId: event.eventId, - roomId: roomId, - reason: reason, - ), - ); - - Future loadOlder() async { - final timelineKeys = ref - .read(RoomsController.provider.select((value) => value[roomId])) - ?.timeline - .keys; - final response = await ref - .watch(ClientController.provider.notifier) - .paginate( - .new( - roomId: roomId, - maxTimelineId: timelineKeys?.isNotEmpty == true - ? timelineKeys?.reduce(min) - : null, - ), - ); - - ref - .watch(RoomsController.provider.notifier) - .update( - IMap({ - roomId: Room( - events: IMap.fromIterable( - response.events.addAll(response.relatedEvents), - keyMapper: (event) => event.rowId, - valueMapper: (event) => event, - ), - hasMore: response.hasMore, - timeline: IMap.fromIterable( - response.events, - keyMapper: (event) => event.timelineRowId, - valueMapper: (event) => event.rowId, - ), - ), - }), - .new(), - ); - - return response.hasMore; - } - - Future send( - String text, { - bool shouldMention = true, - required IList tags, - required RelationType relationType, - Event? relation, - }) async { - final attachment = ref.watch(AttachmentController.provider(roomId)); - - if (attachment != null && attachment.$2 == null) return; - - var taggedMessage = text; - - for (final tag in tags) { - final escaped = RegExp.escape(tag.id); - final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?"); - - taggedMessage = taggedMessage.replaceAllMapped( - pattern, - (match) => match.group(1)!, - ); - } - - final client = ref.watch(ClientController.provider.notifier); - final event = await client.sendMessage( - SendMessageRequest( - roomId: roomId, - baseContent: attachment?.$2, - mentions: Mentions( - userIds: [ - if (shouldMention == true && - relation != null && - relationType == RelationType.reply) - relation.sender, - ].toIList(), - room: taggedMessage.contains("@room"), - ), - text: taggedMessage, - relation: relation == null - ? null - : .new(eventId: relation.eventId, relationType: relationType), - ), - ); - - ref.invalidate(AttachmentController.provider(roomId)); - - ref - .watch(RoomsController.provider.notifier) - .update( - .new({ - roomId: .new( - events: .new({event.rowId: event}), - sticky: .new({event.rowId}), - ), - }), - .new(), - ); - } - - Future removeReaction( - String reaction, - Event event, - String userId, - ) async { - final client = ref.watch(ClientController.provider.notifier); - final allReactionEvents = await client.getRelatedEvents( - .new( - roomId: roomId, - eventId: event.eventId, - relationType: "m.annotation", - ), - ); - - final reactionEvents = allReactionEvents - ?.where((event) => event.redactedBy == null) - .toIList(); - - final reactionEvent = reactionEvents?.firstWhereOrNull( - (event) => switch (event.content) { - ReactionContent(:final key) => - key == reaction && event.sender == userId, - _ => false, - }, - ); - - if (reactionEvent != null) { - await ref - .watch(ClientController.provider.notifier) - .redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId)); - } - } - - Future sendReaction(String reaction, Event event) async { - final client = ref.watch(ClientController.provider.notifier); - - await client.sendEvent( - .new( - roomId: roomId, - type: EventType.reaction.type, - content: ReactionContent(key: reaction), - synchronous: true, - disableEncryption: true, - relatesTo: event.eventId, - relationType: "m.annotation", - ), - ); - } - - static final provider = AsyncNotifierProvider.family - .autoDispose?, String>( - RoomChatController.new, - ); -} diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart new file mode 100644 index 0000000..1a1be39 --- /dev/null +++ b/lib/controllers/room_chat_controller.dart @@ -0,0 +1,145 @@ +import "package:collection/collection.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart" as chat; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/avatar_controller.dart"; +import "package:nexus/controllers/events_controller.dart"; +import "package:nexus/helpers/extensions/event_to_message.dart"; +import "package:nexus/helpers/extensions/list_to_messages.dart"; +import "package:fluttertagger/fluttertagger.dart" as tagger; +import "package:nexus/models/relation_type.dart"; + +class RoomChatController extends AsyncNotifier { + final Room room; + RoomChatController(this.room); + + @override + Future build() async { + final response = await ref.watch(EventsController.provider(room).future); + + ref.onDispose( + room.client.onTimelineEvent.stream.listen((event) async { + if (event.roomId != room.id) return; + + if (event.type == EventTypes.Redaction) { + final controller = await future; + final message = controller.messages.firstWhereOrNull( + (message) => message.id == event.redacts, + ); + if (message == null) return; + + await controller.removeMessage(message); + } else { + final message = await event.toMessage(includeEdits: true); + if (event.relationshipType == RelationshipTypes.edit) { + final controller = await future; + final oldMessage = controller.messages.firstWhereOrNull( + (element) => element.id == event.relationshipEventId, + ); + if (oldMessage == null || message == null) return; + return await updateMessage( + oldMessage, + message.copyWith(id: oldMessage.id), + ); + } + if (message != null) { + return await insertMessage(message); + } + } + }).cancel, + ); + + return InMemoryChatController( + messages: await response.chunk.toMessages(room), + ); + } + + Future insertMessage(Message message) async { + final controller = await future; + final oldMessage = message.metadata?["txnId"] == null + ? null + : controller.messages.firstWhereOrNull( + (element) => + element.metadata?["txnId"] == message.metadata?["txnId"], + ); + + return oldMessage == null + ? controller.insertMessage(message) + : controller.updateMessage(oldMessage, message); + } + + Future deleteMessage(Message message, {String? reason}) async { + final controller = await future; + await controller.removeMessage(message); + await room.redactEvent(message.id, reason: reason); + } + + Future loadOlder() async { + final controller = await future; + final response = await ref + .watch(EventsController.provider(room).notifier) + .prev(); + + final messages = await response.chunk.toMessages(room); + + await controller.insertAllMessages(messages, index: 0); + ref.notifyListeners(); + } + + Future markRead() async { + if (!room.hasNewMessages) return; + final controller = await future; + final id = controller.messages.last.id; + + await room.setReadMarker(id, mRead: id); + } + + Future updateMessage(Message message, Message newMessage) async => + (await future).updateMessage(message, newMessage); + + Future send( + String message, { + required Iterable tags, + required RelationType relationType, + Message? relation, + }) async { + var taggedMessage = message; + + for (final tag in tags) { + final escaped = RegExp.escape(tag.id); + final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?"); + + taggedMessage = taggedMessage.replaceAllMapped( + pattern, + (match) => match.group(1)!, + ); + } + + await room.sendTextEvent( + taggedMessage, + editEventId: relationType == RelationType.edit ? relation?.id : null, + inReplyTo: (relationType == RelationType.reply && relation != null) + ? await room.getEventById(relation.id) + : null, + ); + } + + Future resolveUser(String id) async { + final user = await room.client.getUserProfile(id); + return chat.User( + id: id, + name: user.displayname, + imageSource: user.avatarUrl == null + ? null + : (await ref.watch( + AvatarController.provider(user.avatarUrl!.toString()).future, + )).toString(), + ); + } + + static final provider = AsyncNotifierProvider.family + .autoDispose( + RoomChatController.new, + ); +} diff --git a/lib/controllers/room_creators.dart b/lib/controllers/room_creators.dart deleted file mode 100644 index 7db72c2..0000000 --- a/lib/controllers/room_creators.dart +++ /dev/null @@ -1,33 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/content/create.dart"; -import "package:nexus/models/room.dart"; - -class RoomCreatorsController extends Notifier> { - final Room room; - RoomCreatorsController(this.room); - - @override - IList build() { - final createRowId = room.state[EventType.create.type]?[""]; - final createEvent = createRowId == null ? null : room.events[createRowId]; - - if (createEvent == null) return .new(); - - final createEventContent = switch (createEvent.content) { - CreateContent content => content, - _ => null, - }; - - return switch (createEventContent?.additionalCreatorIds) { - IList creators => creators.add(createEvent.sender), - _ => .new([createEvent.sender]), - }; - } - - static final provider = - NotifierProvider.family, Room>( - RoomCreatorsController.new, - ); -} diff --git a/lib/controllers/rooms.dart b/lib/controllers/rooms.dart deleted file mode 100644 index d0c6eb9..0000000 --- a/lib/controllers/rooms.dart +++ /dev/null @@ -1,98 +0,0 @@ -import "dart:isolate"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/room.dart"; - -class RoomsController extends Notifier> { - @override - IMap build() => .new(); - - Future addState( - String roomId, - IList state, { - bool isMembers = false, - }) async => update( - .new({ - roomId: Room( - events: .fromEntries(state.map((event) => .new(event.rowId, event))), - hasFetchedState: true, - hasFetchedMembers: isMembers, - state: await Isolate.run(() { - final newState = state.fold>>( - .new(), - (previousValue, stateEvent) => previousValue.add( - stateEvent.type, - (previousValue[stateEvent.type] ?? .new()).add( - stateEvent.stateKey!, - stateEvent.rowId, - ), - ), - ); - return newState; - }), - ), - }), - .new(), - ); - - void update(IMap rooms, ISet leftRooms) { - final merged = rooms.entries.fold(state, (acc, entry) { - final roomId = entry.key; - final incoming = entry.value; - final existing = acc[roomId]; - - return acc.add( - roomId, - existing?.copyWith( - hasMore: incoming.hasMore, - sticky: - (incoming.sticky.isEmpty == true - ? existing.sticky - : existing.sticky.addAll(incoming.sticky)) - .removeWhere( - (rowId) => incoming.timeline.values.contains(rowId), - ), - metadata: incoming.metadata ?? existing.metadata, - events: incoming.events.isEmpty - ? existing.events - : existing.events.addAll(incoming.events), - state: incoming.state.entries.fold( - existing.state, - (previousValue, event) => previousValue.add( - event.key, - (previousValue[event.key] ?? .new()).addAll(event.value), - ), - ), - reset: false, - hasFetchedMembers: - incoming.hasFetchedMembers || existing.hasFetchedMembers, - hasFetchedState: - incoming.hasFetchedState || existing.hasFetchedState, - timeline: (incoming.reset - ? incoming.timeline - : existing.timeline.addAll(incoming.timeline)), - receipts: incoming.receipts.entries.fold( - existing.receipts, - (receiptAcc, event) => receiptAcc.add( - event.key, - (receiptAcc[event.key] ?? .new()).addAll(event.value), - ), - ), - ) ?? - incoming, - ); - }); - - final prunedList = leftRooms.fold( - merged, - (acc, roomId) => acc.remove(roomId), - ); - - state = prunedList; - } - - static final provider = NotifierProvider>( - RoomsController.new, - ); -} diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart new file mode 100644 index 0000000..864d656 --- /dev/null +++ b/lib/controllers/rooms_controller.dart @@ -0,0 +1,23 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/helpers/extensions/get_full_room.dart"; +import "package:nexus/models/full_room.dart"; + +class RoomsController extends AsyncNotifier> { + @override + Future> build() async { + final client = await ref.watch(ClientController.provider.future); + + ref.onDispose( + client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel, + ); + + return IList(await Future.wait(client.rooms.map((room) => room.fullRoom))); + } + + static final provider = + AsyncNotifierProvider>( + RoomsController.new, + ); +} diff --git a/lib/controllers/secure_storage_controller.dart b/lib/controllers/secure_storage_controller.dart new file mode 100644 index 0000000..8a579f5 --- /dev/null +++ b/lib/controllers/secure_storage_controller.dart @@ -0,0 +1,26 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:simple_secure_storage/simple_secure_storage.dart"; + +class SecureStorageController extends AsyncNotifier { + @override + Future build() => SimpleSecureStorage.initialize(); + + Future get(String key) async { + await future; + return SimpleSecureStorage.read(key); + } + + Future set(String key, String value) async { + await future; + return SimpleSecureStorage.write(key, value); + } + + Future clear() async { + await future; + return SimpleSecureStorage.clear(); + } + + static final provider = AsyncNotifierProvider( + SecureStorageController.new, + ); +} diff --git a/lib/controllers/selected_room_controller.dart b/lib/controllers/selected_room_controller.dart new file mode 100644 index 0000000..cfeead6 --- /dev/null +++ b/lib/controllers/selected_room_controller.dart @@ -0,0 +1,25 @@ +import "package:collection/collection.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/key_controller.dart"; +import "package:nexus/controllers/selected_space_controller.dart"; +import "package:nexus/models/full_room.dart"; + +class SelectedRoomController extends AsyncNotifier { + @override + Future build() async { + final space = await ref.watch(SelectedSpaceController.provider.future); + final selectedRoomId = ref.watch( + KeyController.provider(KeyController.roomKey), + ); + + return space.children.firstWhereOrNull( + (room) => room.roomData.id == selectedRoomId, + ) ?? + space.children.firstOrNull; + } + + static final provider = + AsyncNotifierProvider( + SelectedRoomController.new, + ); +} diff --git a/lib/controllers/selected_space_controller.dart b/lib/controllers/selected_space_controller.dart new file mode 100644 index 0000000..75bf287 --- /dev/null +++ b/lib/controllers/selected_space_controller.dart @@ -0,0 +1,24 @@ +import "package:collection/collection.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/key_controller.dart"; +import "package:nexus/controllers/spaces_controller.dart"; +import "package:nexus/models/space.dart"; + +class SelectedSpaceController extends AsyncNotifier { + @override + Future build() async { + final spaces = await ref.watch( + SpacesController.provider.selectAsync((data) => data), + ); + final selectedSpaceId = ref.watch( + KeyController.provider(KeyController.spaceKey), + ); + + return spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ?? + spaces.first; + } + + static final provider = AsyncNotifierProvider( + SelectedSpaceController.new, + ); +} diff --git a/lib/controllers/settings.dart b/lib/controllers/settings.dart deleted file mode 100644 index 572d2e9..0000000 --- a/lib/controllers/settings.dart +++ /dev/null @@ -1,27 +0,0 @@ -import "dart:convert"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/settings_file.dart"; -import "package:nexus/models/settings.dart"; - -class SettingsController extends AsyncNotifier { - @override - Future build() async { - final file = await ref.watch(SettingsFileController.provider.future); - - try { - return Settings.fromJson(json.decode(await file.readAsString())); - } catch (_) { - return Settings(); - } - } - - Future set(Settings settings) async { - state = AsyncData(settings); - final file = await ref.watch(SettingsFileController.provider.future); - await file.writeAsString(json.encode(settings.toJson())); - } - - static final provider = AsyncNotifierProvider( - SettingsController.new, - ); -} diff --git a/lib/controllers/settings_file.dart b/lib/controllers/settings_file.dart deleted file mode 100644 index 2c050bc..0000000 --- a/lib/controllers/settings_file.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "dart:io"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:path/path.dart"; -import "package:path_provider/path_provider.dart"; -import "package:xdg_directories/xdg_directories.dart"; - -class SettingsFileController extends AsyncNotifier { - @override - Future build() async { - final directory = await switch (Platform.isLinux) { - true => Directory( - join(configHome.absolute.path, "nexus"), - ).create(recursive: true), - false => getApplicationSupportDirectory(), - }; - - return File(join(directory.absolute.path, "config.json")); - } - - static final provider = AsyncNotifierProvider( - SettingsFileController.new, - ); -} diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart deleted file mode 100644 index 7c9796b..0000000 --- a/lib/controllers/settings_sections.dart +++ /dev/null @@ -1,142 +0,0 @@ -import "dart:io"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:intl/intl.dart"; -import "package:m3e_buttons/m3e_buttons.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/settings.dart"; -import "package:nexus/models/settings_category.dart"; -import "package:nexus/main.dart"; -import "package:nexus/widgets/settings/dialog_list_tile.dart"; - -class SettingsSectionsController - extends AsyncNotifier>> { - @override - Future>> build() async { - final settings = await ref.watch(SettingsController.provider.future); - - return .new({ - "General": .new([ - .new( - title: "Appearance", - icon: Icons.brush, - settings: .new([ - .new( - title: "Theme", - description: - "Toggle between Light Mode, Dark Mode, and System themes.", - icon: Icons.contrast, - builder: (title, description, icon) => DialogListTile( - icon: Icon(icon), - title: title, - subtitle: Text(description), - initialValue: settings.theme, - options: ThemeMode.values, - getName: (option) => toBeginningOfSentenceCase(option.name), - onChanged: (value) => ref - .watch(SettingsController.provider.notifier) - .set(settings.copyWith(theme: value)) - .onError(showError), - ), - ), - .new( - title: "Use Dynamic Theme", - icon: Icons.palette, - description: - "Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.", - builder: (title, description, icon) => SwitchListTile( - title: Text(title), - subtitle: Text(description), - secondary: Icon(icon), - value: settings.useDynamicTheming, - onChanged: - (Platform.isAndroid || - Platform.isLinux || - Platform.isMacOS || - Platform.isWindows) - ? (value) => ref - .watch(SettingsController.provider.notifier) - .set(settings.copyWith(useDynamicTheming: value)) - .onError(showError) - : null, - ), - ), - ]), - ), - .new( - title: "Behavior", - icon: Icons.psychology, - settings: .new([ - .new( - title: "Linux Mobile Mode", - description: - "Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.", - icon: Icons.construction, - builder: (title, description, icon) => SwitchListTile( - title: Text(title), - subtitle: Text(description), - secondary: Icon(icon), - value: settings.linuxMobileMode, - onChanged: Platform.isLinux - ? (value) => ref - .watch(SettingsController.provider.notifier) - .set(settings.copyWith(linuxMobileMode: value)) - .onError(showError) - : null, - ), - ), - ]), - ), - ]), - if (ref.watch(ClientStateController.provider)?.isLoggedIn == true) - "Account": .new([ - .new(title: "Profile", icon: Icons.person, settings: .new([])), - .new( - title: "Other", - icon: Icons.key, - settings: .new([ - .new( - title: "Log Out", - description: - "Log out of your account, returning you to the login page.", - builder: (title, description, icon) => Builder( - builder: (context) { - final colorScheme = Theme.of(context).colorScheme; - return M3EButton.icon( - onPressed: () async { - Navigator.of( - context, - ).popUntil((route) => route.isFirst); - - await WidgetsBinding.instance.endOfFrame; - - await ref - .watch(ClientController.provider.notifier) - .logout(); - }, - label: Text(title), - icon: Icon(icon), - tooltip: description, - decoration: .styleFrom( - backgroundColor: colorScheme.errorContainer, - foregroundColor: colorScheme.onErrorContainer, - ), - ); - }, - ), - icon: Icons.logout, - ), - ]), - ), - ]), - }); - } - - static final provider = - AsyncNotifierProvider.autoDispose< - SettingsSectionsController, - IMap> - >(SettingsSectionsController.new); -} diff --git a/lib/controllers/shared_prefs.dart b/lib/controllers/shared_prefs_controller.dart similarity index 82% rename from lib/controllers/shared_prefs.dart rename to lib/controllers/shared_prefs_controller.dart index 876fc47..f4dcdae 100644 --- a/lib/controllers/shared_prefs.dart +++ b/lib/controllers/shared_prefs_controller.dart @@ -3,7 +3,7 @@ import "package:shared_preferences/shared_preferences.dart"; class SharedPrefsController extends AsyncNotifier { @override - Future build() async => .getInstance(); + Future build() => SharedPreferences.getInstance(); static final provider = AsyncNotifierProvider( diff --git a/lib/controllers/space_edges.dart b/lib/controllers/space_edges.dart deleted file mode 100644 index 81347c5..0000000 --- a/lib/controllers/space_edges.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/space_edge.dart"; - -class SpaceEdgesController extends Notifier>> { - @override - IMap> build() => .new(); - - void set(IMap> newEdges) => - state = state.addAll(newEdges); - - static final provider = - NotifierProvider>>( - SpaceEdgesController.new, - ); -} diff --git a/lib/controllers/spaces.dart b/lib/controllers/spaces.dart deleted file mode 100644 index 24113ad..0000000 --- a/lib/controllers/spaces.dart +++ /dev/null @@ -1,151 +0,0 @@ -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/account_data.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/top_level_spaces.dart"; -import "package:nexus/controllers/space_edges.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/space.dart"; -import "package:nexus/models/subspace.dart"; - -class SpacesController extends Notifier> { - @override - IList build() { - final rooms = ref.watch(RoomsController.provider); - final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider); - final spaceEdges = ref.watch(SpaceEdgesController.provider); - final accountData = ref.watch(AccountDataController.provider); - - final childrenById = { - for (final entry in spaceEdges.entries) - entry.key: entry.value.map((e) => e.childId).toList(), - }; - - Set collectDescendants(String startId) { - final visited = {}; - final stack = [startId]; - - while (stack.isNotEmpty) { - final current = stack.removeLast(); - final children = childrenById[current] ?? const []; - - for (final child in children) { - if (visited.add(child)) { - stack.add(child); - } - } - } - - return visited; - } - - Space buildSpace(String spaceId) { - final space = rooms[spaceId]; - final directChildrenIds = childrenById[spaceId] ?? const []; - - final directRooms = []; - final subSpaces = []; - - for (final childId in directChildrenIds) { - final room = rooms[childId]; - if (room == null) continue; - - if (childrenById.containsKey(childId)) { - final descendants = collectDescendants(childId); - - subSpaces.add( - .new( - room: room, - children: .new(descendants.map((id) => rooms[id]).nonNulls), - ), - ); - } else { - directRooms.add(room); - } - } - - return .new( - id: spaceId, - room: space, - title: space?.metadata?.name ?? "Unnamed Space", - children: .new(directRooms), - subSpaces: .new(subSpaces), - ); - } - - final spaces = topLevelSpaceIds.map(buildSpace).toIList(); - - final usedRoomIds = { - for (final space in spaces) ...[ - ...space.children.map((r) => r.metadata?.id), - ...space.subSpaces.expand((s) => s.children.map((r) => r.metadata?.id)), - ], - }.nonNulls.toISet(); - - final directMessages = IMap( - accountData["m.direct"]?.content ?? {}, - ).values.expand((e) => e).toISet(); - - final otherRooms = rooms.entries - .where( - (e) => - !usedRoomIds.contains(e.key) && - !topLevelSpaceIds.contains(e.key) && - !childrenById.containsKey(e.key), - ) - .map((e) => e.value) - .toIList(); - - final homeRooms = otherRooms - .where((r) => !directMessages.contains(r.metadata?.id)) - .toIList(); - - final dmRooms = otherRooms - .where((r) => directMessages.contains(r.metadata?.id)) - .toIList(); - - final allSpaces = [ - .new( - id: "home", - title: "Home", - icon: Icons.home, - children: homeRooms, - subSpaces: .new(), - ), - .new( - id: "dms", - title: "Direct Messages", - icon: Icons.people, - children: dmRooms, - subSpaces: .new(), - ), - ...spaces, - ]; - - return allSpaces - .map( - (space) => space.copyWith( - children: .new( - space.children - .sortedBy( - (element) => - element - .metadata - ?.sortingTimestamp - .millisecondsSinceEpoch ?? - 0, - ) - .sortedBy((room) => room.metadata?.unreadMessages ?? 0) - .reversed, - ), - ), - ) - .toIList(); - } - - static final provider = NotifierProvider>( - SpacesController.new, - ); -} diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart new file mode 100644 index 0000000..3501de6 --- /dev/null +++ b/lib/controllers/spaces_controller.dart @@ -0,0 +1,77 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/helpers/extensions/get_full_room.dart"; +import "package:nexus/helpers/extensions/room_to_children.dart"; +import "package:nexus/models/space.dart"; + +class SpacesController extends AsyncNotifier> { + @override + Future> build() async { + final client = await ref.watch(ClientController.provider.future); + + ref.onDispose( + client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel, + ); + + final topLevel = IList( + await Future.wait( + client.rooms + .where((room) => !room.isDirectChat) + .where( + (room) => client.rooms + .where((room) => room.isSpace) + .every( + (match) => match.spaceChildren.every( + (child) => child.roomId != room.id, + ), + ), + ) + .map((room) => room.fullRoom), + ), + ); + + final topLevelSpaces = topLevel.where((r) => r.roomData.isSpace).toIList(); + final topLevelRooms = topLevel.where((r) => !r.roomData.isSpace).toIList(); + + return IList([ + Space( + client: client, + title: "Home", + id: "home", + children: topLevelRooms, + icon: Icons.home, + ), + Space( + client: client, + title: "Direct Messages", + id: "dms", + children: IList( + await Future.wait( + client.rooms + .where((room) => room.isDirectChat) + .map((room) => room.fullRoom), + ), + ), + icon: Icons.person, + ), + ...(await Future.wait( + topLevelSpaces.map( + (space) async => Space( + client: client, + title: space.title, + avatar: space.avatar, + id: space.roomData.id, + roomData: space.roomData, + children: IList(await space.roomData.getAllChildren(client)), + ), + ), + )), + ]); + } + + static final provider = AsyncNotifierProvider>( + SpacesController.new, + ); +} diff --git a/lib/controllers/sync_status.dart b/lib/controllers/sync_status.dart deleted file mode 100644 index 256c8e2..0000000 --- a/lib/controllers/sync_status.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/main.dart"; -import "package:nexus/models/sync_status.dart"; - -class SyncStatusController extends Notifier { - @override - Null build() => null; - - void set(SyncStatus newStatus) { - if (newStatus.type == .permanentlyFailed) { - showError(newStatus.error ?? "Syncing failed"); - } - state = newStatus; - } - - static final provider = NotifierProvider( - SyncStatusController.new, - ); -} diff --git a/lib/controllers/thumbnail_controller.dart b/lib/controllers/thumbnail_controller.dart new file mode 100644 index 0000000..4500523 --- /dev/null +++ b/lib/controllers/thumbnail_controller.dart @@ -0,0 +1,22 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/models/image_data.dart"; + +class ThumbnailController extends AsyncNotifier { + ThumbnailController(this.data); + final ImageData data; + + @override + Future build({String? from}) async { + final client = await ref.watch(ClientController.provider.future); + final uri = await Uri.tryParse(data.uri)?.getDownloadUri(client); + + return uri.toString(); + } + + static final provider = AsyncNotifierProvider.family + .autoDispose( + ThumbnailController.new, + ); +} diff --git a/lib/controllers/top_level_spaces.dart b/lib/controllers/top_level_spaces.dart deleted file mode 100644 index 321e29d..0000000 --- a/lib/controllers/top_level_spaces.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -class TopLevelSpacesController extends Notifier> { - @override - IList build() => .new(); - - void set(IList newSpaces) => state = newSpaces; - - static final provider = - NotifierProvider>( - TopLevelSpacesController.new, - ); -} diff --git a/lib/controllers/url_preview.dart b/lib/controllers/url_preview.dart deleted file mode 100644 index 0f807bb..0000000 --- a/lib/controllers/url_preview.dart +++ /dev/null @@ -1,54 +0,0 @@ -import "dart:convert"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:http/http.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/header.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/open_graph_data.dart"; - -class UrlPreviewController extends AsyncNotifier { - final String link; - UrlPreviewController(this.link); - - @override - Future build() async { - try { - final homeserver = ref.watch( - ClientStateController.provider.select((value) => value?.homeserverUrl), - ); - - if (homeserver != null && !link.contains("matrix.to")) { - { - final response = await get( - .parse(homeserver) - .resolve("/_matrix/client/v1/media/preview_url") - .replace(queryParameters: {"url": link}), - headers: await ref.watch(HeaderController.provider.future), - ); - - if (response.statusCode == 200) { - final decodedValue = json.decode(response.body); - if (decodedValue is! Map) return null; - - final mxc = decodedValue["og:image"]; - final image = mxc == null - ? null - : Uri.tryParse(mxc)?.mxcToHttps(homeserver); - - return .fromJson(decodedValue).copyWith(imageUrl: image); - } - } - } - } catch (_) { - return null; - } - return null; - } - - static final provider = - AsyncNotifierProvider.family< - UrlPreviewController, - OpenGraphData?, - String - >(UrlPreviewController.new); -} diff --git a/lib/controllers/user.dart b/lib/controllers/user.dart deleted file mode 100644 index b366c88..0000000 --- a/lib/controllers/user.dart +++ /dev/null @@ -1,47 +0,0 @@ -import "dart:async"; -import "package:collection/collection.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/members.dart"; -import "package:nexus/controllers/profile.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/models/configs/user.dart"; -import "package:nexus/models/content/membership.dart"; - -class UserController extends AsyncNotifier { - final UserConfig config; - UserController(this.config); - - @override - Future build() async { - final member = config.roomId == null - ? null - : await ref.watch( - MembersController.provider(config.roomId!).selectAsync( - (value) => value.firstWhereOrNull( - (membership) => membership.stateKey == config.userId, - ), - ), - ); - - if (member?.content case final MembershipContent content) { - return content; - } - - final profile = await ref.watch( - ProfileController.provider(config.userId).future, - ); - - return .new( - status: .leave, - avatarUrl: profile.avatarUrl, - displayName: profile.displayName ?? config.userId.localpart, - ); - } - - static final provider = - AsyncNotifierProvider.family< - UserController, - MembershipContent, - UserConfig - >(UserController.new); -} diff --git a/lib/controllers/via.dart b/lib/controllers/via.dart deleted file mode 100644 index d9227ba..0000000 --- a/lib/controllers/via.dart +++ /dev/null @@ -1,63 +0,0 @@ -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/content/power_levels.dart"; -import "package:nexus/models/room.dart"; - -class ViaController extends Notifier { - final Room room; - ViaController(this.room); - - @override - String build() { - final servers = {}; - - void addUserId(String? userId) { - final server = userId?.split(":").lastOrNull; - if (server != null) { - servers.add(server); - } - } - - addUserId(ref.watch(ClientStateController.provider)?.userId); - - final powerLevelsEventId = room.state[EventType.powerLevels.type]?[""]; - final powerLevels = powerLevelsEventId == null - ? null - : room.events[powerLevelsEventId]; - - if (powerLevels?.content case PowerLevelsContent(:final users)) { - for (final userId in users.keys) { - addUserId(userId); - if (servers.length >= 5) break; - } - } - - final members = room.state[EventType.membership.type]?.values.toIList(); - for (var i = 0; servers.length < 5; i++) { - final membershipEventId = members?.getOrNull(i); - final member = membershipEventId == null - ? null - : room.events[membershipEventId]; - - if (member?.content case MembershipContent(:final status)) { - if (status == .join) { - addUserId(member?.stateKey); - } - } - - if (members?.getOrNull(i) == null) break; - } - - return servers.isEmpty - ? "" - : "?${servers.map((server) => "via=$server").join("&")}"; - } - - static final provider = NotifierProvider.family( - ViaController.new, - ); -} diff --git a/lib/helpers/extensions/color_hex.dart b/lib/helpers/extensions/color_hex.dart new file mode 100644 index 0000000..3f04629 --- /dev/null +++ b/lib/helpers/extensions/color_hex.dart @@ -0,0 +1,8 @@ +import "package:flutter/widgets.dart"; + +extension ColorHex on Color { + String get hex { + final rgb = toARGB32() & 0x00FFFFFF; + return "#${rgb.toRadixString(16).padLeft(6, "0")}"; + } +} diff --git a/lib/helpers/extensions/event_to_message.dart b/lib/helpers/extensions/event_to_message.dart new file mode 100644 index 0000000..c003180 --- /dev/null +++ b/lib/helpers/extensions/event_to_message.dart @@ -0,0 +1,134 @@ +import "package:collection/collection.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:matrix/matrix.dart"; + +extension EventToMessage on Event { + Future toMessage({ + bool mustBeText = false, + bool includeEdits = false, + }) async { + final replyId = inReplyToEventId(); + + final newEvent = (unsigned?["m.relations"] as Map?)?["m.replace"]; + final event = newEvent == null ? this : Event.fromJson(newEvent, room); + + final replyEvent = replyId == null + ? null + : await room.getEventById(replyId); + + final sender = + await event.fetchSenderUser() ?? event.senderFromMemoryOrFallback; + final newContent = event.content["m.new_content"] as Map?; + final metadata = { + "formatted": + newContent?["formatted_body"] ?? + newContent?["body"] ?? + event.content["formatted_body"] ?? + event.content["body"] ?? + "", + "reply": await replyEvent?.toMessage(mustBeText: true), + "body": newContent?["body"] ?? event.content["body"], + "eventType": event.type, + "avatarUrl": sender.avatarUrl.toString(), + "displayName": sender.displayName ?? sender.id, + "txnId": transactionId, + }; + + final editedAt = event.relationshipType == RelationshipTypes.edit + ? event.originServerTs + : null; + + if ((redacted && !mustBeText) || + (!includeEdits && (relationshipType == RelationshipTypes.edit))) { + return null; + } + + // TODO: Use server-generated preview if enabled when https://github.com/famedly/matrix-dart-sdk/issues/2195 is fixed. + + // final match = Uri.tryParse( + // RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "", + // ); + + // final preview = match == null + // ? null + // : await room.client.getUrlPreview(match); + + final asText = + Message.text( + metadata: metadata, + id: eventId, + authorId: senderId, + text: redacted ? "This message has been deleted..." : event.body, + replyToMessageId: replyId, + deliveredAt: originServerTs, + editedAt: editedAt, + ) + as TextMessage; + + if (mustBeText) return asText; + + return switch (type) { + EventTypes.Encrypted => asText.copyWith( + text: "Unable to decrypt message.", + metadata: {...metadata, "formatted": "Unable to decrypt message."}, + ), + (EventTypes.Sticker || EventTypes.Message) => switch (messageType) { + (MessageTypes.Sticker || MessageTypes.Image) => Message.image( + metadata: metadata, + id: eventId, + authorId: senderId, + text: event.text, + source: (await getAttachmentUri()).toString(), + replyToMessageId: replyId, + deliveredAt: originServerTs, + ), + MessageTypes.Audio => Message.audio( + metadata: metadata, + id: eventId, + authorId: senderId, + text: event.text, + replyToMessageId: replyId, + source: (await event.getAttachmentUri()).toString(), + deliveredAt: originServerTs, + // TODO: See if we can figure out duration + duration: Duration(hours: 1), + ), + MessageTypes.File => Message.file( + name: event.content["filename"].toString(), + metadata: metadata, + id: eventId, + authorId: senderId, + source: (await event.getAttachmentUri()).toString(), + replyToMessageId: replyId, + deliveredAt: originServerTs, + ), + _ => asText, + }, + EventTypes.RoomMember => Message.system( + metadata: metadata, + id: eventId, + authorId: senderId, + text: + "${event.asUser.displayName ?? event.asUser.id} ${switch (Membership.values.firstWhereOrNull((membership) => membership.name == event.content["membership"])) { + Membership.invite => "was invited to", + Membership.join => "joined", + Membership.leave => "left", + Membership.knock => "asked to join", + Membership.ban => "was banned from", + _ => "did something relating to", + }} the room.", + ), + EventTypes.Redaction => null, + _ => + kDebugMode + ? Message.unsupported( + metadata: metadata, + id: eventId, + authorId: senderId, + replyToMessageId: replyId, + ) + : null, + }; + } +} diff --git a/lib/helpers/extensions/get_full_room.dart b/lib/helpers/extensions/get_full_room.dart new file mode 100644 index 0000000..bbd0bc5 --- /dev/null +++ b/lib/helpers/extensions/get_full_room.dart @@ -0,0 +1,13 @@ +import "package:matrix/matrix.dart"; +import "package:nexus/models/full_room.dart"; + +extension GetFullRoom on Room { + Future get fullRoom async { + await loadHeroUsers(); + return FullRoom( + roomData: this, + title: getLocalizedDisplayname(), + avatar: await avatar?.getThumbnailUri(client, width: 24, height: 24), + ); + } +} diff --git a/lib/helpers/extensions/get_headers.dart b/lib/helpers/extensions/get_headers.dart index 0fcec48..b8b1fde 100644 --- a/lib/helpers/extensions/get_headers.dart +++ b/lib/helpers/extensions/get_headers.dart @@ -1,7 +1,5 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/header.dart"; +import "package:matrix/matrix.dart"; -extension GetHeaders on WidgetRef { - Map get headers => - watch(HeaderController.provider).requireValue; +extension GetHeaders on Client { + Map get headers => {"authorization": "Bearer $accessToken"}; } diff --git a/lib/helpers/extensions/get_localpart.dart b/lib/helpers/extensions/get_localpart.dart deleted file mode 100644 index fa3d285..0000000 --- a/lib/helpers/extensions/get_localpart.dart +++ /dev/null @@ -1,3 +0,0 @@ -extension GetLocalpart on String { - String get localpart => length > 1 ? substring(1).split(":").first : "?"; -} diff --git a/lib/helpers/extensions/get_xcode_sdk.dart b/lib/helpers/extensions/get_xcode_sdk.dart deleted file mode 100644 index 4c09246..0000000 --- a/lib/helpers/extensions/get_xcode_sdk.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "dart:io"; - -Future getXCodeSDK() async { - final result = await Process.run("xcrun", ["--show-sdk-path"]); - - if (result.exitCode != 0) { - throw Exception("Failed to get XCode SDK\n${result.stderr}"); - } - - return result.stdout.trim(); -} diff --git a/lib/helpers/extensions/gomuks_buffer.dart b/lib/helpers/extensions/gomuks_buffer.dart deleted file mode 100644 index cc16b46..0000000 --- a/lib/helpers/extensions/gomuks_buffer.dart +++ /dev/null @@ -1,36 +0,0 @@ -import "dart:convert"; -import "dart:ffi"; -import "dart:typed_data"; -import "package:ffi/ffi.dart"; -import "package:nexus/src/third_party/gomuks.g.dart"; - -extension GomuksOwnedBufferToX on GomuksOwnedBuffer { - Uint8List toBytes() { - try { - if (base == nullptr || length <= 0) return .new(0); - return .fromList(base.asTypedList(length)); - } finally { - calloc.free(base); - } - } - - dynamic toJson() => jsonDecode(utf8.decode(toBytes())); -} - -extension JsonToGomuksBuffer on Map { - Pointer toGomuksBufferPtr() { - final jsonString = json.encode(this); - final bytes = utf8.encode(jsonString); - - final dataPtr = calloc(bytes.length); - dataPtr.asTypedList(bytes.length).setAll(0, bytes); - - final ptr = calloc(); - - ptr.ref - ..base = dataPtr - ..length = bytes.length; - - return ptr; - } -} diff --git a/lib/helpers/extensions/link_to_mention.dart b/lib/helpers/extensions/link_to_mention.dart deleted file mode 100644 index f4868d3..0000000 --- a/lib/helpers/extensions/link_to_mention.dart +++ /dev/null @@ -1,45 +0,0 @@ -extension LinkToMention on String { - /// Extracts a Matrix identifier from this string. - /// - /// Supports: - /// - https://matrix.to/#/... - /// - matrix:roomid/... - /// - matrix:r/... - /// - matrix:u/... - /// - /// Returns the decoded identifier (e.g. "#room:matrix.org") - /// or null if this is not a Matrix link. - String? get mention { - final trimmed = trim(); - - final matrixTo = RegExp( - r"^https?://matrix\.to/#/(.[^/?#]+)", - caseSensitive: false, - ); - - final matrixToMatch = matrixTo.firstMatch(trimmed); - if (matrixToMatch != null) { - return Uri.decodeComponent(matrixToMatch.group(1)!); - } - - if (trimmed.toLowerCase().startsWith("matrix:")) { - try { - final uri = Uri.parse(trimmed); - - if (uri.pathSegments.isNotEmpty) { - final identifier = uri.pathSegments.last; - if (identifier.isNotEmpty) { - return "${switch (uri.pathSegments.firstOrNull) { - "r" => "#", - "roomid" => "!", - "u" => "@", - _ => "", - }}${Uri.decodeComponent(identifier)}"; - } - } - } catch (_) {} - } - - return null; - } -} diff --git a/lib/helpers/extensions/list_to_messages.dart b/lib/helpers/extensions/list_to_messages.dart new file mode 100644 index 0000000..c14618b --- /dev/null +++ b/lib/helpers/extensions/list_to_messages.dart @@ -0,0 +1,9 @@ +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/helpers/extensions/event_to_message.dart"; + +extension ListToMessages on List { + Future> toMessages(Room room) async => (await Future.wait( + map((event) => Event.fromMatrixEvent(event, room).toMessage()), + )).nonNulls.toList().reversed.toList(); +} diff --git a/lib/helpers/extensions/mxc_to_https.dart b/lib/helpers/extensions/mxc_to_https.dart deleted file mode 100644 index b21f056..0000000 --- a/lib/helpers/extensions/mxc_to_https.dart +++ /dev/null @@ -1,4 +0,0 @@ -extension MxcToHttps on Uri { - Uri mxcToHttps(String homeserver) => - .parse(homeserver).resolve("_matrix/client/v1/media/download/$host$path"); -} diff --git a/lib/helpers/extensions/room_to_children.dart b/lib/helpers/extensions/room_to_children.dart new file mode 100644 index 0000000..afdc99e --- /dev/null +++ b/lib/helpers/extensions/room_to_children.dart @@ -0,0 +1,27 @@ +import "package:collection/collection.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/helpers/extensions/get_full_room.dart"; +import "package:nexus/models/full_room.dart"; + +extension RoomToChildren on Room { + Future> getAllChildren(Client client) async { + final direct = await Future.wait( + spaceChildren + .map( + (child) => client.rooms + .firstWhereOrNull((r) => r.id == child.roomId) + ?.fullRoom, + ) + .nonNulls, + ); + + return (await Future.wait( + direct.map( + (child) async => child.roomData.isSpace + ? await child.roomData.getAllChildren(client) + : [child], + ), + )).expand((list) => list).toIList(); + } +} diff --git a/lib/helpers/extensions/scheme_to_theme.dart b/lib/helpers/extensions/scheme_to_theme.dart index bf48535..e238cf9 100644 --- a/lib/helpers/extensions/scheme_to_theme.dart +++ b/lib/helpers/extensions/scheme_to_theme.dart @@ -1,36 +1,14 @@ import "package:flutter/material.dart"; extension SchemeToTheme on ColorScheme { - ThemeData get theme { - final textTheme = ThemeData( - fontFamilyFallback: ["sans", "emoji", "fallback-sans", "fallback-emoji"], - brightness: brightness, - ).textTheme; - return .from(colorScheme: this).copyWith( - cardTheme: .new(color: primaryContainer), - popupMenuTheme: .new( - shape: RoundedRectangleBorder(borderRadius: .circular(16)), - color: surfaceContainerHigh, - ), - appBarTheme: AppBarTheme( - titleSpacing: 0, - backgroundColor: surfaceContainerLow, - ), - tooltipTheme: .new( - textStyle: textTheme.labelLarge?.copyWith( - fontSize: 16, - fontWeight: .w600, - ), - padding: .all(8), - decoration: BoxDecoration( - color: surfaceContainerHighest, - borderRadius: .circular(8), - ), - ), - textTheme: textTheme, - inputDecorationTheme: const InputDecorationTheme( - border: OutlineInputBorder(), - ), - ); - } + ThemeData get theme => ThemeData.from(colorScheme: this).copyWith( + cardTheme: CardThemeData(color: primaryContainer), + appBarTheme: AppBarTheme( + titleSpacing: 0, + backgroundColor: surfaceContainerLow, + ), + inputDecorationTheme: const InputDecorationTheme( + border: OutlineInputBorder(), + ), + ); } diff --git a/lib/helpers/extensions/show_about_dialog.dart b/lib/helpers/extensions/show_about_dialog.dart deleted file mode 100644 index 2f24a3e..0000000 --- a/lib/helpers/extensions/show_about_dialog.dart +++ /dev/null @@ -1,83 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_svg/flutter_svg.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:m3e_card_list/m3e_card_list.dart"; -import "package:nexus/helpers/launch_helper.dart"; -import "package:package_info_plus/package_info_plus.dart"; - -extension ShowContextMenu on BuildContext { - Future showAboutDialog(WidgetRef ref) async { - final packageInfo = await PackageInfo.fromPlatform(); - - if (mounted) { - showDialog( - context: this, - builder: (context) => AlertDialog( - content: Column( - mainAxisSize: .min, - spacing: 16, - children: [ - Row( - spacing: 12, - children: [ - SvgPicture.asset("assets/icon.svg", width: 64), - Expanded( - child: Column( - crossAxisAlignment: .start, - children: [ - Wrap( - crossAxisAlignment: .center, - spacing: 4, - children: [ - Text( - "Nexus", - style: Theme.of(context).textTheme.headlineMedium, - ), - Text("(${packageInfo.version})"), - ], - ), - - Text( - "A simple and user-friendly Matrix client", - overflow: .ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - ), - ], - ), - M3ECardColumn( - onTap: (index) => - ref.watch(LaunchHelper.provider).launchUrl(switch (index) { - 0 => Uri.https("git.federated.nexus", "nexus/nexus"), - _ => Uri.https("liberapay.com", "QuadRadical"), - }), - children: [ - ListTile( - leading: Icon(Icons.commit), - title: Text("Source Code"), - ), - ListTile( - leading: Icon(Icons.favorite, color: Colors.pinkAccent), - title: Text("Donate"), - ), - ], - ), - ], - ), - actions: [ - TextButton( - onPressed: () => showLicensePage(context: context), - child: Text("View licenses"), - ), - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Close"), - ), - ], - ), - ); - } - } -} diff --git a/lib/helpers/extensions/show_context_menu.dart b/lib/helpers/extensions/show_context_menu.dart index c860115..f4762c3 100644 --- a/lib/helpers/extensions/show_context_menu.dart +++ b/lib/helpers/extensions/show_context_menu.dart @@ -9,13 +9,13 @@ extension ShowContextMenu on BuildContext { showMenu( context: this, - constraints: .loose(Size.infinite), - position: .fromLTRB( + position: RelativeRect.fromLTRB( globalPosition.dx, globalPosition.dy, overlay.size.width - globalPosition.dx, overlay.size.height - globalPosition.dy, ), + color: Theme.of(this).colorScheme.surfaceContainerHighest, items: children, ); } diff --git a/lib/helpers/extensions/show_user_popover.dart b/lib/helpers/extensions/show_user_popover.dart deleted file mode 100644 index 1ea3015..0000000 --- a/lib/helpers/extensions/show_user_popover.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:flutter/material.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/widgets/user_bottom_sheet.dart"; - -extension ShowUserPopover on BuildContext { - void showUserPopover( - MembershipContent member, - String userId, { - String? roomId, - }) => showModalBottomSheet( - constraints: BoxConstraints.loose( - Size(500, View.of(this).physicalSize.height - 80), - ), - isScrollControlled: true, - context: this, - builder: (context) => UserBottomSheet(member, userId, roomId: roomId), - ); -} diff --git a/lib/helpers/extensions/size_to_string.dart b/lib/helpers/extensions/size_to_string.dart deleted file mode 100644 index a9db345..0000000 --- a/lib/helpers/extensions/size_to_string.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; - -extension SizeToString on int { - String get sizeAsString { - const suffixes = IListConst(["B", "KB", "MB", "GB", "TB", "PB"]); - - var i = 0; - var size = toDouble(); - while (size > 1024 && i < suffixes.length - 1) { - size /= 1024; - i++; - } - return "${size.toStringAsFixed(2)} ${suffixes[i]}"; - } -} diff --git a/lib/helpers/extensions/string_to_color.dart b/lib/helpers/extensions/string_to_color.dart deleted file mode 100644 index eaa7714..0000000 --- a/lib/helpers/extensions/string_to_color.dart +++ /dev/null @@ -1,6 +0,0 @@ -import "package:color_hash/color_hash.dart"; -import "package:flutter/material.dart"; - -extension ToColor on String { - Color get colorHash => ColorHash(this, lightness: .5, saturation: .7).color; -} diff --git a/lib/helpers/font_licenses.dart b/lib/helpers/font_licenses.dart deleted file mode 100644 index 41567c2..0000000 --- a/lib/helpers/font_licenses.dart +++ /dev/null @@ -1,196 +0,0 @@ -import "package:flutter/foundation.dart"; - -const fontLicenses = [ - LicenseEntryWithLineBreaks( - ["Noto Color Emoji"], - """Copyright 2021 Google Inc. All Rights Reserved. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE.""", - ), - LicenseEntryWithLineBreaks( - ["Roboto"], - """Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE.""", - ), -]; diff --git a/lib/helpers/launch_helper.dart b/lib/helpers/launch_helper.dart index 575395f..f872ef7 100644 --- a/lib/helpers/launch_helper.dart +++ b/lib/helpers/launch_helper.dart @@ -10,7 +10,9 @@ class LaunchHelper { try { return await ul.launchUrl( url, - mode: useWebview ? .inAppBrowserView : .externalApplication, + mode: useWebview + ? ul.LaunchMode.inAppBrowserView + : ul.LaunchMode.externalApplication, ); } on PlatformException catch (_) { return false; diff --git a/lib/helpers/required_validator_helper.dart b/lib/helpers/required_validator_helper.dart deleted file mode 100644 index d243684..0000000 --- a/lib/helpers/required_validator_helper.dart +++ /dev/null @@ -1,2 +0,0 @@ -String? requiredValidator(String? value) => - value == null || value.isEmpty ? "This field is required" : null; diff --git a/lib/main.dart b/lib/main.dart index f50d22d..8cf4365 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,25 +1,19 @@ -import "dart:io"; -import "package:dynamic_color/dynamic_color.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/foundation.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:media_kit/media_kit.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/header.dart"; -import "package:nexus/controllers/multi_provider.dart"; -import "package:nexus/controllers/settings.dart"; -import "package:nexus/controllers/shared_prefs.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/controllers/shared_prefs_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart"; -import "package:nexus/helpers/font_licenses.dart"; -import "package:nexus/pages/chat.dart"; -import "package:nexus/pages/select_server.dart"; -import "package:nexus/pages/verify.dart"; +import "package:nexus/pages/chat_page.dart"; +import "package:nexus/pages/login_page.dart"; +import "package:nexus/pages/settings_page.dart"; +import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:window_manager/window_manager.dart"; import "package:flutter/material.dart"; +import "package:dynamic_system_colors/dynamic_system_colors.dart"; +import "package:window_size/window_size.dart"; final GlobalKey navigatorKey = GlobalKey(); @@ -34,20 +28,13 @@ Time: ${DateTime.now().toIso8601String()} Provider: ${context.provider} Previous Value: ${previousValue is AsyncData ? previousValue.value : previousValue} New Value: ${newValue is AsyncData ? newValue.value : newValue} -"""); +}"""); } void showError(Object error, [StackTrace? stackTrace]) { - if (error.toString().contains("DioException") || - error.toString().contains( - "setState() or markNeedsBuild() called during build.", - ) || - error.toString().contains("Invalid source") || - error.toString().contains("UTF-16") || - error.toString().contains("HTTP request failed") || - error.toString().contains("Invalid image data")) { - return; - } + if (error.toString().contains("DioException")) return; + if (error.toString().contains("UTF-16")) return; + if (error.toString().contains("Invalid image data")) return; debugPrintStack(stackTrace: stackTrace, label: error.toString()); if (navigatorKey.currentContext != null) { @@ -64,27 +51,19 @@ void showError(Object error, [StackTrace? stackTrace]) { void main() async { WidgetsFlutterBinding.ensureInitialized(); - MediaKit.ensureInitialized(); - if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { - await windowManager.ensureInitialized(); - await windowManager.waitUntilReadyToShow( - WindowOptions( - titleBarStyle: TitleBarStyle.hidden, - windowButtonVisibility: false, - ), - ); - await windowManager.setMinimumSize(Size.square(500)); - } - - LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses)); + await windowManager.ensureInitialized(); + await windowManager.waitUntilReadyToShow( + WindowOptions(titleBarStyle: TitleBarStyle.hidden), + ); FlutterError.onError = (FlutterErrorDetails details) => showError(details.exception.toString(), details.stack); + setWindowMinSize(const Size.square(500)); + runApp( ProviderScope( - retry: (_, _) => null, observers: [ // Change false to true if you want debug information on provider reloads // ignore: dead_code @@ -95,81 +74,60 @@ void main() async { ); } -class App extends StatelessWidget { +class App extends ConsumerWidget { const App({super.key}); @override - Widget build(BuildContext context) => DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) => Consumer( - builder: (context, ref, child) => MaterialApp( - navigatorKey: navigatorKey, - debugShowCheckedModeBanner: false, - // Use indigo to work around bugs in theme generation - theme: - (ref - .watch(SettingsController.provider) - .maybeWhen( - orElse: () => lightDynamic, - data: (settings) => - settings.useDynamicTheming ? lightDynamic : null, - ) ?? - ColorScheme.fromSeed(seedColor: Colors.indigo)) - .theme, - darkTheme: - (ref - .watch(SettingsController.provider) - .maybeWhen( - orElse: () => darkDynamic, - data: (settings) => - settings.useDynamicTheming ? darkDynamic : null, - ) ?? - ColorScheme.fromSeed( - seedColor: Colors.indigo, - brightness: Brightness.dark, - )) - .theme, - themeMode: ref - .watch(SettingsController.provider) - .maybeWhen( - data: (settings) => settings.theme, - orElse: () => ThemeMode.system, + Widget build(BuildContext context, WidgetRef ref) => DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) => MaterialApp( + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + // Use indigo to work around bugs in theme generation + theme: (lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo)) + .theme, + darkTheme: + (darkDynamic ?? + ColorScheme.fromSeed( + seedColor: Colors.indigo, + brightness: Brightness.dark, + )) + .theme, + home: Builder( + builder: (context) => ref + .watch(SharedPrefsController.provider) + .betterWhen( + data: (_) => ref + .watch(ClientController.provider) + .betterWhen( + data: (client) => + client.accessToken == null ? LoginPage() : ChatPage(), + loading: () => Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + Text( + "Syncing...", + style: Theme.of(context).textTheme.headlineMedium, + ), + Loading(), + ], + ), + ), + appBar: Appbar( + actions: [ + IconButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => SettingsPage()), + ), + icon: Icon(Icons.settings), + ), + ], + ), + ), + ), ), - home: child, - ), - child: Scaffold( - body: Consumer( - builder: (_, ref, _) => ref - .watch( - MultiProviderController.provider( - IListConst([ - SharedPrefsController.provider, - ClientController.provider, - HeaderController.provider, - ]), - ), - ) - .betterWhen( - data: (_) => Consumer( - builder: (_, ref, _) { - final clientState = ref.watch( - ClientStateController.provider, - ); - - if (clientState == null || !clientState.isInitialized) { - return Loading(); - } - - if (!clientState.isLoggedIn) { - return SelectServerPage(); - } else if (!clientState.isVerified) { - return VerifyPage(); - } else { - return ChatPage(); - } - }, - ), - ), - ), ), ), ); diff --git a/lib/models/account_data.dart b/lib/models/account_data.dart deleted file mode 100644 index a325ffe..0000000 --- a/lib/models/account_data.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "account_data.freezed.dart"; -part "account_data.g.dart"; - -@freezed -abstract class AccountData with _$AccountData { - const factory AccountData({ - required String userId, - required String? roomId, - required String type, - required dynamic content, - }) = _AccountData; - - factory AccountData.fromJson(Map json) => - _$AccountDataFromJson(json); -} diff --git a/lib/models/client_state.dart b/lib/models/client_state.dart deleted file mode 100644 index 1e15136..0000000 --- a/lib/models/client_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "client_state.freezed.dart"; -part "client_state.g.dart"; - -@freezed -abstract class ClientState with _$ClientState { - const factory ClientState({ - required bool isInitialized, - required bool isLoggedIn, - required bool isVerified, - required String? userId, - required String? homeserverUrl, - }) = _ClientState; - - factory ClientState.fromJson(Map json) => - _$ClientStateFromJson(json); -} diff --git a/lib/models/configs/members_by_status.dart b/lib/models/configs/members_by_status.dart deleted file mode 100644 index 29fc471..0000000 --- a/lib/models/configs/members_by_status.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/membership_status.dart"; -part "members_by_status.freezed.dart"; -part "members_by_status.g.dart"; - -@freezed -abstract class MembersByStatusConfig with _$MembersByStatusConfig { - const factory MembersByStatusConfig({ - required String roomId, - required MembershipStatus status, - }) = _MembersByStatusConfig; - - factory MembersByStatusConfig.fromJson(Map json) => - _$MembersByStatusConfigFromJson(json); -} diff --git a/lib/models/configs/power_level.dart b/lib/models/configs/power_level.dart deleted file mode 100644 index ed9c5e8..0000000 --- a/lib/models/configs/power_level.dart +++ /dev/null @@ -1,28 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/membership_action.dart"; -part "power_level.freezed.dart"; - -@freezed -sealed class PowerLevelConfig with _$PowerLevelConfig { - const factory PowerLevelConfig({ - required EventType eventType, - required String roomId, - }) = EventPowerLevelConfig; - - const factory PowerLevelConfig.membershipAction({ - required MembershipAction action, - required String targetUser, - required String roomId, - }) = MembershipActionPowerLevelConfig; - - const factory PowerLevelConfig.state({ - required EventType eventType, - required String roomId, - }) = StatePowerLevelConfig; - - const factory PowerLevelConfig.redaction({ - required String targetUser, - required String roomId, - }) = RedactionPowerLevelConfig; -} diff --git a/lib/models/configs/reactions.dart b/lib/models/configs/reactions.dart deleted file mode 100644 index 787b28c..0000000 --- a/lib/models/configs/reactions.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "reactions.freezed.dart"; -part "reactions.g.dart"; - -@freezed -abstract class ReactionsConfig with _$ReactionsConfig { - const factory ReactionsConfig({ - required String roomId, - required int eventRowId, - }) = _ReactionsConfig; - - factory ReactionsConfig.fromJson(Map json) => - _$ReactionsConfigFromJson(json); -} diff --git a/lib/models/configs/user.dart b/lib/models/configs/user.dart deleted file mode 100644 index 0331597..0000000 --- a/lib/models/configs/user.dart +++ /dev/null @@ -1,12 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "user.freezed.dart"; -part "user.g.dart"; - -@freezed -abstract class UserConfig with _$UserConfig { - const factory UserConfig({required String? roomId, required String userId}) = - _UserConfig; - - factory UserConfig.fromJson(Map json) => - _$UserConfigFromJson(json); -} diff --git a/lib/models/content/avatar.dart b/lib/models/content/avatar.dart deleted file mode 100644 index 66d4c47..0000000 --- a/lib/models/content/avatar.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/info/image.dart"; -part "avatar.freezed.dart"; -part "avatar.g.dart"; - -@freezed -abstract class AvatarContent extends Content with _$AvatarContent { - AvatarContent._(); - factory AvatarContent({ImageInfo? info, Uri? url}) = _AvatarContent; - - factory AvatarContent.fromJson(Map json) => - _$AvatarContentFromJson(json); -} diff --git a/lib/models/content/canonical_alias.dart b/lib/models/content/canonical_alias.dart deleted file mode 100644 index 636be13..0000000 --- a/lib/models/content/canonical_alias.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "canonical_alias.freezed.dart"; -part "canonical_alias.g.dart"; - -@freezed -abstract class CanonicalAliasContent extends Content - with _$CanonicalAliasContent { - CanonicalAliasContent._(); - factory CanonicalAliasContent({ - String? alias, - @Default(ISet.empty()) ISet altAliases, - }) = _CanonicalAliasContent; - - factory CanonicalAliasContent.fromJson(Map json) => - _$CanonicalAliasContentFromJson(json); -} diff --git a/lib/models/content/content.dart b/lib/models/content/content.dart deleted file mode 100644 index e7b1141..0000000 --- a/lib/models/content/content.dart +++ /dev/null @@ -1,68 +0,0 @@ -import "package:collection/collection.dart"; -import "package:nexus/models/content/avatar.dart"; -import "package:nexus/models/content/canonical_alias.dart"; -import "package:nexus/models/content/create.dart"; -import "package:nexus/models/content/encryption.dart"; -import "package:nexus/models/content/join_rules.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/content/name.dart"; -import "package:nexus/models/content/pinned_events.dart"; -import "package:nexus/models/content/power_levels.dart"; -import "package:nexus/models/content/reaction.dart"; -import "package:nexus/models/content/encrypted.dart"; -import "package:nexus/models/content/redaction.dart"; -import "package:nexus/models/content/server_acl.dart"; -import "package:nexus/models/content/topic.dart"; -import "package:nexus/models/content/sticker.dart"; -import "package:nexus/models/content/history_visibility.dart"; - -class Content { - final Error? parseError; - Content({this.parseError}); - - factory Content.fromJson(Map json) => Content(); - Map toJson() => {}; - - static Map readValue(Map json, _) => - json["decrypted"] ?? json["content"]; - - static Content fromEventJson(Map json, String type) { - try { - return (EventType.values - .firstWhereOrNull((eventType) => eventType.type == type) - ?.contentFromJson ?? - Content.fromJson)(json); - } catch (error) { - if (error is Error) return .new(parseError: error); - rethrow; - } - } -} - -enum EventType { - encrypted("m.room.encrypted", EncryptedContent.fromJson), - redaction("m.room.redaction", RedactionContent.fromJson), - encryption("m.room.encryption", EncryptionContent.fromJson), - membership("m.room.member", MembershipContent.fromJson), - create("m.room.create", CreateContent.fromJson), - historyVisibility( - "m.room.history_visibility", - HistoryVisibilityContent.fromJson, - ), - canonicalAlias("m.room.canonical_alias", CanonicalAliasContent.fromJson), - sticker("m.sticker", StickerContent.fromJson), - joinRules("m.room.join_rules", JoinRulesContent.fromJson), - powerLevels("m.room.power_levels", PowerLevelsContent.fromJson), - serverACL("m.room.server_acl", ServerACLContent.fromJson), - avatar("m.room.avatar", AvatarContent.fromJson), - topic("m.room.topic", TopicContent.fromJson), - name("m.room.name", NameContent.fromJson), - reaction("m.reaction", ReactionContent.fromJson), - pinnedEvents("m.room.pinned_events", PinnedEventsContent.fromJson), - message("m.room.message", MessageContent.fromJson); - - final String type; - final Content Function(Map json) contentFromJson; - const EventType(this.type, this.contentFromJson); -} diff --git a/lib/models/content/create.dart b/lib/models/content/create.dart deleted file mode 100644 index c534558..0000000 --- a/lib/models/content/create.dart +++ /dev/null @@ -1,39 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "create.freezed.dart"; -part "create.g.dart"; - -@freezed -abstract class CreateContent extends Content with _$CreateContent { - CreateContent._(); - factory CreateContent({ - @JsonKey(name: "additional_creators") - @Default(IList.empty()) - IList additionalCreatorIds, - - PreviousRoom? predecessor, - - @JsonKey(name: "m.federate") @Default(true) bool federated, - - @Default("1") String roomVersion, - @JsonKey(unknownEnumValue: RoomType.room) RoomType? type, - }) = _CreateContent; - - factory CreateContent.fromJson(Map json) => - _$CreateContentFromJson(json); -} - -enum RoomType { - room, - @JsonValue("m.space") - space, -} - -@freezed -abstract class PreviousRoom with _$PreviousRoom { - const factory PreviousRoom({required String roomId}) = _PreviousRoom; - - factory PreviousRoom.fromJson(Map json) => - _$PreviousRoomFromJson(json); -} diff --git a/lib/models/content/encrypted.dart b/lib/models/content/encrypted.dart deleted file mode 100644 index b33a440..0000000 --- a/lib/models/content/encrypted.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "encrypted.freezed.dart"; -part "encrypted.g.dart"; - -@freezed -abstract class EncryptedContent extends Content with _$EncryptedContent { - EncryptedContent._(); - factory EncryptedContent() = _EncryptedContent; - - factory EncryptedContent.fromJson(Map json) => - _$EncryptedContentFromJson(json); -} diff --git a/lib/models/content/encryption.dart b/lib/models/content/encryption.dart deleted file mode 100644 index 3380632..0000000 --- a/lib/models/content/encryption.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "encryption.freezed.dart"; -part "encryption.g.dart"; - -@freezed -abstract class EncryptionContent extends Content with _$EncryptionContent { - EncryptionContent._(); - factory EncryptionContent({ - required String algorithm, - - @JsonKey(name: "rotation_period_ms") - @Default(604800000) - int rotationPeriodMS, - - @JsonKey(name: "rotation_period_msgs") - @Default(100) - int rotationPeriodMessages, - }) = _EncryptionContent; - - factory EncryptionContent.fromJson(Map json) => - _$EncryptionContentFromJson(json); -} diff --git a/lib/models/content/history_visibility.dart b/lib/models/content/history_visibility.dart deleted file mode 100644 index 707805c..0000000 --- a/lib/models/content/history_visibility.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "history_visibility.freezed.dart"; -part "history_visibility.g.dart"; - -@freezed -abstract class HistoryVisibilityContent extends Content - with _$HistoryVisibilityContent { - HistoryVisibilityContent._(); - factory HistoryVisibilityContent({ - required HistoryVisibility historyVisibility, - }) = _HistoryVisibilityContent; - - factory HistoryVisibilityContent.fromJson(Map json) => - _$HistoryVisibilityContentFromJson(json); -} - -@JsonEnum(fieldRename: FieldRename.snake) -enum HistoryVisibility { invited, joined, shared, worldReadable } diff --git a/lib/models/content/join_rules.dart b/lib/models/content/join_rules.dart deleted file mode 100644 index 1d14eee..0000000 --- a/lib/models/content/join_rules.dart +++ /dev/null @@ -1,34 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/join_rule.dart"; -part "join_rules.freezed.dart"; -part "join_rules.g.dart"; - -@freezed -abstract class JoinRulesContent extends Content with _$JoinRulesContent { - JoinRulesContent._(); - factory JoinRulesContent({ - required JoinRule joinRule, - @Default(IList.empty()) IList allow, - }) = _JoinRulesContent; - - factory JoinRulesContent.fromJson(Map json) => - _$JoinRulesContentFromJson(json); -} - -@freezed -abstract class AllowCondition with _$AllowCondition { - const factory AllowCondition({ - String? roomId, - required AllowConditionType type, - }) = _AllowCondition; - - factory AllowCondition.fromJson(Map json) => - _$AllowConditionFromJson(json); -} - -enum AllowConditionType { - @JsonValue("m.room_membership") - membership, -} diff --git a/lib/models/content/membership.dart b/lib/models/content/membership.dart deleted file mode 100644 index dbbd123..0000000 --- a/lib/models/content/membership.dart +++ /dev/null @@ -1,27 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/membership_status.dart"; -part "membership.freezed.dart"; -part "membership.g.dart"; - -@freezed -abstract class MembershipContent extends Content with _$MembershipContent { - MembershipContent._(); - - static String? displaynameFromJson(String? displayName) => - displayName?.isEmpty == true ? null : displayName; - - factory MembershipContent({ - @JsonKey( - name: "displayname", - fromJson: MembershipContent.displaynameFromJson, - ) - required String? displayName, - @JsonKey(name: "membership") required MembershipStatus status, - Uri? avatarUrl, - String? reason, - }) = _MembershipContent; - - factory MembershipContent.fromJson(Map json) => - _$MembershipContentFromJson(json); -} diff --git a/lib/models/content/message.dart b/lib/models/content/message.dart deleted file mode 100644 index acbe70e..0000000 --- a/lib/models/content/message.dart +++ /dev/null @@ -1,94 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/info/audio.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/info/file.dart"; -import "package:nexus/models/info/image.dart"; -import "package:nexus/models/info/video.dart"; -part "message.freezed.dart"; -part "message.g.dart"; - -typedef EncryptedFile = Map; - -@Freezed(unionKey: "msgtype", fallbackUnion: "default") -abstract class MessageContent extends Content with _$MessageContent { - MessageContent._(); - factory MessageContent({String? body}) = UnknownMessageContent; - - @FreezedUnionValue("m.text") - factory MessageContent.text({ - required String body, - MessageFormat? format, - String? formattedBody, - }) = TextMessageContent; - - @FreezedUnionValue("m.notice") - factory MessageContent.notice({ - required String body, - MessageFormat? format, - String? formattedBody, - }) = NoticeMessageContent; - - @FreezedUnionValue("m.emote") - factory MessageContent.emote({ - required String body, - MessageFormat? format, - String? formattedBody, - }) = EmoteMessageContent; - - @FreezedUnionValue("m.image") - factory MessageContent.image({ - required String body, - MessageFormat? format, - String? formattedBody, - EncryptedFile? file, - String? filename, - ImageInfo? info, - Uri? url, - }) = ImageMessageContent; - - @FreezedUnionValue("m.file") - factory MessageContent.file({ - required String body, - MessageFormat? format, - String? formattedBody, - EncryptedFile? file, - String? filename, - FileInfo? info, - Uri? url, - }) = FileMessageContent; - - @FreezedUnionValue("m.audio") - factory MessageContent.audio({ - required String body, - MessageFormat? format, - String? formattedBody, - EncryptedFile? file, - String? filename, - AudioInfo? info, - Uri? url, - }) = AudioMessageContent; - - @FreezedUnionValue("m.video") - factory MessageContent.video({ - required String body, - MessageFormat? format, - String? formattedBody, - EncryptedFile? file, - String? filename, - VideoInfo? info, - Uri? url, - }) = VideoMessageContent; - - @FreezedUnionValue("m.location") - factory MessageContent.location({required String body, required Uri geoUri}) = - LocationMessageContent; - - factory MessageContent.fromJson(Map json) => - _$MessageContentFromJson(json); -} - -@JsonEnum() -enum MessageFormat { - @JsonValue("org.matrix.custom.html") - html, -} diff --git a/lib/models/content/name.dart b/lib/models/content/name.dart deleted file mode 100644 index 205f6bb..0000000 --- a/lib/models/content/name.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "name.freezed.dart"; -part "name.g.dart"; - -@freezed -abstract class NameContent extends Content with _$NameContent { - NameContent._(); - factory NameContent({required String name}) = _NameContent; - - factory NameContent.fromJson(Map json) => - _$NameContentFromJson(json); -} diff --git a/lib/models/content/pinned_events.dart b/lib/models/content/pinned_events.dart deleted file mode 100644 index 8aea838..0000000 --- a/lib/models/content/pinned_events.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "pinned_events.freezed.dart"; -part "pinned_events.g.dart"; - -@freezed -abstract class PinnedEventsContent extends Content with _$PinnedEventsContent { - PinnedEventsContent._(); - factory PinnedEventsContent({ - @Default(IList.empty()) @JsonKey(name: "pinned") IList pinnedEvents, - }) = _PinnedEventsContent; - - factory PinnedEventsContent.fromJson(Map json) => - _$PinnedEventsContentFromJson(json); -} diff --git a/lib/models/content/power_levels.dart b/lib/models/content/power_levels.dart deleted file mode 100644 index 3709c38..0000000 --- a/lib/models/content/power_levels.dart +++ /dev/null @@ -1,36 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "power_levels.freezed.dart"; -part "power_levels.g.dart"; - -@freezed -abstract class PowerLevelsContent extends Content with _$PowerLevelsContent { - PowerLevelsContent._(); - factory PowerLevelsContent({ - @Default(IMap.empty()) IMap events, - @Default(IMap.empty()) IMap users, - Notifications? notifications, - @Default(50) int ban, - @Default(0) int eventsDefault, - @Default(0) int invite, - @Default(50) int kick, - @Default(50) int redact, - @Default(50) int stateDefault, - @Default(0) int usersDefault, - }) = _PowerLevelsContent; - - factory PowerLevelsContent.fromJson(Map json) => - _$PowerLevelsContentFromJson(json); -} - -@freezed -abstract class Notifications with _$Notifications { - const factory Notifications({ - @Default(50) int room, - @Default(IMapConst({})) IMap other, - }) = _Notifications; - - factory Notifications.fromJson(Map json) => - _$NotificationsFromJson(json); -} diff --git a/lib/models/content/reaction.dart b/lib/models/content/reaction.dart deleted file mode 100644 index 0f81bc0..0000000 --- a/lib/models/content/reaction.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "reaction.freezed.dart"; -part "reaction.g.dart"; - -@Freezed(toJson: false) -abstract class ReactionContent extends Content with _$ReactionContent { - ReactionContent._(); - static String? keyJsonFromJson(Map json, String key) => - json["m.relates_to"]?["key"]; - - factory ReactionContent({ - @JsonKey(readValue: ReactionContent.keyJsonFromJson) String? key, - }) = _ReactionContent; - - @override - Map toJson() => { - "m.relates_to": {"key": key}, - }; - - factory ReactionContent.fromJson(Map json) => - _$ReactionContentFromJson(json); -} diff --git a/lib/models/content/redaction.dart b/lib/models/content/redaction.dart deleted file mode 100644 index e9c1a90..0000000 --- a/lib/models/content/redaction.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "redaction.freezed.dart"; -part "redaction.g.dart"; - -@freezed -abstract class RedactionContent extends Content with _$RedactionContent { - RedactionContent._(); - factory RedactionContent({String? reason, String? redacts}) = - _RedactionContent; - - factory RedactionContent.fromJson(Map json) => - _$RedactionContentFromJson(json); -} diff --git a/lib/models/content/server_acl.dart b/lib/models/content/server_acl.dart deleted file mode 100644 index 1e50988..0000000 --- a/lib/models/content/server_acl.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "server_acl.freezed.dart"; -part "server_acl.g.dart"; - -@freezed -abstract class ServerACLContent extends Content with _$ServerACLContent { - ServerACLContent._(); - factory ServerACLContent({ - @Default(IList.empty()) IList allow, - @Default(IList.empty()) IList deny, - @Default(true) allowIpLiterals, - }) = _ServerACLContent; - - factory ServerACLContent.fromJson(Map json) => - _$ServerACLContentFromJson(json); -} diff --git a/lib/models/content/sticker.dart b/lib/models/content/sticker.dart deleted file mode 100644 index 89d9332..0000000 --- a/lib/models/content/sticker.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/info/image.dart"; -part "sticker.freezed.dart"; -part "sticker.g.dart"; - -@freezed -abstract class StickerContent extends Content with _$StickerContent { - StickerContent._(); - factory StickerContent({ - required String body, - required ImageInfo info, - required Uri url, - }) = _StickerContent; - - factory StickerContent.fromJson(Map json) => - _$StickerContentFromJson(json); -} diff --git a/lib/models/content/topic.dart b/lib/models/content/topic.dart deleted file mode 100644 index 8fa5229..0000000 --- a/lib/models/content/topic.dart +++ /dev/null @@ -1,40 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "topic.freezed.dart"; -part "topic.g.dart"; - -@freezed -abstract class TopicContent extends Content with _$TopicContent { - TopicContent._(); - factory TopicContent({ - required String topic, - @JsonKey(name: "m.topic") TopicContentBlock? content, - }) = _TopicContent; - - factory TopicContent.fromJson(Map json) => - _$TopicContentFromJson(json); -} - -@freezed -abstract class TopicContentBlock with _$TopicContentBlock { - factory TopicContentBlock({ - @Default(IList.empty()) - @JsonKey(name: "m.text") - IList representations, - }) = _TopicContentBlock; - - factory TopicContentBlock.fromJson(Map json) => - _$TopicContentBlockFromJson(json); -} - -@freezed -abstract class TextualRepresentation with _$TextualRepresentation { - factory TextualRepresentation({ - required String body, - @Default("text/plain") String mimetype, - }) = _TextualRepresentation; - - factory TextualRepresentation.fromJson(Map json) => - _$TextualRepresentationFromJson(json); -} diff --git a/lib/models/emoji.dart b/lib/models/emoji.dart deleted file mode 100644 index 8e4eac6..0000000 --- a/lib/models/emoji.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "emoji.freezed.dart"; -part "emoji.g.dart"; - -@freezed -abstract class Emoji with _$Emoji { - const factory Emoji({ - required String emoji, - required String category, - required IList aliases, - required String description, - required IList tags, - }) = _Emoji; - - factory Emoji.fromJson(Map json) => _$EmojiFromJson(json); -} diff --git a/lib/models/epoch_date_time_converter.dart b/lib/models/epoch_date_time_converter.dart deleted file mode 100644 index c26d020..0000000 --- a/lib/models/epoch_date_time_converter.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -class EpochDateTimeConverter implements JsonConverter { - const EpochDateTimeConverter(); - - @override - DateTime fromJson(int json) => DateTime.fromMillisecondsSinceEpoch(json); - - @override - int toJson(DateTime object) => object.millisecondsSinceEpoch; -} diff --git a/lib/models/event.dart b/lib/models/event.dart deleted file mode 100644 index c54dbc5..0000000 --- a/lib/models/event.dart +++ /dev/null @@ -1,112 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/epoch_date_time_converter.dart"; -import "package:nexus/models/profile.dart"; -part "event.freezed.dart"; -part "event.g.dart"; - -@freezed -abstract class Event with _$Event { - static String typeJsonFromJson(Map json, _) => - json["decrypted_type"] ?? json["type"]; - - static Map getContentFromJson(Map json) { - final content = json["decrypted"] ?? json["content"]; - - return content["m.new_content"] ?? content; - } - - const factory Event({ - @JsonKey(name: "rowid") required int rowId, - @JsonKey(name: "timeline_rowid") required int timelineRowId, - required String roomId, - required String eventId, - required String sender, - @JsonKey(readValue: Event.typeJsonFromJson) required String type, - String? stateKey, - @EpochDateTimeConverter() required DateTime timestamp, - @Default(IMap.empty()) IMap unsigned, - LocalContent? localContent, - String? transactionId, - String? redactedBy, - String? relatesTo, - String? relationType, - String? replyTo, - String? decryptionError, - String? sendError, - @Default(IMap.empty()) IMap reactions, - @JsonKey(name: "last_edit_rowid") @Default(0) int lastEditRowId, - @UnreadTypeConverter() UnreadType? unreadType, - Profile? pmp, - required Content content, - required Content? previousContent, - }) = _Event; - - factory Event.fromJson(Map json) => - _$EventFromJson(json).copyWith( - replyTo: getContentFromJson( - json, - )["m.relates_to"]?["m.in_reply_to"]?["event_id"], - pmp: json["content"]?["com.beeper.per_message_profile"] == null - ? null - : Profile.fromJsonWithCatch( - json["content"]?["com.beeper.per_message_profile"], - ), - content: Content.fromEventJson( - getContentFromJson(json), - json["decrypted_type"] ?? json["type"], - ), - previousContent: json["unsigned"]?["prev_content"] == null - ? null - : Content.fromEventJson( - json["unsigned"]?["prev_content"], - json["decrypted_type"] ?? json["type"], - ), - ); -} - -@freezed -abstract class LocalContent with _$LocalContent { - const factory LocalContent({ - String? sanitizedHtml, - String? editSource, - bool? wasPlaintext, - bool? bigEmoji, - bool? hasMath, - bool? replyFallbackRemoved, - }) = _LocalContent; - - factory LocalContent.fromJson(Map json) => - _$LocalContentFromJson(json); -} - -class UnreadTypeConverter implements JsonConverter { - const UnreadTypeConverter(); - - @override - UnreadType? fromJson(int? json) => json == null ? null : UnreadType(json); - - @override - int? toJson(UnreadType? object) => object?.value; -} - -// I think this is correct but I'm not sure, its some type of bitmask. -@immutable -class UnreadType { - final int value; - - const UnreadType(this.value); - - static const none = UnreadType(0); - static const normal = UnreadType(1); - static const notify = UnreadType(2); - static const highlight = UnreadType(4); - static const sound = UnreadType(8); - - bool get isNone => value == 0; - bool get isNormal => (value & 1) != 0; - bool get shouldNotify => (value & 2) != 0; - bool get isHighlighted => (value & 4) != 0; - bool get playsSound => (value & 8) != 0; -} diff --git a/lib/models/full_room.dart b/lib/models/full_room.dart new file mode 100644 index 0000000..ee61da6 --- /dev/null +++ b/lib/models/full_room.dart @@ -0,0 +1,13 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:matrix/matrix.dart"; +part "full_room.freezed.dart"; + +@freezed +abstract class FullRoom with _$FullRoom { + const FullRoom._(); + const factory FullRoom({ + required Room roomData, + required String title, + required Uri? avatar, + }) = _FullRoom; +} diff --git a/lib/models/image_data.dart b/lib/models/image_data.dart new file mode 100644 index 0000000..e5bc57e --- /dev/null +++ b/lib/models/image_data.dart @@ -0,0 +1,11 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "image_data.freezed.dart"; + +@freezed +abstract class ImageData with _$ImageData { + const factory ImageData({ + required String uri, + required int? height, + required int? width, + }) = _ImageData; +} diff --git a/lib/models/info/audio.dart b/lib/models/info/audio.dart deleted file mode 100644 index ccfcf7a..0000000 --- a/lib/models/info/audio.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/ms_duration.dart"; -part "audio.freezed.dart"; -part "audio.g.dart"; - -@freezed -abstract class AudioInfo with _$AudioInfo { - /// Information for images, [size] is in bytes. - const factory AudioInfo({ - @MSDuration() Duration? duration, - @JsonKey(name: "mimetype") String? mimeType, - int? size, - }) = _AudioInfo; - - factory AudioInfo.fromJson(Map json) => - _$AudioInfoFromJson(json); -} diff --git a/lib/models/info/file.dart b/lib/models/info/file.dart deleted file mode 100644 index 1509c99..0000000 --- a/lib/models/info/file.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "file.freezed.dart"; -part "file.g.dart"; - -@freezed -abstract class FileInfo with _$FileInfo { - /// Information for images, [size] is in bytes. - const factory FileInfo({ - @JsonKey(name: "mimetype") String? mimeType, - int? size, - }) = _FileInfo; - - factory FileInfo.fromJson(Map json) => - _$FileInfoFromJson(json); -} diff --git a/lib/models/info/image.dart b/lib/models/info/image.dart deleted file mode 100644 index 9833016..0000000 --- a/lib/models/info/image.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "image.freezed.dart"; -part "image.g.dart"; - -@freezed -abstract class ImageInfo with _$ImageInfo { - /// Information for images, [size] is in bytes. - const factory ImageInfo({ - @JsonKey(name: "h") double? height, - @JsonKey(name: "w") double? width, - @JsonKey(name: "mimetype") String? mimeType, - @JsonKey(name: "xyz.amorgan.blurhash") String? blurHash, - int? size, - }) = _ImageInfo; - - factory ImageInfo.fromJson(Map json) => - _$ImageInfoFromJson(json); -} diff --git a/lib/models/info/video.dart b/lib/models/info/video.dart deleted file mode 100644 index 6ff3547..0000000 --- a/lib/models/info/video.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/ms_duration.dart"; -part "video.freezed.dart"; -part "video.g.dart"; - -@freezed -abstract class VideoInfo with _$VideoInfo { - /// Information for images, [size] is in bytes. - const factory VideoInfo({ - @JsonKey(name: "h") int? height, - @JsonKey(name: "w") int? width, - @JsonKey(name: "mimetype") String? mimeType, - @MSDuration() Duration? duration, - int? size, - }) = _VideoInfo; - - factory VideoInfo.fromJson(Map json) => - _$VideoInfoFromJson(json); -} diff --git a/lib/models/join_rule.dart b/lib/models/join_rule.dart deleted file mode 100644 index 3fade23..0000000 --- a/lib/models/join_rule.dart +++ /dev/null @@ -1,4 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -@JsonEnum(fieldRename: FieldRename.snake) -enum JoinRule { public, knock, invite, private, restricted, knockRestricted } diff --git a/lib/models/lazy_load_summary.dart b/lib/models/lazy_load_summary.dart deleted file mode 100644 index 0cd250f..0000000 --- a/lib/models/lazy_load_summary.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "lazy_load_summary.freezed.dart"; -part "lazy_load_summary.g.dart"; - -@freezed -abstract class LazyLoadSummary with _$LazyLoadSummary { - const factory LazyLoadSummary({ - required IList? heroes, - required int? joinedMemberCount, - required int? invitedMemberCount, - }) = _LazyLoadSummary; - - factory LazyLoadSummary.fromJson(Map json) => - _$LazyLoadSummaryFromJson(json); -} diff --git a/lib/models/membership_action.dart b/lib/models/membership_action.dart deleted file mode 100644 index d852164..0000000 --- a/lib/models/membership_action.dart +++ /dev/null @@ -1,4 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -@JsonEnum() -enum MembershipAction { ban, kick, unban, invite } diff --git a/lib/models/membership_status.dart b/lib/models/membership_status.dart deleted file mode 100644 index ba7a241..0000000 --- a/lib/models/membership_status.dart +++ /dev/null @@ -1,4 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -@JsonEnum() -enum MembershipStatus { leave, invite, ban, join, knock } diff --git a/lib/models/ms_duration.dart b/lib/models/ms_duration.dart deleted file mode 100644 index de12943..0000000 --- a/lib/models/ms_duration.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -class MSDuration implements JsonConverter { - const MSDuration(); - - @override - Duration fromJson(int ms) => Duration(milliseconds: ms); - - @override - int toJson(Duration duration) => duration.inMilliseconds; -} diff --git a/lib/models/oauth_auth_code_response.dart b/lib/models/oauth_auth_code_response.dart deleted file mode 100644 index 5bb3f3f..0000000 --- a/lib/models/oauth_auth_code_response.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "oauth_auth_code_response.freezed.dart"; -part "oauth_auth_code_response.g.dart"; - -@freezed -abstract class OAuthAuthCodeResponse with _$OAuthAuthCodeResponse { - const factory OAuthAuthCodeResponse({ - required String state, - required String codeVerifier, - required Uri url, - }) = _OAuthAuthCodeResponse; - - factory OAuthAuthCodeResponse.fromJson(Map json) => - _$OAuthAuthCodeResponseFromJson(json); -} diff --git a/lib/models/open_graph_data.dart b/lib/models/open_graph_data.dart deleted file mode 100644 index d7e840d..0000000 --- a/lib/models/open_graph_data.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "open_graph_data.freezed.dart"; -part "open_graph_data.g.dart"; - -@freezed -abstract class OpenGraphData with _$OpenGraphData { - const factory OpenGraphData({ - @JsonKey(name: "og:title") required String? title, - @JsonKey(name: "og:description") required String? description, - @JsonKey(name: "og:image") required Uri? imageUrl, - @JsonKey(name: "og:image:width") required double? width, - @JsonKey(name: "og:image:height") required double? height, - }) = _OpenGraphData; - - factory OpenGraphData.fromJson(Map json) => - _$OpenGraphDataFromJson(json); -} diff --git a/lib/models/paginate.dart b/lib/models/paginate.dart deleted file mode 100644 index df0a0f6..0000000 --- a/lib/models/paginate.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/event.dart"; -part "paginate.freezed.dart"; -part "paginate.g.dart"; - -@freezed -abstract class Paginate with _$Paginate { - const factory Paginate({ - required IList events, - required IList relatedEvents, - required bool hasMore, - }) = _Paginate; - - factory Paginate.fromJson(Map json) => - _$PaginateFromJson(json); -} diff --git a/lib/models/profile.dart b/lib/models/profile.dart deleted file mode 100644 index 6ae1e94..0000000 --- a/lib/models/profile.dart +++ /dev/null @@ -1,52 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/membership.dart"; -part "profile.freezed.dart"; -part "profile.g.dart"; - -@freezed -abstract class Profile with _$Profile { - static Object? readPronouns(Map map, _) => - map["m.pronouns"] ?? map["io.fsky.nyx.pronouns"]; - - static Object? readTimezone(Map map, _) => - map["m.tz"] ?? map["us.cloke.msc4175.tz"]; - - const factory Profile({ - required String id, - String? parseError, - Uri? avatarUrl, - - @JsonKey( - name: "displayname", - fromJson: MembershipContent.displaynameFromJson, - ) - String? displayName, - - @JsonKey(readValue: Profile.readTimezone, name: "m.tz") String? timezone, - - @Default(IList.empty()) - @JsonKey(readValue: Profile.readPronouns, name: "io.fsky.nyx.pronouns") - IList pronouns, - }) = _Profile; - - factory Profile.fromJson(Map json) => - _$ProfileFromJson(json); - - factory Profile.fromJsonWithCatch(Map json) { - try { - return Profile.fromJson(json); - } catch (error) { - return Profile(id: json["id"], parseError: error.toString()); - } - } -} - -@freezed -abstract class Pronoun with _$Pronoun { - const factory Pronoun({required String language, required String summary}) = - _Pronoun; - - factory Pronoun.fromJson(Map json) => - _$PronounFromJson(json); -} diff --git a/lib/models/read_receipt.dart b/lib/models/read_receipt.dart deleted file mode 100644 index d533e2d..0000000 --- a/lib/models/read_receipt.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/epoch_date_time_converter.dart"; -part "read_receipt.freezed.dart"; -part "read_receipt.g.dart"; - -@freezed -abstract class ReadReceipt with _$ReadReceipt { - const factory ReadReceipt({ - String? roomId, - required String userId, - String? threadId, - required String eventId, - @EpochDateTimeConverter() required DateTime timestamp, - }) = _ReadReceipt; - - factory ReadReceipt.fromJson(Map json) => - _$ReadReceiptFromJson(json); -} diff --git a/lib/models/requests/get_event.dart b/lib/models/requests/get_event.dart deleted file mode 100644 index 2665a2a..0000000 --- a/lib/models/requests/get_event.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "get_event.freezed.dart"; -part "get_event.g.dart"; - -@freezed -abstract class GetEventRequest with _$GetEventRequest { - const factory GetEventRequest({ - required String roomId, - required String eventId, - @Default(false) bool unredact, - }) = _GetEventRequest; - - factory GetEventRequest.fromJson(Map json) => - _$GetEventRequestFromJson(json); -} diff --git a/lib/models/requests/get_related_events.dart b/lib/models/requests/get_related_events.dart deleted file mode 100644 index 52d2716..0000000 --- a/lib/models/requests/get_related_events.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "get_related_events.freezed.dart"; -part "get_related_events.g.dart"; - -@freezed -abstract class GetRelatedEventsRequest with _$GetRelatedEventsRequest { - const factory GetRelatedEventsRequest({ - required String roomId, - required String eventId, - required String relationType, - }) = _GetRelatedEventsRequest; - - factory GetRelatedEventsRequest.fromJson(Map json) => - _$GetRelatedEventsRequestFromJson(json); -} diff --git a/lib/models/requests/get_room_state.dart b/lib/models/requests/get_room_state.dart deleted file mode 100644 index d3f52f7..0000000 --- a/lib/models/requests/get_room_state.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "get_room_state.freezed.dart"; -part "get_room_state.g.dart"; - -@freezed -abstract class GetRoomStateRequest with _$GetRoomStateRequest { - const factory GetRoomStateRequest({ - required String roomId, - @Default(false) bool refetch, - @Default(false) bool fetchMembers, - @Default(false) bool includeMembers, - }) = _GetRoomStateRequest; - - factory GetRoomStateRequest.fromJson(Map json) => - _$GetRoomStateRequestFromJson(json); -} diff --git a/lib/models/requests/join_room.dart b/lib/models/requests/join_room.dart deleted file mode 100644 index 8487298..0000000 --- a/lib/models/requests/join_room.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "join_room.freezed.dart"; -part "join_room.g.dart"; - -@freezed -abstract class JoinRoomRequest with _$JoinRoomRequest { - const factory JoinRoomRequest({ - required String roomIdOrAlias, - required IList via, - }) = _JoinRoomRequest; - - factory JoinRoomRequest.fromJson(Map json) => - _$JoinRoomRequestFromJson(json); -} diff --git a/lib/models/requests/oauth/exchange_token.dart b/lib/models/requests/oauth/exchange_token.dart deleted file mode 100644 index 9d55c1f..0000000 --- a/lib/models/requests/oauth/exchange_token.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "exchange_token.freezed.dart"; -part "exchange_token.g.dart"; - -@freezed -abstract class OAuthExchangeTokenRequest with _$OAuthExchangeTokenRequest { - const factory OAuthExchangeTokenRequest({ - required Uri homeserverUrl, - required String codeVerifier, - required Uri redirectUri, - required String code, - required String clientId, - }) = _OAuthExchangeTokenRequest; - - factory OAuthExchangeTokenRequest.fromJson(Map json) => - _$OAuthExchangeTokenRequestFromJson(json); -} diff --git a/lib/models/requests/oauth/get_auth_url.dart b/lib/models/requests/oauth/get_auth_url.dart deleted file mode 100644 index 5ca5b6f..0000000 --- a/lib/models/requests/oauth/get_auth_url.dart +++ /dev/null @@ -1,43 +0,0 @@ -import "dart:math"; - -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "get_auth_url.freezed.dart"; -part "get_auth_url.g.dart"; - -@freezed -abstract class OAuthGetAuthUrl with _$OAuthGetAuthUrl { - const factory OAuthGetAuthUrl({ - required ResponseMode responseMode, - required Uri homeserverUrl, - required Uri redirectUri, - required IList scopes, - required String clientId, - String? userIdHint, - }) = _OAuthGetAuthUrl; - - factory OAuthGetAuthUrl.fromJson(Map json) => - _$OAuthGetAuthUrlFromJson(json); -} - -abstract class Scope { - static final openid = "openid"; - static final email = "email"; - static final clientApi = "urn:matrix:client:api:*"; - - static final _deviceChars = IList( - ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789" - "-._~") - .split(""), - ); - - static String get _deviceId => - _deviceChars.shuffle(Random.secure()).sublist(0, 10).join(); - - static String get device => "urn:matrix:client:device:$_deviceId"; -} - -@JsonEnum(fieldRename: .snake) -enum ResponseMode { query, fragment } diff --git a/lib/models/requests/oauth/register_client.dart b/lib/models/requests/oauth/register_client.dart deleted file mode 100644 index c4ef68f..0000000 --- a/lib/models/requests/oauth/register_client.dart +++ /dev/null @@ -1,47 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "register_client.freezed.dart"; -part "register_client.g.dart"; - -@freezed -abstract class OAuthRegisterClientRequest with _$OAuthRegisterClientRequest { - const factory OAuthRegisterClientRequest({ - required Uri homeserverUrl, - @Default(ApplicationType.web) ApplicationType applicationType, - String? clientName, - required Uri clientUri, - Uri? logoUri, - Uri? policyUri, - Uri? tosUri, - IList? grantTypes, - IList? redirectUris, - IList? responseTypes, - AuthMethod? authMethod, - }) = _OAuthRegisterClientRequest; - - factory OAuthRegisterClientRequest.fromJson(Map json) => - _$OAuthRegisterClientRequestFromJson(json); -} - -enum ApplicationType { native, web } - -@JsonEnum(fieldRename: .snake) -enum ResponseType { code, idToken } - -@JsonEnum(fieldRename: .snake) -enum AuthMethod { - clientSecretPost, - clientSecretBasic, - clientSecretJwt, - privateKeyJwt, - none, -} - -@JsonEnum(fieldRename: .snake) -enum GrantType { - authorizationCode, - refreshToken, - clientCredentials, - @JsonValue("urn:ietf:params:oauth:grant-type:device_code") - deviceCode, -} diff --git a/lib/models/requests/paginate.dart b/lib/models/requests/paginate.dart deleted file mode 100644 index ddc62f3..0000000 --- a/lib/models/requests/paginate.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "paginate.freezed.dart"; -part "paginate.g.dart"; - -@freezed -abstract class PaginateRequest with _$PaginateRequest { - const factory PaginateRequest({ - required String roomId, - required int? maxTimelineId, - @Default(20) int limit, - }) = _PaginateRequest; - - factory PaginateRequest.fromJson(Map json) => - _$PaginateRequestFromJson(json); -} diff --git a/lib/models/requests/redact_event.dart b/lib/models/requests/redact_event.dart deleted file mode 100644 index 3a01673..0000000 --- a/lib/models/requests/redact_event.dart +++ /dev/null @@ -1,3 +0,0 @@ -import "package:nexus/models/requests/report.dart"; - -typedef RedactEventRequest = ReportRequest; diff --git a/lib/models/requests/report.dart b/lib/models/requests/report.dart deleted file mode 100644 index f87b1f1..0000000 --- a/lib/models/requests/report.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "report.freezed.dart"; -part "report.g.dart"; - -@freezed -abstract class ReportRequest with _$ReportRequest { - const factory ReportRequest({ - required String roomId, - required String eventId, - String? reason, - }) = _ReportRequest; - - factory ReportRequest.fromJson(Map json) => - _$ReportRequestFromJson(json); -} diff --git a/lib/models/requests/send_event.dart b/lib/models/requests/send_event.dart deleted file mode 100644 index 196c0b5..0000000 --- a/lib/models/requests/send_event.dart +++ /dev/null @@ -1,20 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -part "send_event.freezed.dart"; -part "send_event.g.dart"; - -@freezed -abstract class SendEventRequest with _$SendEventRequest { - const factory SendEventRequest({ - required String roomId, - required String type, - required Content content, - String? relatesTo, - String? relationType, - @Default(false) bool synchronous, - @Default(false) bool disableEncryption, - }) = _SendEventRequest; - - factory SendEventRequest.fromJson(Map json) => - _$SendEventRequestFromJson(json); -} diff --git a/lib/models/requests/send_message.dart b/lib/models/requests/send_message.dart deleted file mode 100644 index 951198e..0000000 --- a/lib/models/requests/send_message.dart +++ /dev/null @@ -1,56 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/relation_type.dart"; -part "send_message.freezed.dart"; -part "send_message.g.dart"; - -@freezed -abstract class SendMessageRequest with _$SendMessageRequest { - const factory SendMessageRequest({ - required String roomId, - required String text, - Content? baseContent, - @Default(Mentions()) @JsonKey(name: "mentions") Mentions mentions, - @JsonKey(name: "relates_to") Relation? relation, - }) = _SendMessageRequest; - - factory SendMessageRequest.fromJson(Map json) => - _$SendMessageRequestFromJson(json); -} - -@freezed -abstract class Mentions with _$Mentions { - const factory Mentions({ - @Default(false) bool room, - @Default(IList.empty()) IList userIds, - }) = _Mentions; - - factory Mentions.fromJson(Map json) => - _$MentionsFromJson(json); -} - -@Freezed(toJson: false) -abstract class Relation with _$Relation { - const Relation._(); - - const factory Relation({ - required String eventId, - required RelationType relationType, - }) = _Relation; - - Map toJson() { - switch (relationType) { - case RelationType.reply: - return { - "m.in_reply_to": {"event_id": eventId}, - }; - - case RelationType.edit: - return {"rel_type": "m.replace", "event_id": eventId}; - } - } - - factory Relation.fromJson(Map json) => - _$RelationFromJson(json); -} diff --git a/lib/models/requests/set_membership.dart b/lib/models/requests/set_membership.dart deleted file mode 100644 index 4bbe8a3..0000000 --- a/lib/models/requests/set_membership.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/membership_action.dart"; -part "set_membership.freezed.dart"; -part "set_membership.g.dart"; - -@freezed -abstract class SetMembershipRequest with _$SetMembershipRequest { - const factory SetMembershipRequest({ - required String userId, - required String roomId, - - String? reason, - @JsonKey(name: "action") required MembershipAction action, - @Default(false) @JsonKey(name: "msc4293_redact_events") bool redact, - }) = _SetMembershipRequest; - - factory SetMembershipRequest.fromJson(Map json) => - _$SetMembershipRequestFromJson(json); -} diff --git a/lib/models/requests/set_state.dart b/lib/models/requests/set_state.dart deleted file mode 100644 index c92763d..0000000 --- a/lib/models/requests/set_state.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/ms_duration.dart"; -part "set_state.freezed.dart"; -part "set_state.g.dart"; - -@freezed -abstract class SetStateRequest with _$SetStateRequest { - const factory SetStateRequest({ - required String roomId, - required String type, - required String stateKey, - required Content content, - - @JsonKey(name: "delay_ms", includeIfNull: false) - @MSDuration() - @Default(null) - Duration? delay, - }) = _SetStateRequest; - - factory SetStateRequest.fromJson(Map json) => - _$SetStateRequestFromJson(json); -} diff --git a/lib/models/requests/upload_media.dart b/lib/models/requests/upload_media.dart deleted file mode 100644 index a640bea..0000000 --- a/lib/models/requests/upload_media.dart +++ /dev/null @@ -1,24 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "upload_media.freezed.dart"; -part "upload_media.g.dart"; - -@freezed -abstract class UploadMediaRequest with _$UploadMediaRequest { - const factory UploadMediaRequest({ - required String path, - required bool encrypt, - String? filename, - @Default(false) @JsonKey(name: "voice_message") bool isVoiceMessage, - @Default(false) bool forceFile, - - // Below params only work if encodeTo is set - String? encodeTo, - int? resizeWidth, - int? resizeHeight, - int? resizePercent, - @Default(80) int quality, - }) = _UploadMediaRequest; - - factory UploadMediaRequest.fromJson(Map json) => - _$UploadMediaRequestFromJson(json); -} diff --git a/lib/models/room.dart b/lib/models/room.dart deleted file mode 100644 index fb21a55..0000000 --- a/lib/models/room.dart +++ /dev/null @@ -1,57 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/read_receipt.dart"; -import "package:nexus/models/room_metadata.dart"; -part "room.freezed.dart"; -part "room.g.dart"; - -@freezed -abstract class Room with _$Room { - static IMap timelineTupleJsonToIMap(List json) => - IMap.fromEntries( - json.map( - (timelineTuple) => MapEntry( - timelineTuple["timeline_rowid"], - timelineTuple["event_rowid"], - ), - ), - ); - - static IMap eventsJsonToIMap(List json) => - IMap.fromEntries( - json.map((eventJson) { - final event = Event.fromJson(eventJson); - return MapEntry(event.rowId, event); - }), - ); - - /// [timeline] is an IMap of timelineRowId to eventRowId - /// [events] is an IMap of eventRowId to event - /// [sticky] is an ISet of eventRowId - const factory Room({ - @JsonKey(name: "meta") RoomMetadata? metadata, - @Default(IMap.empty()) - @JsonKey(fromJson: Room.timelineTupleJsonToIMap) - IMap timeline, - @Default(ISet.empty()) ISet sticky, - - @Default(IMap.empty()) - @JsonKey(fromJson: Room.eventsJsonToIMap) - IMap events, - - @Default(false) bool reset, - @Default(false) bool hasFetchedState, - @Default(false) bool hasFetchedMembers, - @Default(IMap.empty()) IMap> state, - - @Default(IMap.empty()) IMap> receipts, - @Default(false) bool dismissNotifications, - @Default(true) bool hasMore, - - // required IMap accountData, - // required IList notifications, - }) = _Room; - - factory Room.fromJson(Map json) => _$RoomFromJson(json); -} diff --git a/lib/models/room_metadata.dart b/lib/models/room_metadata.dart deleted file mode 100644 index 7c16cae..0000000 --- a/lib/models/room_metadata.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/epoch_date_time_converter.dart"; -import "package:nexus/models/lazy_load_summary.dart"; -part "room_metadata.freezed.dart"; -part "room_metadata.g.dart"; - -@freezed -abstract class RoomMetadata with _$RoomMetadata { - const factory RoomMetadata({ - @JsonKey(name: "room_id") required String id, - - // required CreateEventContent creationContent, - // required TombstoneEventContent tombstoneEventContent, - String? name, - Uri? avatar, - String? dmUserId, - String? topic, - String? canonicalAlias, - LazyLoadSummary? lazyLoadSummary, - required bool hasMemberList, - @JsonKey(name: "preview_event_rowid") required int previewEventRowID, - @EpochDateTimeConverter() required DateTime sortingTimestamp, - required int unreadHighlights, - required int unreadNotifications, - required int unreadMessages, - }) = _RoomMetadata; - - factory RoomMetadata.fromJson(Map json) => - _$RoomMetadataFromJson(json); -} diff --git a/lib/models/session_backup.dart b/lib/models/session_backup.dart new file mode 100644 index 0000000..0245c7e --- /dev/null +++ b/lib/models/session_backup.dart @@ -0,0 +1,17 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "session_backup.freezed.dart"; +part "session_backup.g.dart"; + +@freezed +abstract class SessionBackup with _$SessionBackup { + const factory SessionBackup({ + required String accessToken, + required Uri homeserver, + required String userID, + required String deviceID, + required String deviceName, + }) = _SessionBackup; + + factory SessionBackup.fromJson(Map json) => + _$SessionBackupFromJson(json); +} diff --git a/lib/models/setting.dart b/lib/models/setting.dart deleted file mode 100644 index 6a49845..0000000 --- a/lib/models/setting.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:flutter/material.dart"; - -class Setting { - final String title; - final String description; - final IconData icon; - final Widget Function(String title, String description, IconData icon) - builder; - - Setting({ - required this.title, - required this.description, - required this.builder, - required this.icon, - }); -} diff --git a/lib/models/settings.dart b/lib/models/settings.dart deleted file mode 100644 index eddc2da..0000000 --- a/lib/models/settings.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:flutter/material.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "settings.freezed.dart"; -part "settings.g.dart"; - -@freezed -abstract class Settings with _$Settings { - const factory Settings({ - @Default(ThemeMode.system) ThemeMode theme, - @Default(true) bool useDynamicTheming, - @Default(false) bool linuxMobileMode, - }) = _Settings; - - factory Settings.fromJson(Map json) => - _$SettingsFromJson(json); -} diff --git a/lib/models/settings_category.dart b/lib/models/settings_category.dart deleted file mode 100644 index 88bfdd0..0000000 --- a/lib/models/settings_category.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/setting.dart"; -part "settings_category.freezed.dart"; - -@freezed -abstract class SettingsCategory with _$SettingsCategory { - const factory SettingsCategory({ - required String title, - required IconData icon, - required IList settings, - }) = _SettingsCategory; -} diff --git a/lib/models/space.dart b/lib/models/space.dart index 73fbbc6..d64dcc9 100644 --- a/lib/models/space.dart +++ b/lib/models/space.dart @@ -1,18 +1,20 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/widgets.dart"; import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/subspace.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/models/full_room.dart"; part "space.freezed.dart"; @freezed abstract class Space with _$Space { + const Space._(); const factory Space({ - required String id, required String title, + required String id, + required IList children, + required Client client, + Room? roomData, + Uri? avatar, IconData? icon, - Room? room, - required IList children, - required IList subSpaces, }) = _Space; } diff --git a/lib/models/space_edge.dart b/lib/models/space_edge.dart deleted file mode 100644 index 192af31..0000000 --- a/lib/models/space_edge.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "space_edge.freezed.dart"; -part "space_edge.g.dart"; - -@freezed -abstract class SpaceEdge with _$SpaceEdge { - const factory SpaceEdge({ - required String childId, - @Default(false) bool suggested, - }) = _SpaceEdge; - - factory SpaceEdge.fromJson(Map json) => - _$SpaceEdgeFromJson(json); -} diff --git a/lib/models/subspace.dart b/lib/models/subspace.dart deleted file mode 100644 index 1a1879c..0000000 --- a/lib/models/subspace.dart +++ /dev/null @@ -1,10 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/room.dart"; -part "subspace.freezed.dart"; - -@freezed -abstract class Subspace with _$Subspace { - const factory Subspace({required Room room, required IList children}) = - _Subspace; -} diff --git a/lib/models/sync_data.dart b/lib/models/sync_data.dart deleted file mode 100644 index 0f98bb2..0000000 --- a/lib/models/sync_data.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/account_data.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/space_edge.dart"; -part "sync_data.freezed.dart"; -part "sync_data.g.dart"; - -@freezed -abstract class SyncData with _$SyncData { - const factory SyncData({ - @Default(false) bool clearState, - @Default(IMap.empty()) IMap accountData, - @Default(IMap.empty()) IMap rooms, - @Default(ISet.empty()) ISet leftRooms, - // required IList invitedRooms, - IMap>? spaceEdges, - IList? topLevelSpaces, - }) = _SyncData; - - factory SyncData.fromJson(Map json) => - _$SyncDataFromJson(json); -} diff --git a/lib/models/sync_status.dart b/lib/models/sync_status.dart deleted file mode 100644 index 7848fbe..0000000 --- a/lib/models/sync_status.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "sync_status.freezed.dart"; -part "sync_status.g.dart"; - -@freezed -abstract class SyncStatus with _$SyncStatus { - const factory SyncStatus({ - required SyncStatusType type, - String? error, - required int errorCount, - }) = _SyncStatus; - - factory SyncStatus.fromJson(Map json) => - _$SyncStatusFromJson(json); -} - -@JsonEnum(fieldRename: FieldRename.kebab) -enum SyncStatusType { ok, waiting, erroring, permanentlyFailed } diff --git a/lib/pages/chat.dart b/lib/pages/chat.dart deleted file mode 100644 index 316c9af..0000000 --- a/lib/pages/chat.dart +++ /dev/null @@ -1,53 +0,0 @@ -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/emoji.dart"; -import "package:nexus/controllers/init_complete.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/widgets/appbar.dart"; -import "package:nexus/widgets/sidebar.dart"; -import "package:nexus/widgets/room_chat.dart"; -import "package:nexus/widgets/loading.dart"; - -class ChatPage extends ConsumerWidget { - const ChatPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( - builder: (context, constraints) { - final isDesktop = constraints.maxWidth > 650; - final showMembersByDefault = constraints.maxWidth > 1000; - final initComplete = ref.watch(InitCompleteController.provider); - final roomId = ref.watch(KeyController.provider(KeyController.roomKey)); - - ref.read(EmojiController.provider); - - return SafeArea( - child: Scaffold( - appBar: initComplete ? null : Appbar(), - body: initComplete - ? Row( - children: [ - if (isDesktop) Sidebar(isDesktop: isDesktop), - Expanded( - child: RoomChat( - roomId: roomId, - isDesktop: isDesktop, - showMembersByDefault: showMembersByDefault, - ), - ), - ], - ) - : Center( - child: Column( - mainAxisSize: .min, - children: [Loading(), Text("Syncing...")], - ), - ), - drawer: isDesktop || !initComplete - ? null - : Sidebar(isDesktop: isDesktop), - ), - ); - }, - ); -} diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart new file mode 100644 index 0000000..c60cc45 --- /dev/null +++ b/lib/pages/chat_page.dart @@ -0,0 +1,32 @@ +import "package:flutter/material.dart"; +import "package:nexus/widgets/chat_page/room_chat.dart"; +import "package:nexus/widgets/chat_page/sidebar.dart"; + +class ChatPage extends StatelessWidget { + const ChatPage({super.key}); + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) { + final isDesktop = constraints.maxWidth > 650; + final showMembersByDefault = constraints.maxWidth > 1000; + + return Scaffold( + body: Builder( + builder: (context) => Row( + children: [ + if (isDesktop) Sidebar(), + Expanded( + child: RoomChat( + isDesktop: isDesktop, + showMembersByDefault: showMembersByDefault, + ), + ), + ], + ), + ), + drawer: isDesktop ? null : Sidebar(), + ); + }, + ); +} diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart new file mode 100644 index 0000000..4631325 --- /dev/null +++ b/lib/pages/login_page.dart @@ -0,0 +1,205 @@ +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:flutter_svg/flutter_svg.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/helpers/launch_helper.dart"; +import "package:nexus/models/homeserver.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/divider_text.dart"; +import "package:nexus/widgets/loading.dart"; + +class LoginPage extends HookConsumerWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + + final isLoading = useState(false); + final allowLogin = useState(false); + + final launch = ref.watch(LaunchHelper.provider).launchUrl; + + Future setHomeserver(Uri? homeserver) async { + isLoading.value = true; + final succeeded = homeserver == null + ? false + : await ref + .watch(ClientController.provider.notifier) + .setHomeserver( + homeserver.hasScheme + ? homeserver + : Uri.https(homeserver.path), + ); + + if (succeeded) { + allowLogin.value = true; + } else if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Homeserver verification failed. Is your homeserver down?", + style: TextStyle(color: theme.colorScheme.onErrorContainer), + ), + backgroundColor: theme.colorScheme.errorContainer, + ), + ); + } + isLoading.value = false; + } + + final homeserverUrl = useTextEditingController(); + final username = useTextEditingController(); + final password = useTextEditingController(); + + return Scaffold( + appBar: Appbar(), + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 64), + children: [ + Row( + children: [ + SvgPicture.asset("assets/icon.svg"), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Nexus", style: theme.textTheme.displayMedium), + Text( + "A Simple Matrix Client", + style: theme.textTheme.headlineMedium, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 12), + child: Divider(), + ), + + DividerText("Enter a homeserver domain:"), + Row( + spacing: 8, + children: [ + Expanded( + child: TextField( + controller: homeserverUrl, + decoration: InputDecoration( + labelText: "Homeserver URL (e.g. matrix.org)", + ), + ), + ), + IconButton.filled( + onPressed: isLoading.value + ? null + : () => setHomeserver(Uri.tryParse(homeserverUrl.text)), + icon: Icon(Icons.check), + ), + ], + ), + + DividerText("Or, choose from some popular homeservers:"), + ...([ + Homeserver( + name: "Matrix.org", + description: + "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", + url: Uri.https("matrix.org"), + iconUrl: + "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", + ), + Homeserver( + name: "Federated Nexus", + description: + "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", + url: Uri.https("federated.nexus"), + iconUrl: "https://federated.nexus/images/icon.png", + ), + Homeserver( + name: "envs.net", + description: + "envs.net is a minimalist, non-commercial shared linux system and will always be free to use.", + url: Uri.https("envs.net"), + iconUrl: "https://envs.net/favicon.ico", + ), + ].map( + (homeserver) => Card( + child: ListTile( + title: Text(homeserver.name), + leading: Image.network( + homeserver.iconUrl, + errorBuilder: (_, _, _) => SizedBox.shrink(), + height: 32, + ), + subtitle: Text(homeserver.description), + onTap: isLoading.value + ? null + : () => setHomeserver(homeserver.url), + trailing: IconButton( + onPressed: () => launch(homeserver.url), + icon: Icon(Icons.info_outline), + ), + ), + ), + )), + SizedBox(height: 8), + TextButton( + onPressed: () => launch(Uri.https("servers.joinmatrix.org")), + child: Text("See more homeservers..."), + ), + if (isLoading.value) + Padding(padding: EdgeInsets.only(top: 32), child: Loading()) + else if (allowLogin.value) ...[ + DividerText("Then, sign in:"), + SizedBox(height: 4), + TextField( + decoration: InputDecoration(label: Text("Username")), + controller: username, + ), + SizedBox(height: 12), + TextField( + decoration: InputDecoration(label: Text("Password")), + controller: password, + obscureText: true, + ), + SizedBox(height: 12), + ElevatedButton( + onPressed: () async { + isLoading.value = true; + final succeeded = await ref + .watch(ClientController.provider.notifier) + .login(username.text, password.text); + + if (!succeeded && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Login failed. Is your password right?", + style: TextStyle( + color: theme.colorScheme.onErrorContainer, + ), + ), + backgroundColor: theme.colorScheme.errorContainer, + ), + ); + isLoading.value = false; + } + }, + child: Text("Sign In"), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/select_server.dart b/lib/pages/select_server.dart deleted file mode 100644 index f7cefb8..0000000 --- a/lib/pages/select_server.dart +++ /dev/null @@ -1,215 +0,0 @@ -import "package:app_links/app_links.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_svg/flutter_svg.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/auth_url.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_id.dart"; -import "package:nexus/helpers/launch_helper.dart"; -import "package:nexus/main.dart"; -import "package:nexus/models/homeserver.dart"; -import "package:nexus/pages/settings.dart"; -import "package:nexus/widgets/appbar.dart"; -import "package:nexus/widgets/divider_text.dart"; - -class SelectServerPage extends HookConsumerWidget { - const SelectServerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - - final launch = ref.watch(LaunchHelper.provider).launchUrl; - - final isLoading = useState(false); - final homeserverUrl = useTextEditingController(); - - Future setHomeserver(Uri? newHomeserver) async { - isLoading.value = true; - - try { - if (newHomeserver?.hasScheme == false) { - newHomeserver = Uri.https(newHomeserver!.path); - } - - final newUrl = newHomeserver == null - ? null - : await ref - .read(ClientController.provider.notifier) - .discoverHomeserver(newHomeserver); - - if (context.mounted) { - if (newUrl == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Homeserver verification failed. Is your homeserver down?", - style: .new(color: theme.colorScheme.onErrorContainer), - ), - backgroundColor: theme.colorScheme.errorContainer, - ), - ); - } else { - final codeResponse = await ref.watch( - AuthUrlController.provider(newUrl).future, - ); - - await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url); - - AppLinks().uriLinkStream.listen((encodedUri) async { - final state = encodedUri.queryParameters["state"]; - if (state != codeResponse.state) return; - - final code = encodedUri.queryParameters["code"]; - - if (code != null) { - await ref - .watch(ClientController.provider.notifier) - .exchangeToken( - .new( - homeserverUrl: newUrl, - codeVerifier: codeResponse.codeVerifier, - redirectUri: .new( - scheme: "nexus.federated.nexus", - path: "/", - ), - code: code, - clientId: await ref.watch( - ClientIdController.provider(newUrl).future, - ), - ), - ) - .onError(showError); - } - }); - } - } - } catch (error, stackTrace) { - showError(error, stackTrace); - } finally { - isLoading.value = false; - } - } - - return Scaffold( - appBar: Appbar( - actions: .new([ - IconButton( - onPressed: () => - showDialog(context: context, builder: (_) => SettingsPage()), - icon: Icon(Icons.settings), - ), - ]), - ), - body: SafeArea( - child: Center( - child: ConstrainedBox( - constraints: .new(maxWidth: 600), - child: ListView( - padding: .symmetric(vertical: 8, horizontal: 12), - children: [ - Row( - children: [ - SvgPicture.asset("assets/icon.svg", width: 128), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: .start, - children: [ - Text("Nexus", style: theme.textTheme.displayMedium), - Text( - "A Simple Matrix Client", - style: theme.textTheme.headlineMedium, - overflow: .ellipsis, - ), - ], - ), - ), - ], - ), - Padding(padding: .symmetric(vertical: 12), child: Divider()), - DividerText("Enter a homeserver domain:"), - Row( - spacing: 8, - children: [ - Expanded( - child: TextField( - textInputAction: .done, - autofocus: true, - onSubmitted: (text) => setHomeserver(.tryParse(text)), - controller: homeserverUrl, - decoration: .new( - labelText: "Homeserver URL", - hintText: "matrix.org", - ), - ), - ), - IconButton.filled( - tooltip: "Confirm homeserver choice", - onPressed: isLoading.value - ? null - : () => setHomeserver(.tryParse(homeserverUrl.text)), - icon: Icon(Icons.check), - ), - ], - ), - DividerText("Or, choose from some popular homeservers:"), - ...([ - .new( - name: "Matrix.org", - description: - "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", - url: .https("matrix.org"), - iconUrl: - "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", - ), - .new( - name: "Federated Nexus", - description: - "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", - url: .https("federated.nexus"), - iconUrl: "https://federated.nexus/images/icon.png", - ), - .new( - name: "Unredacted", - description: - "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", - url: .https("unredacted.org", "services/si/matrix"), - iconUrl: "https://unredacted.org/favicon.ico", - ), - ].map( - (homeserver) => Card( - child: ListTile( - enabled: !isLoading.value, - title: Text(homeserver.name), - leading: Image.network( - homeserver.iconUrl, - errorBuilder: (_, _, _) => SizedBox.shrink(), - height: 32, - ), - subtitle: Text(homeserver.description), - onTap: isLoading.value - ? null - : () => setHomeserver(homeserver.url), - trailing: IconButton( - tooltip: "Launch homeserver info page", - onPressed: () => launch(homeserver.url), - icon: Icon(Icons.info_outline), - ), - ), - ), - )), - - TextButton( - onPressed: () => launch(.https("servers.joinmatrix.org")), - child: Text("See more homeservers..."), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart deleted file mode 100644 index fa4d8da..0000000 --- a/lib/pages/settings.dart +++ /dev/null @@ -1,245 +0,0 @@ -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:m3e_card_list/m3e_card_list.dart"; -import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; -import "package:nexus/controllers/settings_sections.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/show_about_dialog.dart"; -import "package:nexus/pages/settings_category.dart"; -import "package:nexus/widgets/divider_text.dart"; -import "package:nexus/widgets/highlight_wrapper.dart"; -import "package:super_sliver_list/super_sliver_list.dart"; - -class SettingsPage extends ConsumerWidget { - const SettingsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( - builder: (_, constraints) => HookBuilder( - builder: (context) { - final categoriesArePages = constraints.maxWidth < 550; - - final selected = useState(0); - - final highlightedMatch = useState<(int, int)?>(null); - final listController = useRef(ListController()); - final scrollController = useScrollController(); - - final searchBar = SearchAnchor.bar( - barHintText: "Search...", - isFullScreen: categoriesArePages, - suggestionsBuilder: (suggestionsContext, controller) async { - final categories = await ref.watch( - SettingsSectionsController.provider.future, - ); - final query = controller.text.toLowerCase(); - - final matches = categories.values.flattenedToList - .asMap() - .entries - .expand( - (categoryEntry) => categoryEntry.value.settings - .asMap() - .entries - .where( - (settingEntry) => - settingEntry.value.title.toLowerCase().contains( - query, - ) || - settingEntry.value.description - .toLowerCase() - .contains(query), - ) - .map( - (settingEntry) => ( - (categoryEntry.key, settingEntry.key), - settingEntry.value, - ), - ), - ) - .toIList(); - - return matches.map( - (match) => ListTile( - onTap: () async { - if (context.mounted) Navigator.of(suggestionsContext).pop(); - controller.text = ""; - - if (categoriesArePages) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => SettingsCategoryPage( - match.$1.$1, - initialHighlight: match.$1.$2, - ), - ), - ); - } else { - selected.value = match.$1.$1; - listController.value.animateToItem( - index: match.$1.$2, - scrollController: scrollController, - alignment: 0.5, - duration: (_) => .new(milliseconds: 700), - curve: (_) => Curves.easeInOut, - ); - highlightedMatch.value = match.$1; - await Future.delayed(.new(seconds: 1), () { - if (highlightedMatch.value == match.$1) { - highlightedMatch.value = null; - } - }); - } - }, - leading: Icon(match.$2.icon), - title: Text(match.$2.title), - subtitle: Text(match.$2.description), - ), - ); - }, - ); - - final settingsContent = Scaffold( - appBar: AppBar( - title: Text("Settings"), - actionsPadding: .symmetric(horizontal: 12), - actions: [ - IconButton( - onPressed: () => context.showAboutDialog(ref), - icon: Icon(Icons.info_outline), - ), - ], - ), - body: ref - .watch(SettingsSectionsController.provider) - .betterWhen( - data: (sections) => categoriesArePages - ? CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.all(12).copyWith(bottom: 8), - child: searchBar, - ), - ), - ...sections - .mapTo( - (section, categories) => [ - SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 16, - ).copyWith(bottom: 4), - child: DividerText(section), - ), - ), - SliverM3ECardList( - padding: .symmetric( - horizontal: 4, - vertical: 8, - ), - margin: .symmetric(horizontal: 12), - color: Theme.of( - context, - ).colorScheme.primaryContainer, - itemCount: categories.length, - onTap: (index) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - SettingsCategoryPage( - sections - .values - .flattenedToList - .indexOf( - categories[index], - ), - ), - ), - ), - itemBuilder: (context, index) => ListTile( - leading: Icon(categories[index].icon), - title: Text(categories[index].title), - ), - ), - ], - ) - .flattened, - ], - ) - : Row( - children: [ - NavigationRailM3E( - type: .alwaysExpand, - trailing: searchBar, - scrollable: true, - sections: sections - .mapTo( - (categoryGroup, categories) => - NavigationRailM3ESection( - header: DividerText(categoryGroup), - destinations: categories - .map( - (category) => - NavigationRailM3EDestination( - icon: Icon(category.icon), - label: category.title, - ), - ) - .toList(), - ), - ) - .toList(), - selectedIndex: selected.value, - onDestinationSelected: (value) => - selected.value = value, - ), - VerticalDivider(), - Expanded( - child: SuperListView( - listController: listController.value, - controller: scrollController, - padding: .symmetric(vertical: 12), - children: sections - .values - .flattenedToList[selected.value] - .settings - .mapIndexed( - (index, setting) => Padding( - padding: .only(bottom: 4), - child: HighlightWrapper( - setting.builder( - setting.title, - setting.description, - setting.icon, - ), - isHighlighted: - highlightedMatch.value == - (selected.value, index), - ), - ), - ) - .toList(), - ), - ), - ], - ), - ), - ); - - return constraints.maxWidth < 650 - ? settingsContent - : Dialog( - constraints: .loose(Size(900, 600)), - child: ClipRRect( - borderRadius: BorderRadiusGeometry.circular(12), - child: settingsContent, - ), - ); - }, - ), - ); -} diff --git a/lib/pages/settings_category.dart b/lib/pages/settings_category.dart deleted file mode 100644 index b21d048..0000000 --- a/lib/pages/settings_category.dart +++ /dev/null @@ -1,79 +0,0 @@ -import "dart:async"; - -import "package:collection/collection.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/settings_sections.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/widgets/highlight_wrapper.dart"; -import "package:super_sliver_list/super_sliver_list.dart"; - -class SettingsCategoryPage extends HookConsumerWidget { - final int index; - final int? initialHighlight; - const SettingsCategoryPage(this.index, {this.initialHighlight, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final highlight = useState(initialHighlight); - final listController = useRef(ListController()); - final scrollController = useScrollController(); - - useEffect(() { - if (initialHighlight == null) return null; - Timer? timer; - - void listener() => WidgetsBinding.instance.addPostFrameCallback((_) { - if (!listController.value.isAttached) return; - - listController.value.animateToItem( - index: initialHighlight!, - scrollController: scrollController, - alignment: 0.5, - duration: (_) => .new(milliseconds: 700), - curve: (_) => Curves.easeInOut, - ); - timer = Timer(.new(seconds: 1), () { - highlight.value = null; - }); - listController.value.removeListener(listener); - }); - - listController.value.addListener(listener); - return timer?.cancel; - }, []); - - return ref - .watch(SettingsSectionsController.provider) - .betterWhen( - data: (sections) => Scaffold( - appBar: AppBar( - title: Text(sections.values.flattenedToList[index].title), - ), - body: SafeArea( - child: SuperListView( - controller: scrollController, - listController: listController.value, - padding: .symmetric(vertical: 12, horizontal: 8), - children: sections.values.flattenedToList[index].settings - .mapIndexed( - (index, setting) => Padding( - padding: .only(bottom: 4), - child: HighlightWrapper( - setting.builder( - setting.title, - setting.description, - setting.icon, - ), - isHighlighted: highlight.value == index, - ), - ), - ) - .toList(), - ), - ), - ), - ); - } -} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart new file mode 100644 index 0000000..b348aac --- /dev/null +++ b/lib/pages/settings_page.dart @@ -0,0 +1,18 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/secure_storage_controller.dart"; + +class SettingsPage extends ConsumerWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: Text("Settings")), + body: ElevatedButton( + onPressed: ref.watch(SecureStorageController.provider.notifier).clear, + child: Text("Log out"), + ), + ); + } +} diff --git a/lib/pages/verify.dart b/lib/pages/verify.dart deleted file mode 100644 index 4eefa6c..0000000 --- a/lib/pages/verify.dart +++ /dev/null @@ -1,84 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/pages/settings.dart"; -import "package:nexus/widgets/appbar.dart"; -import "package:nexus/helpers/required_validator_helper.dart"; - -class VerifyPage extends HookConsumerWidget { - const VerifyPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final passphraseController = useTextEditingController(); - final isLoading = useState(false); - final inputError = useState(null); - final formKey = useRef(GlobalKey()); - - Future verify() async { - isLoading.value = true; - - try { - if (formKey.value.currentState?.validate() != true) { - return; - } - - inputError.value = await ref - .watch(ClientController.provider.notifier) - .verify(passphraseController.text); - } finally { - isLoading.value = false; - } - } - - return Scaffold( - appBar: Appbar( - actions: .new([ - IconButton( - onPressed: () => - showDialog(context: context, builder: (_) => SettingsPage()), - icon: Icon(Icons.settings), - ), - ]), - ), - body: AlertDialog( - title: Text("Verify"), - content: Form( - key: formKey.value, - child: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text( - "Enter your recovery key or passphrase below to unlock encrypted events.\nYour passphrase is usually not the same as your password.", - ), - SizedBox(height: 12), - TextFormField( - autofocus: true, - controller: passphraseController, - textInputAction: .done, - autovalidateMode: .onUserInteraction, - validator: requiredValidator, - obscureText: true, - decoration: .new( - label: Text("Recovery Key or Passphrase"), - errorText: inputError.value, - ), - onFieldSubmitted: (_) => verify(), - // Don't defocus on submit - onEditingComplete: () {}, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: isLoading.value ? null : verify, - child: Text("Verify"), - ), - ], - ), - ); - } -} diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index 9da03aa..3ecaa1d 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -1,78 +1,35 @@ import "dart:io"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/settings.dart"; -import "package:window_manager/window_manager.dart"; -class Appbar extends ConsumerWidget implements PreferredSizeWidget { +class Appbar extends StatelessWidget implements PreferredSizeWidget { final Widget? leading; final Widget? title; final Color? backgroundColor; final double? scrolledUnderElevation; - final IList actions; - final VoidCallback? onTap; - + final List actions; const Appbar({ super.key, this.title, - this.onTap, this.backgroundColor, this.scrolledUnderElevation, this.leading, - this.actions = const .empty(), + this.actions = const [], }); @override - Size get preferredSize => const .fromHeight(kToolbarHeight); + Size get preferredSize => AppBar().preferredSize; @override - Widget build(BuildContext context, WidgetRef ref) { - Future maximize() async { - final isMaximized = await windowManager.isMaximized(); - - if (isMaximized) { - return windowManager.unmaximize(); - } - - return windowManager.maximize(); - } - - return GestureDetector( - onPanStart: ref - .watch(SettingsController.provider) - .whenOrNull( - data: (settings) => settings.linuxMobileMode - ? null - : (_) => windowManager.startDragging(), - ), - child: AppBar( - leading: InkWell(onTap: onTap, child: leading), - backgroundColor: backgroundColor, - scrolledUnderElevation: scrolledUnderElevation, - actionsPadding: const .symmetric(horizontal: 8), - title: InkWell( - onTap: onTap, - child: IgnorePointer(child: title), - ), - flexibleSpace: GestureDetector(onDoubleTap: maximize), - actions: [ - ...actions, - if (!(Platform.isAndroid || Platform.isIOS)) ...[ - if (!Platform.isLinux) - IconButton( - tooltip: "Maximize window", - onPressed: maximize, - icon: const Icon(Icons.fullscreen), - ), - IconButton( - tooltip: "Close window", - onPressed: () => exit(0), - icon: const Icon(Icons.close), - ), - ], - ], - ), - ); - } + AppBar build(BuildContext context) => AppBar( + leading: leading, + backgroundColor: backgroundColor, + scrolledUnderElevation: scrolledUnderElevation, + actionsPadding: EdgeInsets.symmetric(horizontal: 8), + title: title, + actions: [ + ...actions, + if (!(Platform.isAndroid || Platform.isIOS)) + IconButton(onPressed: () => exit(0), icon: Icon(Icons.close)), + ], + ); } diff --git a/lib/widgets/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart index 1cca8c6..41bd002 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -1,63 +1,51 @@ import "package:color_hash/color_hash.dart"; -import "package:cross_cache/cross_cache.dart"; import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/cross_cache.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -class AvatarOrHash extends ConsumerWidget { +class AvatarOrHash extends StatelessWidget { final Uri? avatar; final String title; final Widget? fallback; + final bool hasBadge; final double height; + final Map headers; const AvatarOrHash( this.avatar, this.title, { this.fallback, + this.hasBadge = false, this.height = 24, + required this.headers, super.key, }); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final box = ColoredBox( color: ColorHash(title).color, - child: Center(child: Icon(Icons.person, size: height / 2)), + child: Center(child: Text(title[0])), ); - - final parsedAvatar = avatar?.mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, - ), - ) ?? - "", - ); - return SizedBox( width: height, height: height, child: Center( - child: ClipRRect( - borderRadius: .all(.circular((height - 8) / 2.5)), - child: SizedBox( - width: height, - height: height, - child: parsedAvatar == null - ? fallback ?? box - : Image( - image: CachedNetworkImage( - parsedAvatar.toString(), - ref.watch(CrossCacheController.provider), - headers: ref.headers, + child: Badge( + isLabelVisible: hasBadge, + smallSize: 8, + backgroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(4)), + child: SizedBox( + width: height, + height: height, + child: avatar == null + ? fallback ?? box + : Image.network( + avatar.toString(), + headers: headers, + fit: BoxFit.contain, + errorBuilder: (_, _, _) => box, ), - fit: .cover, - loadingBuilder: (_, child, loadingProgress) => - loadingProgress == null ? child : fallback ?? box, - errorBuilder: (_, _, _) => fallback ?? box, - ), + ), ), ), ), diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/chat_box.dart new file mode 100644 index 0000000..016e3a7 --- /dev/null +++ b/lib/widgets/chat_page/chat_box.dart @@ -0,0 +1,157 @@ +import "dart:io"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:fluttertagger/fluttertagger.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/room_chat_controller.dart"; +import "package:nexus/models/relation_type.dart"; +import "package:nexus/widgets/chat_page/mention_overlay.dart"; +import "package:nexus/widgets/chat_page/relation_preview.dart"; + +class ChatBox extends HookConsumerWidget { + final Message? relatedMessage; + final RelationType relationType; + final VoidCallback onDismiss; + final Room room; + const ChatBox({ + required this.relatedMessage, + required this.relationType, + required this.onDismiss, + required this.room, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final controller = useRef(FlutterTaggerController()); + final triggerCharacter = useState(""); + final query = useState(""); + + if (relationType == RelationType.edit && + relatedMessage is TextMessage && + controller.value.text.isEmpty) { + final text = (relatedMessage as TextMessage).text; + controller.value.text = relatedMessage?.replyToMessageId == null + ? text + : text.split("\n\n").sublist(1).join("\n\n"); + } + + void send() { + ref + .watch(RoomChatController.provider(room).notifier) + .send( + controller.value.formattedText, + relation: relatedMessage, + relationType: relationType, + tags: controller.value.tags, + ); + onDismiss(); + controller.value.text = ""; + } + + final node = useFocusNode( + onKeyEvent: (_, event) { + if (event is KeyDownEvent && !Platform.isAndroid && !Platform.isIOS) { + if (event.logicalKey == LogicalKeyboardKey.enter && + !HardwareKeyboard.instance.isShiftPressed) { + send(); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.escape) { + onDismiss(); + return KeyEventResult.handled; + } + } + + return KeyEventResult.ignored; + }, + )..requestFocus(); + + final style = TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ); + + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Padding( + padding: EdgeInsetsGeometry.all(12), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: Column( + children: [ + RelationPreview( + relatedMessage: relatedMessage, + relationType: relationType, + onDismiss: onDismiss, + room: room, + ), + Container( + color: theme.colorScheme.surfaceContainerHighest, + padding: EdgeInsets.symmetric(horizontal: 8), + child: Row( + spacing: 8, + children: [ + PopupMenuButton( + itemBuilder: (context) => [], + icon: Icon(Icons.add), + enabled: room.canSendDefaultMessages, + ), + Expanded( + child: FlutterTagger( + triggerStrategy: TriggerStrategy.eager, + overlay: MentionOverlay( + room, + query: query.value, + triggerCharacter: triggerCharacter.value, + addTag: ({required id, required name}) { + controller.value.addTag(id: id, name: name); + node.requestFocus(); + }, + ), + controller: controller.value, + onSearch: (newQuery, newTriggerCharacter) { + triggerCharacter.value = newTriggerCharacter; + query.value = newQuery; + }, + triggerCharacterAndStyles: { + "@": style, + "#": style, + ":": style, + }, + builder: (context, key) => TextFormField( + enabled: room.canSendDefaultMessages, + maxLines: 12, + minLines: 1, + decoration: InputDecoration( + hintText: room.canSendDefaultMessages + ? "Your message here..." + : "You don't have permission to send messages in this room...", + border: InputBorder.none, + ), + controller: controller.value, + key: key, + autofocus: true, + focusNode: node, + ), + ), + ), + IconButton( + onPressed: room.canSendDefaultMessages ? send : null, + icon: Icon(Icons.send), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/html/code_block.dart b/lib/widgets/chat_page/html/code_block.dart similarity index 74% rename from lib/widgets/html/code_block.dart rename to lib/widgets/chat_page/html/code_block.dart index a5c3dee..fe5b492 100644 --- a/lib/widgets/html/code_block.dart +++ b/lib/widgets/chat_page/html/code_block.dart @@ -11,20 +11,20 @@ class CodeBlock extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); return ClipRRect( - borderRadius: .all(.circular(16)), + borderRadius: BorderRadius.all(Radius.circular(16)), child: ColoredBox( color: theme.colorScheme.surfaceContainerHighest, child: IntrinsicWidth( child: Column( children: [ Row( - mainAxisAlignment: .spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( - padding: .symmetric(horizontal: 8), + padding: EdgeInsets.symmetric(horizontal: 8), child: Text( lang.substring(0, min(lang.length, 15)), - style: .new(fontFamily: "monospace"), + style: TextStyle(fontFamily: "monospace"), ), ), TextButton.icon( @@ -37,13 +37,11 @@ class CodeBlock extends StatelessWidget { ColoredBox( color: theme.colorScheme.surfaceContainerHigh, child: Container( - constraints: .new(minWidth: 250), - padding: .all(8), + constraints: BoxConstraints(minWidth: 250), + padding: EdgeInsets.all(8), child: SelectableText( code, - minLines: 1, - maxLines: 99, - style: .new(fontFamily: "monospace"), + style: TextStyle(fontFamily: "monospace"), ), ), ), diff --git a/lib/widgets/chat_page/html/html.dart b/lib/widgets/chat_page/html/html.dart new file mode 100644 index 0000000..ce5318a --- /dev/null +++ b/lib/widgets/chat_page/html/html.dart @@ -0,0 +1,156 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/thumbnail_controller.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/helpers/launch_helper.dart"; +import "package:nexus/models/image_data.dart"; +import "package:nexus/widgets/chat_page/html/mention_chip.dart"; +import "package:nexus/widgets/chat_page/html/spoiler_text.dart"; +import "package:nexus/widgets/chat_page/html/code_block.dart"; +import "package:nexus/widgets/chat_page/html/quoted.dart"; +import "package:nexus/widgets/error_dialog.dart"; + +class Html extends ConsumerWidget { + final String html; + final Client client; + const Html(this.html, {required this.client, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => HtmlWidget( + html, + customWidgetBuilder: (element) { + if (element.attributes.keys.contains("data-mx-spoiler")) { + return InlineCustomWidget(child: SpoilerText(text: element.text)); + } + + final height = int.tryParse(element.attributes["height"] ?? "") ?? 300; + final width = int.tryParse(element.attributes["width"] ?? ""); + + return switch (element.localName) { + "code" => + element.parent?.localName == "pre" + ? CodeBlock( + element.text, + lang: element.className.replaceAll("language-", ""), + ) + : null, + + "blockquote" => Quoted(Html(element.innerHtml, client: client)), + + "a" => + element.attributes["href"]?.parseIdentifierIntoParts() == null + ? null + : InlineCustomWidget(child: MentionChip(element.text)), + + "img" => + element.attributes["src"] == null + ? null + : Consumer( + builder: (_, ref, _) => ref + .watch( + ThumbnailController.provider( + ImageData( + uri: element.attributes["src"]!, + height: height, + width: width, + ), + ), + ) + .when( + data: (uri) { + if (uri == null) return SizedBox.shrink(); + + return InlineCustomWidget( + child: Image.network( + uri, + headers: client.headers, + errorBuilder: (_, error, _) => Text( + "Image Failed to Load", + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + height: height.toDouble(), + width: width?.toDouble(), + loadingBuilder: (_, child, loadingProgress) => + loadingProgress == null + ? child + : CircularProgressIndicator(), + ), + ); + }, + error: ErrorDialog.new, + loading: () => InlineCustomWidget( + child: SizedBox( + width: width?.toDouble(), + height: height.toDouble(), + child: CircularProgressIndicator(), + ), + ), + ), + ), + + ("del" || + "h1" || + "h2" || + "h3" || + "h4" || + "h5" || + "h6" || + "p" || + "ul" || + "ol" || + "sup" || + "sub" || + "li" || + "b" || + "i" || + "u" || + "strong" || + "em" || + "s" || + "code" || + "hr" || + "br" || + "div" || + "table" || + "thead" || + "tbody" || + "tr" || + "th" || + "td" || + "caption" || + "pre" || + "span" || + "details" || + "summary") => + null, + + _ => SizedBox.shrink(), + }; + }, + customStylesBuilder: (element) => { + "width": "auto", + ...Map.fromEntries( + element.attributes + .mapTo?>( + (key, value) => switch (key) { + "data-mx-color" => MapEntry("color", value), + + "data-mx-bg-color" => MapEntry("background-color", value), + + "edited" => MapEntry("display", "block"), + + _ => null, + }, + ) + .nonNulls, + ), + }, + onTapUrl: (url) => + ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(url)), + ); +} diff --git a/lib/widgets/chat_page/html/mention_chip.dart b/lib/widgets/chat_page/html/mention_chip.dart new file mode 100644 index 0000000..f8fdab1 --- /dev/null +++ b/lib/widgets/chat_page/html/mention_chip.dart @@ -0,0 +1,23 @@ +import "package:flutter/material.dart"; +import "package:matrix/matrix.dart"; + +class MentionChip extends StatelessWidget { + final String label; + const MentionChip(this.label, {super.key}); + + @override + Widget build(BuildContext context) => ActionChip( + label: Text( + label.parseIdentifierIntoParts()?.primaryIdentifier ?? label, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + backgroundColor: Theme.of(context).colorScheme.primary, + onPressed: () { + // TODO: Open room or join room dialog, or user popover + showAboutDialog(context: context); + }, + ); +} diff --git a/lib/widgets/html/quoted.dart b/lib/widgets/chat_page/html/quoted.dart similarity index 66% rename from lib/widgets/html/quoted.dart rename to lib/widgets/chat_page/html/quoted.dart index e582b06..6640118 100644 --- a/lib/widgets/html/quoted.dart +++ b/lib/widgets/chat_page/html/quoted.dart @@ -8,9 +8,9 @@ class Quoted extends StatelessWidget { Widget build(BuildContext context) => Container( decoration: BoxDecoration( border: Border( - left: .new(width: 4, color: Theme.of(context).dividerColor), + left: BorderSide(width: 4, color: Theme.of(context).dividerColor), ), ), - child: Padding(padding: .only(left: 8), child: child), + child: Padding(padding: EdgeInsets.only(left: 8), child: child), ); } diff --git a/lib/widgets/html/spoiler_text.dart b/lib/widgets/chat_page/html/spoiler_text.dart similarity index 69% rename from lib/widgets/html/spoiler_text.dart rename to lib/widgets/chat_page/html/spoiler_text.dart index a7a457b..9a42bff 100644 --- a/lib/widgets/html/spoiler_text.dart +++ b/lib/widgets/chat_page/html/spoiler_text.dart @@ -13,15 +13,15 @@ class SpoilerText extends HookWidget { return InkWell( onTap: () => revealed.value = !revealed.value, child: AnimatedContainer( - duration: const .new(milliseconds: 100), - padding: const .symmetric(horizontal: 4, vertical: 2), + duration: const Duration(milliseconds: 100), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: revealed.value ? Colors.transparent : Colors.blueGrey, - borderRadius: .circular(4), + borderRadius: BorderRadius.circular(4), ), child: Text( text, - style: .new(color: revealed.value ? null : Colors.transparent), + style: TextStyle(color: revealed.value ? null : Colors.transparent), ), ), ); diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart new file mode 100644 index 0000000..3e6e82e --- /dev/null +++ b/lib/widgets/chat_page/member_list.dart @@ -0,0 +1,64 @@ +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/avatar_controller.dart"; +import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; + +class MemberList extends ConsumerWidget { + final Room room; + const MemberList(this.room, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => Drawer( + shape: Border(), + child: ref + .watch(MembersController.provider(room)) + .betterWhen( + data: (members) => ListView( + children: [ + AppBar( + scrolledUnderElevation: 0, + leading: Icon(Icons.people), + title: Text("Members"), + actionsPadding: EdgeInsets.only(right: 4), + actions: [ + if (Scaffold.of(context).hasEndDrawer) + IconButton( + onPressed: Scaffold.of(context).closeEndDrawer, + icon: Icon(Icons.close), + ), + ], + ), + ...members + .where( + (membership) => + membership.content["membership"] == + Membership.join.name, + ) + .map( + (member) => ListTile( + leading: AvatarOrHash( + ref + .watch( + AvatarController.provider( + member.content["avatar_url"].toString(), + ), + ) + .whenOrNull(data: (data) => data), + member.content["displayname"].toString(), + headers: room.client.headers, + ), + title: Text( + member.content["displayname"].toString(), + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ), + ); +} diff --git a/lib/widgets/chat_page/mention_overlay.dart b/lib/widgets/chat_page/mention_overlay.dart new file mode 100644 index 0000000..6558a9d --- /dev/null +++ b/lib/widgets/chat_page/mention_overlay.dart @@ -0,0 +1,130 @@ +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/avatar_controller.dart"; +import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/controllers/rooms_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/loading.dart"; + +class MentionOverlay extends ConsumerWidget { + final String? triggerCharacter; + final String query; + final Room room; + final void Function({required String id, required String name}) addTag; + const MentionOverlay( + this.room, { + required this.query, + required this.addTag, + required this.triggerCharacter, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) => Padding( + padding: EdgeInsets.all(8), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: Container( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + padding: EdgeInsets.all(8), + child: switch (triggerCharacter) { + "@" => + ref + .watch(MembersController.provider(room)) + .betterWhen( + data: (members) => ListView( + children: + (query.isEmpty + ? members + : members.where( + (member) => + member.senderId.toLowerCase().contains( + query.toLowerCase(), + ) || + (member.content["displayname"] + as String?) + ?.toLowerCase() + .contains( + query.toLowerCase(), + ) == + true, + )) + .map( + (member) => ListTile( + leading: AvatarOrHash( + ref + .watch( + AvatarController.provider( + member.content["avatar_url"] + .toString(), + ), + ) + .whenOrNull(data: (data) => data), + member.content["displayname"].toString(), + headers: room.client.headers, + ), + title: Text( + member.content["displayname"] as String? ?? + member.senderId, + ), + onTap: () => addTag( + id: member.senderId, + name: member.senderId + .substring(1) + .split(":") + .first, + ), + ), + ) + .toList(), + ), + ), + "#" => + ref + .watch(RoomsController.provider) + .betterWhen( + data: (rooms) => ListView( + children: + (query.isEmpty + ? rooms + : rooms.where( + (room) => room.title.toLowerCase().contains( + query.toLowerCase(), + ), + )) + .map( + (room) => ListTile( + leading: AvatarOrHash( + room.avatar, + room.title, + fallback: Icon(Icons.numbers), + headers: room.roomData.client.headers, + ), + title: Text(room.title), + subtitle: room.roomData.topic.isEmpty + ? null + : Text(room.roomData.topic), + onTap: () => addTag( + id: "[#${room.roomData.getLocalizedDisplayname()}](https://matrix.to/#/${room.roomData.id})", + name: + (room.roomData.canonicalAlias.isEmpty + ? room.roomData.id + : room.roomData.canonicalAlias) + .substring(1) + .split(":") + .first, + ), + ), + ) + .toList(), + ), + ), + _ => Loading(), + }, + ), + ), + ); +} diff --git a/lib/widgets/chat_page/relation_preview.dart b/lib/widgets/chat_page/relation_preview.dart new file mode 100644 index 0000000..07bac4e --- /dev/null +++ b/lib/widgets/chat_page/relation_preview.dart @@ -0,0 +1,79 @@ +import "package:flutter/material.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/avatar_controller.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/models/relation_type.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; + +class RelationPreview extends ConsumerWidget { + final Message? relatedMessage; + final RelationType relationType; + final VoidCallback onDismiss; + final Room room; + const RelationPreview({ + required this.relatedMessage, + required this.relationType, + required this.onDismiss, + required this.room, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (relatedMessage == null) return SizedBox.shrink(); + final theme = Theme.of(context); + + return Container( + color: theme.colorScheme.surfaceContainerHigh, + padding: EdgeInsets.symmetric(horizontal: 8), + child: Row( + spacing: 8, + children: [ + SizedBox(width: 4), + if (relationType == RelationType.edit) + Text( + "Editing message:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + AvatarOrHash( + ref + .watch( + AvatarController.provider( + relatedMessage!.metadata!["avatarUrl"], + ), + ) + .whenOrNull(data: (data) => data), + relatedMessage!.metadata!["displayName"].toString(), + headers: room.client.headers, + height: 16, + ), + Text( + relatedMessage!.metadata?["displayName"] ?? + relatedMessage!.authorId, + style: theme.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Expanded( + child: Text( + (relatedMessage is TextMessage) + ? (relatedMessage as TextMessage).text + : relatedMessage?.metadata?["body"] ?? + relatedMessage?.metadata?["eventType"], + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelMedium, + maxLines: 1, + ), + ), + IconButton( + onPressed: onDismiss, + icon: Icon(Icons.close), + iconSize: 20, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/chat_page/room_appbar.dart b/lib/widgets/chat_page/room_appbar.dart new file mode 100644 index 0000000..17696dd --- /dev/null +++ b/lib/widgets/chat_page/room_appbar.dart @@ -0,0 +1,60 @@ +import "package:flutter/material.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/models/full_room.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/chat_page/room_menu.dart"; + +class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { + final bool isDesktop; + final FullRoom room; + final void Function(BuildContext context) onOpenMemberList; + final void Function(BuildContext context) onOpenDrawer; + const RoomAppbar( + this.room, { + required this.isDesktop, + required this.onOpenMemberList, + required this.onOpenDrawer, + super.key, + }); + + @override + Size get preferredSize => AppBar().preferredSize; + + @override + Widget build(BuildContext context) => Appbar( + leading: isDesktop + ? AvatarOrHash( + room.avatar, + room.title, + height: 24, + fallback: Icon(Icons.numbers), + headers: room.roomData.client.headers, + ) + : DrawerButton(onPressed: () => onOpenDrawer(context)), + scrolledUnderElevation: 0, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(room.title, overflow: TextOverflow.ellipsis), + if (room.roomData.topic.isNotEmpty) + Text( + room.roomData.topic, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + actions: [ + IconButton(onPressed: () {}, icon: Icon(Icons.push_pin)), + IconButton( + onPressed: () => onOpenMemberList(context), + icon: Icon(Icons.people), + ), + RoomMenu(room.roomData), + ], + ); +} diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart new file mode 100644 index 0000000..0190e64 --- /dev/null +++ b/lib/widgets/chat_page/room_chat.dart @@ -0,0 +1,391 @@ +import "package:flutter/material.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_chat_ui/flutter_chat_ui.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:flutter_link_previewer/flutter_link_previewer.dart"; +import "package:flyer_chat_file_message/flyer_chat_file_message.dart"; +import "package:flyer_chat_image_message/flyer_chat_image_message.dart"; +import "package:flyer_chat_system_message/flyer_chat_system_message.dart"; +import "package:flyer_chat_text_message/flyer_chat_text_message.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/selected_room_controller.dart"; +import "package:nexus/controllers/room_chat_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/helpers/extensions/show_context_menu.dart"; +import "package:nexus/models/relation_type.dart"; +import "package:nexus/widgets/chat_page/chat_box.dart"; +import "package:nexus/widgets/chat_page/html/html.dart"; +import "package:nexus/widgets/chat_page/member_list.dart"; +import "package:nexus/widgets/chat_page/room_appbar.dart"; +import "package:nexus/widgets/chat_page/top_widget.dart"; +import "package:nexus/widgets/form_text_input.dart"; +import "package:nexus/widgets/loading.dart"; + +class RoomChat extends HookConsumerWidget { + final bool isDesktop; + final bool showMembersByDefault; + const RoomChat({ + required this.isDesktop, + required this.showMembersByDefault, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final replyToMessage = useState(null); + final memberListOpened = useState(showMembersByDefault); + final relationType = useState(RelationType.reply); + final theme = Theme.of(context); + final danger = theme.colorScheme.error; + + return ref + .watch(SelectedRoomController.provider) + .betterWhen( + data: (room) { + if (room == null) { + return Center( + child: Text( + "Nothing to see here...", + style: theme.textTheme.headlineMedium, + ), + ); + } + final controllerProvider = RoomChatController.provider( + room.roomData, + ); + final notifier = ref.watch(controllerProvider.notifier); + + List getMessageOptions(Message message) => [ + PopupMenuItem( + onTap: () { + replyToMessage.value = message; + relationType.value = RelationType.reply; + }, + child: ListTile( + leading: Icon(Icons.reply), + title: Text("Reply"), + ), + ), + if (message.authorId == room.roomData.client.userID) + PopupMenuItem( + onTap: () { + replyToMessage.value = message; + relationType.value = RelationType.edit; + }, + child: ListTile( + leading: Icon(Icons.edit), + title: Text("Edit"), + ), + ), + if (message.authorId == room.roomData.client.userID || + room.roomData.canRedact) + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (_) { + final deleteReasonController = + useTextEditingController(); + return AlertDialog( + title: Text("Delete Message"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Are you sure you want to delete this message? This can not be reversed.", + ), + SizedBox(height: 12), + FormTextInput( + required: false, + capitalize: true, + controller: deleteReasonController, + title: "Reason for deletion (optional)", + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () async { + notifier.deleteMessage( + message, + reason: deleteReasonController.text, + ); + Navigator.of(context).pop(); + }, + child: Text("Delete"), + ), + ], + ); + }, + ), + ), + child: ListTile( + leading: Icon(Icons.delete), + title: Text("Delete"), + ), + ), + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Report"), + content: Text( + "Report this message to your server administrators, who can take action like banning that user or blocking that server from federating.", + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () { + room.roomData.client.reportEvent( + room.roomData.id, + message.id, + ); + Navigator.of(context).pop(); + }, + child: Text("Report"), + ), + ], + ), + ), + child: ListTile( + leading: Icon(Icons.report, color: danger), + title: Text("Report", style: TextStyle(color: danger)), + ), + ), + ]; + + return Scaffold( + appBar: RoomAppbar( + room, + isDesktop: isDesktop, + onOpenDrawer: (_) => Scaffold.of(context).openDrawer(), + onOpenMemberList: (thisContext) { + memberListOpened.value = !memberListOpened.value; + Scaffold.of(thisContext).openEndDrawer(); + }, + ), + body: Row( + children: [ + Expanded( + child: Column( + children: [ + Expanded( + child: ref + .watch(controllerProvider) + .betterWhen( + data: (controller) => Chat( + currentUserId: room.roomData.client.userID!, + theme: ChatTheme.fromThemeData(theme) + .copyWith( + colors: ChatColors.fromThemeData(theme) + .copyWith( + primary: theme + .colorScheme + .primaryContainer, + onPrimary: theme + .colorScheme + .onPrimaryContainer, + ), + ), + onMessageSecondaryTap: + ( + context, + message, { + required details, + required index, + }) => context.showContextMenu( + globalPosition: details.globalPosition, + children: getMessageOptions(message), + ), + onMessageLongPress: + ( + context, + message, { + required details, + required index, + }) => context.showContextMenu( + globalPosition: details.globalPosition, + children: getMessageOptions(message), + ), + builders: Builders( + loadMoreBuilder: (_) => Loading(), + chatAnimatedListBuilder: (_, itemBuilder) => + ChatAnimatedList( + itemBuilder: itemBuilder, + onEndReached: notifier.loadOlder, + onStartReached: notifier.markRead, + bottomPadding: 72, + ), + composerBuilder: (_) => ChatBox( + relationType: relationType.value, + relatedMessage: replyToMessage.value, + onDismiss: () => + replyToMessage.value = null, + room: room.roomData, + ), + textMessageBuilder: + ( + context, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => FlyerChatTextMessage( + customWidget: Html( + (message.metadata?["formatted"] + as String) + .replaceAllMapped( + RegExp( + regexLink, + caseSensitive: false, + ), + (m) => + "${m.group(0)!}", + ) + .replaceAll("\n", "
") + + ((message.editedAt != null) + ? "(edited)" + : ""), + client: room.roomData.client, + ), + topWidget: TopWidget( + message, + headers: + room.roomData.client.headers, + groupStatus: groupStatus, + ), + message: message, + showTime: true, + index: index, + ), + linkPreviewBuilder: + (_, message, isSentByMe) => LinkPreview( + text: message.text, + backgroundColor: isSentByMe + ? theme.colorScheme.inversePrimary + : theme + .colorScheme + .surfaceContainerLow, + insidePadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + linkPreviewData: + message.linkPreviewData, + onLinkPreviewDataFetched: + (linkPreviewData) => + notifier.updateMessage( + message, + message.copyWith( + linkPreviewData: + linkPreviewData, + ), + ), + ), + imageMessageBuilder: + ( + _, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => FlyerChatImageMessage( + topWidget: TopWidget( + message, + headers: + room.roomData.client.headers, + groupStatus: groupStatus, + alwaysShow: true, + ), + errorBuilder: + (context, error, stackTrace) => + Center( + child: Text( + "Image Failed to Load", + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.error, + ), + ), + ), + message: message, + index: index, + headers: room.roomData.client.headers, + ), + fileMessageBuilder: + ( + _, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => InkWell( + onTap: () => showAboutDialog( + context: context, + ), // TODO: Download + child: FlyerChatFileMessage( + topWidget: TopWidget( + message, + headers: + room.roomData.client.headers, + groupStatus: groupStatus, + ), + message: message, + index: index, + ), + ), + systemMessageBuilder: + ( + _, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => FlyerChatSystemMessage( + message: message, + index: index, + ), + unsupportedMessageBuilder: + ( + _, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => Text( + "${message.authorId} sent ${message.metadata?["eventType"]}", + style: theme.textTheme.labelSmall + ?.copyWith(color: Colors.grey), + ), + ), + resolveUser: notifier.resolveUser, + chatController: controller, + ), + ), + ), + ], + ), + ), + + if (memberListOpened.value == true && showMembersByDefault) + MemberList(room.roomData), + ], + ), + + endDrawer: showMembersByDefault + ? null + : MemberList(room.roomData), + ); + }, + ); + } +} diff --git a/lib/widgets/chat_page/room_menu.dart b/lib/widgets/chat_page/room_menu.dart new file mode 100644 index 0000000..c5fa322 --- /dev/null +++ b/lib/widgets/chat_page/room_menu.dart @@ -0,0 +1,105 @@ +import "package:clipboard/clipboard.dart"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/widgets/form_text_input.dart"; + +class RoomMenu extends StatelessWidget { + final Room room; + const RoomMenu(this.room, {super.key}); + + @override + Widget build(BuildContext context) { + final danger = Theme.of(context).colorScheme.error; + + return PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + onTap: () async { + final link = await room.matrixToInviteLink(); + await FlutterClipboard.copy(link.toString()); + }, + child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), + ), + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Leave Room"), + content: Text( + "Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?", + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + final snackbar = ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("Leaving room..."))); + await room.leave(); + snackbar.close(); + }, + child: Text("Leave"), + ), + ], + ), + ), + child: ListTile( + leading: Icon(Icons.logout, color: danger), + title: Text("Leave", style: TextStyle(color: danger)), + ), + ), + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (_) { + final reasonController = useTextEditingController(); + return AlertDialog( + title: Text("Report"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Report this room to your server administrators, who can take action like banning this room.", + ), + + SizedBox(height: 12), + FormTextInput( + required: false, + capitalize: true, + controller: reasonController, + title: "Reason for deletion (optional)", + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () { + room.client.reportRoom(room.id, reasonController.text); + Navigator.of(context).pop(); + }, + child: Text("Report"), + ), + ], + ); + }, + ), + ), + child: ListTile( + leading: Icon(Icons.report, color: danger), + title: Text("Report", style: TextStyle(color: danger)), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart new file mode 100644 index 0000000..d4bd89d --- /dev/null +++ b/lib/widgets/chat_page/sidebar.dart @@ -0,0 +1,178 @@ +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/key_controller.dart"; +import "package:nexus/controllers/selected_space_controller.dart"; +import "package:nexus/controllers/spaces_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/pages/settings_page.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/chat_page/room_menu.dart"; + +class Sidebar extends HookConsumerWidget { + const Sidebar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedSpaceProvider = KeyController.provider( + KeyController.spaceKey, + ); + final selectedSpace = ref.watch(selectedSpaceProvider); + final selectedSpaceNotifier = ref.watch(selectedSpaceProvider.notifier); + + final selectedRoomController = KeyController.provider( + KeyController.roomKey, + ); + final selectedRoom = ref.watch(selectedRoomController); + final selectedRoomNotifier = ref.watch(selectedRoomController.notifier); + + return Drawer( + shape: Border(), + child: Row( + children: [ + ref + .watch(SpacesController.provider) + .when( + loading: SizedBox.shrink, + error: (error, stack) { + debugPrintStack(label: error.toString(), stackTrace: stack); + throw error; + }, + data: (spaces) { + final indexOfSelected = spaces.indexWhere( + (space) => space.id == selectedSpace, + ); + final selectedIndex = indexOfSelected == -1 + ? 0 + : indexOfSelected; + + return NavigationRail( + scrollable: true, + onDestinationSelected: (value) { + selectedSpaceNotifier.set(spaces[value].id); + selectedRoomNotifier.set( + spaces[value].children.firstOrNull?.roomData.id, + ); + }, + destinations: spaces + .map( + (space) => NavigationRailDestination( + icon: AvatarOrHash( + space.avatar, + fallback: space.icon == null + ? null + : Icon(space.icon), + space.title, + headers: space.client.headers, + hasBadge: + space.children.firstWhereOrNull( + (room) => room.roomData.hasNewMessages, + ) != + null, + ), + label: Text(space.title), + padding: EdgeInsets.only(top: 4), + ), + ) + .toList(), + selectedIndex: selectedIndex, + trailingAtBottom: true, + trailing: Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Column( + spacing: 8, + children: [ + IconButton( + onPressed: () => Navigator.of(context).push( + // TODO: join or create room/space + MaterialPageRoute(builder: (_) => SettingsPage()), + ), + icon: Icon(Icons.add), + ), + IconButton( + onPressed: () => Navigator.of(context).push( + // TODO: explore public rooms/spaces + MaterialPageRoute(builder: (_) => SettingsPage()), + ), + icon: Icon(Icons.explore), + ), + IconButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => SettingsPage()), + ), + icon: Icon(Icons.settings), + ), + ], + ), + ), + ); + }, + ), + Expanded( + child: ref + .watch(SelectedSpaceController.provider) + .betterWhen( + data: (space) { + final indexOfSelected = space.children.indexWhere( + (room) => room.roomData.id == selectedRoom, + ); + final selectedIndex = indexOfSelected == -1 + ? space.children.isEmpty + ? null + : 0 + : indexOfSelected; + + return Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: AvatarOrHash( + space.avatar, + fallback: space.icon == null + ? null + : Icon(space.icon), + space.title, + headers: space.client.headers, + ), + title: Text( + space.title, + overflow: TextOverflow.ellipsis, + ), + backgroundColor: Colors.transparent, + actions: [ + if (space.roomData != null) RoomMenu(space.roomData!), + ], + ), + body: NavigationRail( + scrollable: true, + backgroundColor: Colors.transparent, + extended: true, + selectedIndex: selectedIndex, + destinations: space.children + .map( + (room) => NavigationRailDestination( + label: Text(room.title), + icon: AvatarOrHash( + hasBadge: room.roomData.hasNewMessages, + room.avatar, + room.title, + fallback: selectedSpace == "dms" + ? null + : Icon(Icons.numbers), + headers: space.client.headers, + ), + ), + ) + .toList(), + onDestinationSelected: (value) => selectedRoomNotifier + .set(space.children[value].roomData.id), + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/chat_page/top_widget.dart b/lib/widgets/chat_page/top_widget.dart new file mode 100644 index 0000000..733dcc7 --- /dev/null +++ b/lib/widgets/chat_page/top_widget.dart @@ -0,0 +1,116 @@ +import "dart:math"; +import "package:flutter/material.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_chat_ui/flutter_chat_ui.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/widgets/chat_page/html/quoted.dart"; + +class TopWidget extends ConsumerWidget { + final Message message; + final bool alwaysShow; + final Map headers; + final MessageGroupStatus? groupStatus; + const TopWidget( + this.message, { + required this.headers, + required this.groupStatus, + this.alwaysShow = false, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Builder( + builder: (_) { + final replyMessage = message.metadata?["reply"] as TextMessage?; + + if (replyMessage == null) return SizedBox.shrink(); + final smallerText = message is TextMessage + ? replyMessage.text.substring( + 0, + min( + max( + max( + (message as TextMessage).text.length - 20, + message.metadata?["displayName"].length, + ), + 5, + ), + replyMessage.text.length, + ), + ) + : null; + final replyText = + (smallerText == null || + smallerText.length == replyMessage.text.length) + ? replyMessage.text + : "$smallerText..."; + + return Padding( + padding: EdgeInsets.only(bottom: 12), + child: InkWell( + // TODO: Scroll to original message + onTap: () => showAboutDialog(context: context), + child: Quoted( + Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Avatar( + userId: replyMessage.authorId, + headers: headers, + size: 16, + ), + Flexible( + child: Text( + replyMessage.metadata?["displayName"] ?? + replyMessage.authorId, + style: Theme.of(context).textTheme.labelMedium + ?.copyWith(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + Flexible( + child: Text( + replyText, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelMedium, + maxLines: 1, + ), + ), + ], + ), + ), + ), + ); + }, + ), + if (alwaysShow || + groupStatus?.isFirst != false || + message.metadata?["reply"] != null) + InkWell( + onTap: () => + showAboutDialog(context: context), // TODO: Show user profile + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Avatar(userId: message.authorId, headers: headers), + Flexible( + child: Text( + message.metadata?["displayName"] ?? message.authorId, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + SizedBox(height: 4), + ], + ); +} diff --git a/lib/widgets/composer/composer.dart b/lib/widgets/composer/composer.dart deleted file mode 100644 index c63d939..0000000 --- a/lib/widgets/composer/composer.dart +++ /dev/null @@ -1,277 +0,0 @@ -import "dart:io"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:file_selector/file_selector.dart"; -import "package:flutter/material.dart"; -import "package:flutter/services.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:fluttertagger/fluttertagger.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/attachment.dart"; -import "package:nexus/controllers/image_picker.dart"; -import "package:nexus/controllers/power_level.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/composer/mention_overlay.dart"; -import "package:nexus/widgets/composer/relation_preview.dart"; -import "package:nexus/widgets/emoji_picker_button.dart"; -import "package:nexus/main.dart"; - -class Composer extends HookConsumerWidget { - final String roomId; - final Event? relatedEvent; - final RelationType relationType; - final VoidCallback onDismiss; - final FocusNode? node; - final Future Function( - String text, { - required bool shouldMention, - required IList tags, - }) - onSend; - const Composer( - this.roomId, { - required this.relatedEvent, - required this.relationType, - required this.onDismiss, - required this.onSend, - this.node, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final controller = useRef(FlutterTaggerController()); - final triggerCharacter = useState(""); - final shouldMention = useState(true); - final query = useState(""); - - if (relationType == .edit && controller.value.text.isEmpty) { - controller.value.text = - relatedEvent?.localContent?.editSource ?? - switch (relatedEvent?.content) { - TextMessageContent(:final body) => body, - _ => "", - }; - } - - final attachment = ref.watch(AttachmentController.provider(roomId)); - - void send() { - if (controller.value.text.isEmpty && attachment == null) return; - onSend( - controller.value.formattedText, - shouldMention: shouldMention.value, - tags: .new(controller.value.tags), - ); - - onDismiss(); - controller.value.text = ""; - } - - final style = TextStyle( - color: theme.colorScheme.primary, - fontWeight: .bold, - ); - - return Padding( - padding: .all(12), - child: Column( - children: [ - if (attachment != null) - Card( - margin: .only(bottom: 8), - child: ListTile( - leading: attachment.$2 == null - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(), - ) - : Icon(Icons.file_copy), - title: Text(attachment.$1), - trailing: IconButton( - onPressed: () => - ref.invalidate(AttachmentController.provider(roomId)), - icon: Icon(Icons.close), - ), - ), - ), - ClipRRect( - borderRadius: .all(.circular(12)), - child: Column( - children: [ - RelationPreview( - relatedEvent, - shouldMention: shouldMention.value, - toggleShouldMention: () => - shouldMention.value = !shouldMention.value, - relationType: relationType, - onDismiss: onDismiss, - ), - Container( - color: theme.colorScheme.surfaceContainerHighest, - padding: .symmetric(horizontal: 8), - child: Row( - spacing: 8, - mainAxisAlignment: .center, - children: - ref.watch( - PowerLevelController.provider( - .new(eventType: .message, roomId: roomId), - ), - ) - ? [ - EmojiPickerButton( - context: context, - onSelection: (_) => node?.requestFocus(), - controller: controller.value, - ), - PopupMenuButton( - tooltip: "Add media", - enabled: attachment == null, - itemBuilder: (context) => [ - if (Platform.isAndroid || Platform.isIOS) - PopupMenuItem( - child: ListTile( - title: Text("Camera"), - leading: Icon(Icons.add_a_photo), - ), - onTap: () async => ref - .watch( - AttachmentController.provider( - roomId, - ).notifier, - ) - .add( - (await ref - .watch( - ImagePickerController.provider, - ) - .pickImage(source: .camera))!, - ) - .onError(showError), - ), - PopupMenuItem( - child: ListTile( - title: Text("Gallery"), - leading: Icon(Icons.add_photo_alternate), - ), - onTap: () async => ref - .watch( - AttachmentController.provider( - roomId, - ).notifier, - ) - .add( - (await ref - .watch( - ImagePickerController.provider, - ) - .pickImage(source: .gallery))!, - ) - .onError(showError), - ), - PopupMenuItem( - onTap: () async => ref - .watch( - AttachmentController.provider( - roomId, - ).notifier, - ) - .add((await openFile())!) - .onError(showError), - child: ListTile( - title: Text("Files"), - leading: Icon(Icons.attachment), - ), - ), - ], - icon: Icon(Icons.add), - ), - Expanded( - child: FlutterTagger( - triggerStrategy: .eager, - overlay: MentionOverlay( - roomId, - query: query.value, - triggerCharacter: triggerCharacter.value, - addTag: ({required id, required name}) { - controller.value.addTag(id: id, name: name); - node?.requestFocus(); - }, - ), - controller: controller.value, - onSearch: (newQuery, newTriggerCharacter) { - triggerCharacter.value = newTriggerCharacter; - query.value = newQuery; - }, - triggerCharacterAndStyles: { - "@": style, - "#": style, - }, - builder: (context, key) => Focus( - onKeyEvent: (_, event) { - if (event is KeyDownEvent && - event.logicalKey == - LogicalKeyboardKey.enter) { - final shiftPressed = HardwareKeyboard - .instance - .isShiftPressed; - - if (!shiftPressed) { - send(); - return KeyEventResult.handled; - } - } - - return KeyEventResult.ignored; - }, - child: TextField( - maxLines: 12, - minLines: 1, - autofocus: true, - decoration: .new( - hintText: "Your message here...", - border: .none, - ), - controller: controller.value, - key: key, - focusNode: node, - ), - ), - ), - ), - IconButton( - onPressed: - attachment != null && attachment.$2 == null - ? null - : send, - icon: Icon(Icons.send), - tooltip: "Send message", - ), - ] - : [ - Expanded( - child: Padding( - padding: .symmetric( - horizontal: 8, - vertical: 12, - ), - child: Text( - "You don't have permission to send messages in this room...", - ), - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/widgets/composer/mention_overlay.dart b/lib/widgets/composer/mention_overlay.dart deleted file mode 100644 index ca4f95a..0000000 --- a/lib/widgets/composer/mention_overlay.dart +++ /dev/null @@ -1,158 +0,0 @@ -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/members_by_status.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/via.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/loading.dart"; - -class MentionOverlay extends ConsumerWidget { - final String? triggerCharacter; - final String query; - final String roomId; - final void Function({required String id, required String name}) addTag; - const MentionOverlay( - this.roomId, { - required this.query, - required this.addTag, - required this.triggerCharacter, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final rooms = ref.watch(RoomsController.provider); - - return Padding( - padding: .all(8), - child: ClipRRect( - borderRadius: .all(.circular(12)), - child: Container( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - padding: .all(8), - child: switch (triggerCharacter) { - "@" => - ref - .watch( - MembersByStatusController.provider( - .new(roomId: roomId, status: .join), - ), - ) - .betterWhen( - data: (members) => ListView( - children: - (query.isEmpty - ? members - : members.where( - (member) => - member.stateKey - ?.toLowerCase() - .contains( - query.toLowerCase(), - ) == - true || - switch (member.content) { - MembershipContent( - :final displayName, - ) => - displayName - ?.toLowerCase() - .contains( - query.toLowerCase(), - ) == - true, - _ => false, - }, - )) - .map( - (member) => switch (member.content) { - MembershipContent( - :final displayName, - :final avatarUrl, - ) => - Material( - color: Colors.transparent, - child: ListTile( - leading: AvatarOrHash( - avatarUrl, - displayName ?? - member.stateKey!.localpart, - ), - title: Text( - displayName ?? - member.stateKey!.localpart, - ), - subtitle: Text(member.stateKey!), - onTap: () => addTag( - id: "[@$displayName](matrix:u/${member.stateKey!.substring(1)})", - name: member.stateKey!.localpart, - ), - ), - ), - _ => SizedBox.shrink(), - }, - ) - .toList(), - ), - ), - "#" => ListView( - children: - (query.isEmpty - ? rooms.values - : rooms.values.where( - (room) => - (room.metadata?.name ?? - room.metadata?.id ?? - "") - .toLowerCase() - .contains(query.toLowerCase()), - )) - .map((room) { - final name = - room.metadata?.name ?? - room.metadata?.canonicalAlias ?? - room.metadata?.id ?? - "Unknown Room"; - return Material( - color: Colors.transparent, - child: ListTile( - leading: AvatarOrHash( - room.metadata?.avatar, - name, - fallback: Icon(Icons.numbers), - ), - title: Text(name), - subtitle: room.metadata?.topic == null - ? null - : Text(room.metadata!.topic!, maxLines: 1), - onTap: () { - final vias = ref.watch( - ViaController.provider(room), - ); - addTag( - id: "[#$name](matrix:roomid/${room.metadata?.id.substring(1)}$vias)", - name: - (room.metadata?.canonicalAlias ?? - room.metadata?.id) - ?.substring(1) - .split(":") - .first ?? - "", - ); - }, - ), - ); - }) - .toList(), - ), - - _ => Loading(), - }, - ), - ), - ); - } -} diff --git a/lib/widgets/composer/relation_preview.dart b/lib/widgets/composer/relation_preview.dart deleted file mode 100644 index c9cc271..0000000 --- a/lib/widgets/composer/relation_preview.dart +++ /dev/null @@ -1,66 +0,0 @@ -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/event_preview.dart"; - -class RelationPreview extends ConsumerWidget { - final Event? relatedEvent; - final RelationType relationType; - final VoidCallback onDismiss; - final bool shouldMention; - final VoidCallback toggleShouldMention; - - const RelationPreview( - this.relatedEvent, { - required this.relationType, - required this.onDismiss, - required this.shouldMention, - required this.toggleShouldMention, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (relatedEvent == null) return SizedBox.shrink(); - final theme = Theme.of(context); - - return Container( - color: theme.colorScheme.surfaceContainerHigh, - padding: .symmetric(horizontal: 12), - child: Row( - spacing: 8, - children: [ - if (relationType == .edit) - Text("Editing message:", style: .new(fontWeight: .bold)), - - Expanded( - child: Padding( - padding: .symmetric(vertical: 8), - child: EventPreview(relatedEvent!), - ), - ), - - if (relationType == .reply) - TextButton( - onPressed: toggleShouldMention, - child: Text( - shouldMention ? "@On" : "@Off", - style: TextStyle( - fontWeight: .w900, - color: shouldMention ? null : Theme.of(context).disabledColor, - ), - ), - ), - - IconButton( - tooltip: "Cancel ${relationType == .edit ? "edit" : "reply"}", - onPressed: onDismiss, - icon: const Icon(Icons.close), - iconSize: 20, - ), - ], - ), - ); - } -} diff --git a/lib/widgets/divider_text.dart b/lib/widgets/divider_text.dart index 2b0f9bd..ca78844 100644 --- a/lib/widgets/divider_text.dart +++ b/lib/widgets/divider_text.dart @@ -1,5 +1,4 @@ import "package:flutter/material.dart"; -import "package:nexus/widgets/divider_widget.dart"; class DividerText extends StatelessWidget { final String text; @@ -7,6 +6,24 @@ class DividerText extends StatelessWidget { const DividerText(this.text, {super.key}); @override - Widget build(BuildContext context) => - DividerWidget(Text(text, style: Theme.of(context).textTheme.labelLarge)); + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) => Row( + children: [ + SizedBox( + width: 16, + child: Divider(color: Theme.of(context).colorScheme.onSurface), + ), + ConstrainedBox( + constraints: BoxConstraints(maxWidth: constraints.maxWidth - 32), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(text, style: Theme.of(context).textTheme.labelLarge), + ), + ), + Expanded( + child: Divider(color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ); } diff --git a/lib/widgets/divider_widget.dart b/lib/widgets/divider_widget.dart deleted file mode 100644 index 6f13bd4..0000000 --- a/lib/widgets/divider_widget.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:flutter/material.dart"; - -class DividerWidget extends StatelessWidget { - final Widget widget; - const DividerWidget(this.widget, {super.key}); - - @override - Widget build(BuildContext context) => LayoutBuilder( - builder: (_, constraints) => Row( - children: [ - SizedBox( - width: 16, - child: Divider(color: Theme.of(context).colorScheme.onSurface), - ), - ConstrainedBox( - constraints: .new(maxWidth: constraints.maxWidth - 32), - child: Padding(padding: const .all(8), child: widget), - ), - Expanded( - child: Divider(color: Theme.of(context).colorScheme.onSurface), - ), - ], - ), - ); -} diff --git a/lib/widgets/emoji_picker_button.dart b/lib/widgets/emoji_picker_button.dart deleted file mode 100644 index 2ac906a..0000000 --- a/lib/widgets/emoji_picker_button.dart +++ /dev/null @@ -1,52 +0,0 @@ -import "package:emoji_text_field/emoji_text_field.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/emoji.dart"; - -class EmojiPickerButton extends HookConsumerWidget { - final TextEditingController? controller; - final void Function(String emoji)? onSelection; - final VoidCallback? onPressed; - final BuildContext context; - const EmojiPickerButton({ - this.controller, - this.onPressed, - this.onSelection, - required this.context, - super.key, - }); - - @override - Widget build(_, WidgetRef ref) => IconButton( - onPressed: () async { - onPressed?.call(); - final controller = this.controller ?? .new(); - - final emojis = await ref.watch(EmojiController.provider.future); - if (context.mounted) { - showModalBottomSheet( - context: context, - builder: (context) => EmojiKeyboardView( - config: .new( - showRecentTab: false, - customCategories: emojis.$1.unlock, - customKeywords: emojis.$2.unlock, - backgroundColor: Theme.of(context).colorScheme.surfaceContainer, - height: 600, - ), - textController: controller - ..addListener(() { - // Without this, there will sometimes be a debugLocked is not true error sometimes - // It might be preferable to use a microtask instead of a `Future.delayed`. - Future.delayed(.zero, () { - if (context.mounted) Navigator.of(context).pop(); - }); - onSelection?.call(controller.text); - }), - ), - ); - } - }, - icon: Icon(Icons.emoji_emotions), - ); -} diff --git a/lib/widgets/error_dialog.dart b/lib/widgets/error_dialog.dart index 9b62200..b016a8b 100644 --- a/lib/widgets/error_dialog.dart +++ b/lib/widgets/error_dialog.dart @@ -21,12 +21,11 @@ class ErrorDialog extends ConsumerWidget { onPressed: () => ref.invalidate(provider!), child: const Text("Try Again"), ), - if (Navigator.of(context).canPop()) - TextButton( - onPressed: () => - Navigator.of(context).popUntil((route) => route.isFirst), - child: const Text("Go Back"), - ), + TextButton( + onPressed: () => + Navigator.of(context).popUntil((route) => route.isFirst), + child: const Text("Go Back"), + ), ], ); } diff --git a/lib/widgets/event_preview.dart b/lib/widgets/event_preview.dart deleted file mode 100644 index 7a40a75..0000000 --- a/lib/widgets/event_preview.dart +++ /dev/null @@ -1,37 +0,0 @@ -import "package:flutter/material.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/lazy_loading/message_avatar.dart"; -import "package:nexus/widgets/lazy_loading/message_displayname.dart"; -import "package:nexus/widgets/renderers/event.dart"; - -class EventPreview extends StatelessWidget { - final Event event; - const EventPreview(this.event, {super.key}); - - @override - Widget build(BuildContext context) => IgnorePointer( - child: Padding( - padding: .symmetric(vertical: 4), - child: Row( - mainAxisSize: .min, - spacing: 12, - children: [ - if (event.content is MessageContent) MessageAvatar(event), - - Flexible( - child: Wrap( - crossAxisAlignment: .center, - spacing: 8, - runSpacing: 2, - children: [ - if (event.content is MessageContent) MessageDisplayname(event), - EventRenderer(event, textOnly: true, maxLines: 1), - ], - ), - ), - ], - ), - ), - ); -} diff --git a/lib/widgets/expandable_image.dart b/lib/widgets/expandable_image.dart deleted file mode 100644 index 4990458..0000000 --- a/lib/widgets/expandable_image.dart +++ /dev/null @@ -1,58 +0,0 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:m3e_buttons/m3e_buttons.dart"; -import "package:nexus/controllers/cross_cache.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/widgets/error_dialog.dart"; - -class ExpandableImage extends ConsumerWidget { - final Widget child; - final String? source; - const ExpandableImage(this.source, {required this.child, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => InkWell( - onTap: source == null - ? null - : () => showDialog( - context: context, - builder: (_) => SafeArea( - child: Stack( - children: [ - Positioned.fill( - child: GestureDetector( - onTap: Navigator.of(context).pop, - child: InteractiveViewer( - maxScale: 10, - child: Image( - errorBuilder: (_, error, stackTrace) => ErrorDialog( - "Loading failed for $source\nError: $error", - stackTrace, - ), - image: CachedNetworkImage( - source!, - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), - ), - ), - ), - ), - Align( - alignment: .topRight, - child: Padding( - padding: .all(32), - child: M3EButton( - onPressed: Navigator.of(context).pop, - child: Icon(Icons.close), - ), - ), - ), - ], - ), - ), - ), - child: child, - ); -} diff --git a/lib/widgets/file_card.dart b/lib/widgets/file_card.dart deleted file mode 100644 index afdad89..0000000 --- a/lib/widgets/file_card.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:flutter/material.dart"; -import "package:nexus/helpers/extensions/size_to_string.dart"; -import "package:nexus/models/info/file.dart"; - -class FileCard extends StatelessWidget { - final Uri uri; - final FileInfo? info; - final String? filename; - const FileCard(this.uri, this.info, {this.filename, super.key}); - - @override - Widget build(BuildContext context) => SizedBox( - width: 320, - child: Card( - color: Theme.of(context).colorScheme.surfaceContainer, - child: ListTile( - leading: Icon(Icons.file_copy), - title: Text(filename ?? "file", maxLines: 1, overflow: .ellipsis), - subtitle: info?.size == null ? null : Text(info!.size!.sizeAsString), - // TODO: Downloading files - trailing: IconButton(onPressed: null, icon: Icon(Icons.download)), - ), - ), - ); -} diff --git a/lib/widgets/form_text_input.dart b/lib/widgets/form_text_input.dart new file mode 100644 index 0000000..492439b --- /dev/null +++ b/lib/widgets/form_text_input.dart @@ -0,0 +1,80 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; + +class FormTextInput extends StatelessWidget { + final List extraValidators; + final TextEditingController? controller; + final TextInputType keyboardType; + final String? initialValue; + final bool readOnly; + final bool obscure; + final String? title; + final int? minLines; + final int? maxLength; + final bool outlined; + final int? maxLines; + final bool capitalize; + final bool required; + final bool autocorrect; + final void Function()? onTap; + final Widget? trailing; + final InputBorder? border; + final List? formatters; + + const FormTextInput({ + super.key, + this.border, + this.controller, + this.title, + this.obscure = false, + this.readOnly = false, + this.extraValidators = const [], + this.keyboardType = TextInputType.text, + this.initialValue, + this.minLines, + this.capitalize = false, + this.maxLength, + this.formatters, + this.maxLines = 1, + this.outlined = true, + this.trailing, + this.onTap, + this.autocorrect = true, + this.required = true, + }); + + @override + Widget build(BuildContext context) => TextFormField( + controller: controller, + keyboardType: keyboardType, + readOnly: readOnly, + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, + inputFormatters: formatters, + textCapitalization: capitalize + ? TextCapitalization.sentences + : TextCapitalization.none, + initialValue: initialValue, + autocorrect: autocorrect, + obscureText: obscure, + onTap: onTap, + decoration: InputDecoration( + labelText: title, + border: border ?? (outlined ? null : const UnderlineInputBorder()), + suffixIcon: trailing, + ), + validator: (value) { + if ((value?.isEmpty ?? true) && required) { + return "This field is required"; + } + + for (final validator in extraValidators) { + final reason = validator(value!); + if (reason != null) return reason; + } + + return null; + }, + ); +} diff --git a/lib/widgets/highlight_wrapper.dart b/lib/widgets/highlight_wrapper.dart deleted file mode 100644 index c7e568e..0000000 --- a/lib/widgets/highlight_wrapper.dart +++ /dev/null @@ -1,20 +0,0 @@ -import "package:flutter/material.dart"; - -class HighlightWrapper extends StatelessWidget { - final Widget child; - final bool isHighlighted; - const HighlightWrapper(this.child, {this.isHighlighted = false, super.key}); - - @override - Widget build(BuildContext context) => ClipRRect( - borderRadius: .all(.circular(12)), - child: AnimatedContainer( - padding: isHighlighted ? .all(8) : .all(0), - color: isHighlighted - ? Theme.of(context).colorScheme.onSurface.withAlpha(50) - : Colors.transparent, - duration: .new(milliseconds: 250), - child: Material(color: Colors.transparent, child: child), - ), - ); -} diff --git a/lib/widgets/html/html.dart b/lib/widgets/html/html.dart deleted file mode 100644 index 4113eac..0000000 --- a/lib/widgets/html/html.dart +++ /dev/null @@ -1,157 +0,0 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/cross_cache.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/extensions/link_to_mention.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/helpers/launch_helper.dart"; -import "package:nexus/widgets/expandable_image.dart"; -import "package:nexus/widgets/html/mention_chip.dart"; -import "package:nexus/widgets/html/spoiler_text.dart"; -import "package:nexus/widgets/html/code_block.dart"; -import "package:nexus/widgets/html/quoted.dart"; - -class Html extends ConsumerWidget { - final String html; - final String? roomId; - final TextStyle? textStyle; - const Html(this.html, {this.roomId, this.textStyle, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => HtmlWidget( - html, - buildAsync: false, - textStyle: textStyle, - customWidgetBuilder: (element) { - if (element.attributes.keys.contains("data-mx-profile-fallback")) { - return SizedBox.shrink(); - } - - if (element.attributes.keys.contains("data-mx-spoiler")) { - return InlineCustomWidget(child: SpoilerText(text: element.text)); - } - - final height = - int.tryParse(element.attributes["height"] ?? "") ?? - (element.attributes.keys.contains("data-mx-emoticon") ? 32 : null) ?? - 300; - final width = int.tryParse(element.attributes["width"] ?? ""); - final src = Uri.tryParse(element.attributes["src"] ?? "") - ?.mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, - ), - ) ?? - "", - ) - .toString(); - - return switch (element.localName) { - "code" => - element.parent?.localName == "pre" - ? CodeBlock( - element.text, - lang: element.className.replaceAll("language-", ""), - ) - : null, - - "blockquote" => Quoted( - Html(element.innerHtml, textStyle: textStyle, roomId: roomId), - ), - - "a" => - element.attributes["href"]?.mention == null - ? null - : InlineCustomWidget( - child: MentionChip(element.attributes["href"]!, roomId), - ), - - "img" => - src == null - ? SizedBox.shrink() - : InlineCustomWidget( - alignment: PlaceholderAlignment.middle, - child: ExpandableImage( - src, - child: Image( - image: CachedNetworkImage( - src, - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), - errorBuilder: (_, error, _) => Text( - "Image Failed to Load", - style: .new(color: Theme.of(context).colorScheme.error), - ), - height: height.toDouble(), - width: width?.toDouble(), - loadingBuilder: (_, child, loadingProgress) => - loadingProgress == null - ? child - : CircularProgressIndicator(), - ), - ), - ), - - // Allowed elements list - ("del" || - "h1" || - "h2" || - "h3" || - "h4" || - "h5" || - "h6" || - "p" || - "ul" || - "ol" || - "sup" || - "sub" || - "li" || - "b" || - "i" || - "u" || - "strong" || - "em" || - "s" || - "code" || - "hr" || - "br" || - "div" || - "table" || - "thead" || - "tbody" || - "tr" || - "th" || - "td" || - "caption" || - "pre" || - "span" || - "details" || - "summary") => - null, - - _ => SizedBox.shrink(), - }; - }, - customStylesBuilder: (element) => { - "width": "auto", - ...Map.fromEntries( - element.attributes - .mapTo?>( - (key, value) => switch (key) { - "data-mx-color" => .new("color", value), - "data-mx-bg-color" => .new("background-color", value), - _ => null, - }, - ) - .nonNulls, - ), - }, - onTapUrl: (url) => ref.watch(LaunchHelper.provider).launchUrl(.parse(url)), - ); -} diff --git a/lib/widgets/html/mention_chip.dart b/lib/widgets/html/mention_chip.dart deleted file mode 100644 index f6105e3..0000000 --- a/lib/widgets/html/mention_chip.dart +++ /dev/null @@ -1,48 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/user.dart"; -import "package:nexus/helpers/extensions/link_to_mention.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; - -class MentionChip extends ConsumerWidget { - final String? roomId; - final String content; - const MentionChip(this.content, this.roomId, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mention = content.mention; - final membership = mention?.startsWith("@") == true - ? ref - .watch( - UserController.provider(.new(roomId: roomId, userId: mention!)), - ) - .whenOrNull(data: (data) => data) - : null; - - return mention == null - ? SizedBox.shrink() - : InkWell( - onTap: () { - if (membership != null) { - context.showUserPopover(membership, mention, roomId: roomId); - } - }, - child: IgnorePointer( - child: Chip( - label: Text( - (membership?.displayName == null - ? null - : "@${membership!.displayName}") ?? - mention, - style: .new( - fontWeight: .bold, - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ), - ); - } -} diff --git a/lib/widgets/join_dialog.dart b/lib/widgets/join_dialog.dart deleted file mode 100644 index 50b36f2..0000000 --- a/lib/widgets/join_dialog.dart +++ /dev/null @@ -1,138 +0,0 @@ -import "package:collection/collection.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/spaces.dart"; -import "package:nexus/helpers/extensions/link_to_mention.dart"; - -class JoinDialog extends HookWidget { - final WidgetRef ref; - const JoinDialog(this.ref, {super.key}); - - @override - Widget build(BuildContext context) { - final roomAlias = useTextEditingController(); - return AlertDialog( - title: Text("Join a Room"), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text("Enter the room alias, Matrix URI, or Matrix.to link."), - SizedBox(height: 12), - TextField( - controller: roomAlias, - decoration: .new(hintText: "#room:server"), - ), - ], - ), - actions: [ - TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - if (context.mounted) { - final roomIdOrAlias = roomAlias.text.mention ?? roomAlias.text; - - final scaffoldMessenger = ScaffoldMessenger.of(context); - - final snackbar = scaffoldMessenger.showSnackBar( - .new( - content: Text("Joining room $roomIdOrAlias."), - duration: Duration(days: 999), - ), - ); - - try { - final id = await ref - .watch(ClientController.provider.notifier) - .joinRoom( - .new( - roomIdOrAlias: roomIdOrAlias, - via: .new( - Uri.tryParse( - roomAlias.text.replaceAll("/#", ""), - )?.queryParametersAll["via"] ?? - [], - ), - ), - ); - - snackbar.close(); - - scaffoldMessenger.showSnackBar( - .new( - content: Text("Room $roomIdOrAlias successfully joined."), - action: .new( - label: "Open", - onPressed: () async { - final spaces = ref.watch(SpacesController.provider); - final space = spaces.firstWhereOrNull( - (space) => space.id == id, - ); - - await ref - .watch( - KeyController.provider( - KeyController.spaceKey, - ).notifier, - ) - .set( - space?.id ?? - spaces - .firstWhere( - (space) => - space.children.any( - (child) => - child.metadata?.id == id, - ) || - space.subSpaces.any( - (child) => - child.room.metadata?.id == id, - ), - ) - .id, - ); - - if (space == null) { - await ref - .watch( - KeyController.provider( - KeyController.roomKey, - ).notifier, - ) - .set(id); - } - }, - ), - ), - ); - } catch (error) { - snackbar.close(); - if (context.mounted) { - scaffoldMessenger.showSnackBar( - .new( - backgroundColor: Theme.of( - context, - ).colorScheme.errorContainer, - content: Text( - error.toString(), - style: .new( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ), - ); - } - } - } - }, - child: Text("Join"), - ), - ], - ); - } -} diff --git a/lib/widgets/lazy_loading/message_avatar.dart b/lib/widgets/lazy_loading/message_avatar.dart deleted file mode 100644 index 93c2554..0000000 --- a/lib/widgets/lazy_loading/message_avatar.dart +++ /dev/null @@ -1,29 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/author.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; - -class MessageAvatar extends ConsumerWidget { - final Event event; - final double height; - const MessageAvatar(this.event, {this.height = 24, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch( - AuthorController.provider(event), - )) { - AsyncData(:final value) || AsyncLoading(:final value?) => InkWell( - onTap: () => - context.showUserPopover(value, event.sender, roomId: event.roomId), - child: AvatarOrHash( - value.avatarUrl, - value.displayName ?? event.sender.localpart, - height: height, - ), - ), - _ => AvatarOrHash(null, event.sender.localpart, height: height), - }; -} diff --git a/lib/widgets/lazy_loading/message_displayname.dart b/lib/widgets/lazy_loading/message_displayname.dart deleted file mode 100644 index bd5733c..0000000 --- a/lib/widgets/lazy_loading/message_displayname.dart +++ /dev/null @@ -1,62 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/author.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/helpers/extensions/string_to_color.dart"; -import "package:nexus/models/event.dart"; - -class MessageDisplayname extends ConsumerWidget { - final Event event; - final TextStyle? style; - final bool clickable; - const MessageDisplayname( - this.event, { - this.clickable = true, - this.style, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch( - AuthorController.provider(event), - )) { - AsyncData(:final value) || AsyncLoading(:final value?) => InkWell( - onTap: clickable - ? () => context.showUserPopover( - value, - event.sender, - roomId: event.roomId, - ) - : null, - child: Wrap( - spacing: 4, - crossAxisAlignment: .center, - children: [ - Text( - value.displayName ?? event.sender.localpart, - style: - style ?? .new(color: event.sender.colorHash, fontWeight: .bold), - maxLines: 1, - overflow: .ellipsis, - ), - - if (event.pmp != null) - Text( - "(via ${event.sender})", - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: event.sender.colorHash, - fontWeight: .bold, - ), - maxLines: 1, - overflow: .ellipsis, - ), - ], - ), - ), - _ => Text( - event.sender.localpart, - style: .new(color: event.sender.colorHash, fontWeight: .bold), - ), - }; -} diff --git a/lib/widgets/linkified_text.dart b/lib/widgets/linkified_text.dart deleted file mode 100644 index 653248c..0000000 --- a/lib/widgets/linkified_text.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_linkify/flutter_linkify.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/helpers/launch_helper.dart"; - -class LinkifiedText extends ConsumerWidget { - final String text; - final int? maxLines; - final TextStyle? style; - const LinkifiedText(this.text, {this.maxLines, this.style, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => Linkify( - text: text, - maxLines: maxLines, - style: style, - options: .new(humanize: false), - onOpen: (link) => - ref.watch(LaunchHelper.provider).launchUrl(.parse(link.url)), - linkStyle: .new(color: Theme.of(context).colorScheme.primary), - overflow: maxLines == null ? null : .ellipsis, - ); -} diff --git a/lib/widgets/loading.dart b/lib/widgets/loading.dart index fc84563..aadc43c 100644 --- a/lib/widgets/loading.dart +++ b/lib/widgets/loading.dart @@ -1,14 +1,13 @@ import "package:flutter/material.dart"; class Loading extends StatelessWidget { - final double? height; - const Loading({this.height, super.key}); + const Loading({super.key}); @override - Widget build(BuildContext context) => Center( - child: Padding( - padding: .all(16), - child: SizedBox(height: height, child: CircularProgressIndicator()), - ), - ); + Widget build(BuildContext context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(), + ), + ); } diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart deleted file mode 100644 index 92bb179..0000000 --- a/lib/widgets/member_list.dart +++ /dev/null @@ -1,189 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:m3e_buttons/m3e_buttons.dart"; -import "package:m3e_card_list/m3e_card_list.dart"; -import "package:nexus/controllers/members_by_status.dart"; -import "package:nexus/controllers/members_grouped.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/string_to_color.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/membership_status.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/divider_text.dart"; -import "package:nexus/widgets/error_dialog.dart"; -import "package:nexus/widgets/loading.dart"; -import "package:nexus/widgets/user_bottom_sheet.dart"; - -class MemberList extends HookConsumerWidget { - final String roomId; - const MemberList(this.roomId, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final statusIndex = useState(0); - - final options = { - "Joined": .join, - "Invited": .invite, - "Banned": .ban, - }; - final status = options.values.toIList()[statusIndex.value]; - - return Drawer( - shape: Border(), - child: Scaffold( - backgroundColor: Colors.transparent, - body: Column( - children: [ - Padding( - padding: .symmetric(vertical: 8), - child: M3EToggleButtonGroup( - selectedIndex: statusIndex.value, - onSelectedIndexChanged: (index) => - statusIndex.value = index ?? statusIndex.value, - actions: options - .mapTo( - (name, value) => M3EToggleButtonGroupAction( - checkedLabel: Text( - "$name${switch (ref.watch(MembersByStatusController.provider(.new(roomId: roomId, status: value)))) { - AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.length})", - _ => "", - }}", - ), - label: Text(name), - ), - ) - .toList(), - ), - ), - - switch (ref.watch( - MembersGroupedController.provider( - .new(roomId: roomId, status: status), - ), - )) { - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - AsyncData(:final value) || AsyncLoading(:final value?) => - value.isEmpty - ? Center( - child: Padding( - padding: .symmetric(vertical: 18), - child: Text( - "No ${options.keys.toIList()[statusIndex.value]} Members", - style: Theme.of(context).textTheme.headlineSmall, - ), - ), - ) - : Expanded( - child: CustomScrollView( - slivers: [ - for (final MapEntry(key: powerLevel, value: members) - in value) ...[ - SliverToBoxAdapter( - child: Padding( - padding: .symmetric(horizontal: 16), - child: DividerText( - powerLevel == null - ? "Creators" - : "Power Level $powerLevel", - ), - ), - ), - SliverM3ECardList( - padding: .all(4), - color: Theme.of( - context, - ).colorScheme.surfaceContainerHigh, - margin: .symmetric(horizontal: 12, vertical: 4), - itemCount: members.length, - itemBuilder: (context, index) => - switch (members[index].content) { - MembershipContent( - :final avatarUrl, - :final displayName, - ) => - ListTile( - title: Text( - displayName ?? - members[index] - .stateKey! - .localpart, - overflow: .ellipsis, - style: .new( - color: members[index] - .stateKey! - .colorHash, - fontWeight: .bold, - ), - ), - subtitle: Text( - members[index].stateKey!, - overflow: .ellipsis, - ), - leading: AvatarOrHash( - avatarUrl, - height: 36, - displayName ?? - members[index] - .stateKey! - .localpart, - ), - ), - _ => throw Exception( - "Member content was not MembershipContent", - ), - }, - onTap: (index) { - final member = members[index]; - if (member.content - case MembershipContent content) { - showModalBottomSheet( - constraints: .loose( - .new( - 500, - (context.size?.height ?? 1000) - 80, - ), - ), - isScrollControlled: true, - context: context, - builder: (context) => UserBottomSheet( - content, - member.stateKey!, - roomId: roomId, - ), - ); - } - }, - ), - ], - ], - ), - ), - AsyncLoading _ => Loading(), - }, - ], - ), - appBar: Scaffold.of(context).hasEndDrawer - ? AppBar( - scrolledUnderElevation: 0, - leading: Icon(Icons.people), - title: Text("Members"), - actionsPadding: .only(right: 4), - actions: [ - IconButton( - onPressed: Scaffold.of(context).closeEndDrawer, - icon: Icon(Icons.close), - tooltip: "Close member list", - ), - ], - ) - : null, - ), - ); - } -} diff --git a/lib/widgets/message_image.dart b/lib/widgets/message_image.dart deleted file mode 100644 index 9a4caaf..0000000 --- a/lib/widgets/message_image.dart +++ /dev/null @@ -1,57 +0,0 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:flutter/material.dart"; -import "package:flutter_blurhash/flutter_blurhash.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/cross_cache.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/models/info/image.dart" as i; -import "package:nexus/widgets/expandable_image.dart"; -import "package:nexus/widgets/loading.dart"; - -class MessageImage extends ConsumerWidget { - final Uri url; - final i.ImageInfo? info; - const MessageImage(this.url, {this.info, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => ExpandableImage( - url.toString(), - child: ClipRRect( - borderRadius: .all(.circular(8)), - child: AspectRatio( - aspectRatio: (info?.width ?? 1) / (info?.height ?? 1), - child: Image( - image: CachedNetworkImage( - url.toString(), - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), - width: info?.width, - fit: BoxFit.fitWidth, - loadingBuilder: (_, child, loadingProgress) => loadingProgress == null - ? child - : switch (info?.blurHash) { - final blurHash? => - info?.width == null || info?.height == null - ? SizedBox( - width: 200, - height: 200, - child: BlurHash(hash: blurHash), - ) - : SizedBox( - width: info!.width, - child: BlurHash(hash: blurHash), - ), - _ => Loading(), - }, - errorBuilder: (context, error, stackTrace) => Center( - child: Text( - "Image Failed to Load", - style: .new(color: Theme.of(context).colorScheme.error), - ), - ), - ), - ), - ), - ); -} diff --git a/lib/widgets/pinned_events_drawer.dart b/lib/widgets/pinned_events_drawer.dart deleted file mode 100644 index 7cb9fe3..0000000 --- a/lib/widgets/pinned_events_drawer.dart +++ /dev/null @@ -1,93 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/pinned_events.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/error_dialog.dart"; -import "package:nexus/widgets/loading.dart"; -import "package:nexus/widgets/renderers/event.dart"; - -class PinnedEventsDrawer extends HookConsumerWidget { - final String roomId; - final IList Function(Event event) getEventOptions; - final Future Function(String eventId) jumpToId; - const PinnedEventsDrawer( - this.roomId, { - required this.getEventOptions, - required this.jumpToId, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pinsProvider = ref.watch(PinnedEventsController.provider(roomId)); - final theme = Theme.of(context); - - return Drawer( - width: 400, - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - scrolledUnderElevation: 0, - leading: Icon(Icons.push_pin), - title: Text("Pinned Events"), - actionsPadding: .only(right: 4), - actions: [ - IconButton( - onPressed: Scaffold.of(context).closeEndDrawer, - icon: Icon(Icons.close), - tooltip: "Close pinned events", - ), - ], - ), - body: switch (pinsProvider) { - AsyncData(:final value) when value.isEmpty => Center( - child: Column( - mainAxisSize: .min, - children: [ - Icon( - Icons.push_pin_outlined, - size: 48, - color: theme.colorScheme.onSurface, - ), - SizedBox(height: 12), - Text("No pinned events", style: theme.textTheme.headlineSmall), - ], - ), - ), - AsyncData(:final value) || - AsyncLoading(:final value?) => ListView.builder( - padding: .all(8), - reverse: true, - itemCount: value.length, - itemBuilder: (context, index) { - final event = value.reversed[index]; - - return InkWell( - borderRadius: .circular(12), - onTap: () { - Navigator.of(context).pop(); - jumpToId(event.eventId); - }, - child: Padding( - padding: .symmetric(vertical: 4), - child: EventRenderer( - event, - maxLines: 2, - isGrouped: false, - getEventOptions: getEventOptions, - ), - ), - ); - }, - ), - AsyncLoading() => Loading(), - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - }, - ), - ); - } -} diff --git a/lib/widgets/players/audio.dart b/lib/widgets/players/audio.dart deleted file mode 100644 index 0c96579..0000000 --- a/lib/widgets/players/audio.dart +++ /dev/null @@ -1,102 +0,0 @@ -import "dart:async"; - -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:media_kit/media_kit.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/models/info/audio.dart"; - -class AudioPlayer extends HookConsumerWidget { - final Uri url; - final AudioInfo? info; - - const AudioPlayer(this.url, this.info, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final player = useMemoized( - () => Player(configuration: .new(bufferSize: 128 * 1024 * 1024)), - ); - - final playing = useState(false); - final position = useState(Duration.zero); - final duration = useState(Duration.zero); - - useEffect(() { - scheduleMicrotask(() async { - await player.open( - Media(url.toString(), httpHeaders: ref.headers), - play: false, - ); - - player.stream.playing.listen((value) { - playing.value = value; - }); - - player.stream.position.listen((value) { - position.value = value; - }); - - player.stream.duration.listen((value) { - duration.value = value; - }); - }); - - return player.dispose; - }, []); - - String format(Duration duration) { - final minutes = duration.inMinutes - .remainder(60) - .toString() - .padLeft(2, "0"); - final seconds = duration.inSeconds - .remainder(60) - .toString() - .padLeft(2, "0"); - - return "$minutes:$seconds"; - } - - return SizedBox( - height: 60, - child: Card( - color: Theme.of(context).colorScheme.surfaceContainer, - child: Padding( - padding: .only(left: 8, right: 16), - child: Row( - children: [ - IconButton( - onPressed: player.playOrPause, - icon: Icon( - playing.value ? Icons.pause_circle : Icons.play_circle, - ), - ), - SizedBox(width: 8), - Text( - format(position.value), - style: Theme.of(context).textTheme.bodySmall, - ), - Expanded( - child: Slider( - min: 0, - max: duration.value.inMilliseconds <= 0 - ? 1 - : duration.value.inMilliseconds.toDouble(), - value: position.value.inMilliseconds.toDouble(), - onChanged: (value) => - player.seek(.new(milliseconds: value.toInt())), - ), - ), - Text( - format(duration.value), - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/widgets/players/video.dart b/lib/widgets/players/video.dart deleted file mode 100644 index 8083860..0000000 --- a/lib/widgets/players/video.dart +++ /dev/null @@ -1,36 +0,0 @@ -import "dart:async"; - -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/models/info/video.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:media_kit/media_kit.dart"; -import "package:media_kit_video/media_kit_video.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; - -class VideoPlayer extends HookConsumerWidget { - final VideoInfo? info; - final Uri url; - const VideoPlayer(this.url, this.info, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final player = useMemoized( - () => Player(configuration: .new(bufferSize: 128 * 1024 * 1024)), - ); - final controller = useMemoized(() => VideoController(player)); - - useEffect(() { - scheduleMicrotask( - () => player.open( - Media(url.toString(), httpHeaders: ref.headers), - play: false, - ), - ); - - return player.dispose; - }, []); - - return SizedBox(height: 300, child: Video(controller: controller)); - } -} diff --git a/lib/widgets/reaction_row.dart b/lib/widgets/reaction_row.dart deleted file mode 100644 index 2035b59..0000000 --- a/lib/widgets/reaction_row.dart +++ /dev/null @@ -1,118 +0,0 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/cross_cache.dart"; -import "package:nexus/controllers/reactions.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/error_dialog.dart"; -import "package:nexus/main.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; - -class ReactionRow extends ConsumerWidget { - final Event event; - const ReactionRow(this.event, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final clientState = ref.watch(ClientStateController.provider); - - return Padding( - padding: .only(top: 4), - child: switch (ref.watch( - ReactionsController.provider( - .new(roomId: event.roomId, eventRowId: event.rowId), - ), - )) { - AsyncData(value: final IMap>? reactors) || - AsyncLoading(value: final reactors) => Wrap( - spacing: 4, - runSpacing: 4, - children: event.reactions - .where((_, value) => value != 0) - .mapTo( - (reaction, count) => HookBuilder( - builder: (context) { - final enabled = useState(true); - - final selected = - reactors?[reaction]?.contains(clientState!.userId) ?? - false; - return Tooltip( - message: reactors?[reaction]?.join(", ") ?? "", - child: ChoiceChip( - showCheckmark: false, - selected: selected, - label: Row( - mainAxisSize: .min, - spacing: 8, - children: [ - Flexible( - child: reaction.startsWith("mxc://") - ? Image( - height: 20, - image: CachedNetworkImage( - headers: ref.headers, - Uri.parse(reaction) - .mxcToHttps( - clientState!.homeserverUrl!, - ) - .toString(), - ref.watch( - CrossCacheController.provider, - ), - ), - ) - : Text(reaction, overflow: .ellipsis), - ), - Text(count.toString(), overflow: .ellipsis), - ], - ), - onSelected: enabled.value - ? (value) async { - enabled.value = false; - try { - final controller = ref.watch( - RoomChatController.provider( - event.roomId, - ).notifier, - ); - - if (selected) { - await controller - .removeReaction( - reaction, - event, - clientState!.userId!, - ) - .onError(showError); - } else { - await controller - .sendReaction(reaction, event) - .onError(showError); - } - } finally { - enabled.value = true; - } - } - : null, - ), - ); - }, - ), - ) - .toList(), - ), - - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - }, - ); - } -} diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart deleted file mode 100644 index bcca961..0000000 --- a/lib/widgets/renderers/event.dart +++ /dev/null @@ -1,232 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/helpers/extensions/show_context_menu.dart"; -import "package:nexus/models/content/avatar.dart"; -import "package:nexus/models/content/canonical_alias.dart"; -import "package:nexus/models/content/content.dart"; -import "package:nexus/models/content/create.dart"; -import "package:nexus/models/content/encrypted.dart"; -import "package:nexus/models/content/history_visibility.dart"; -import "package:nexus/models/content/join_rules.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/content/pinned_events.dart"; -import "package:nexus/models/content/power_levels.dart"; -import "package:nexus/models/content/server_acl.dart"; -import "package:nexus/models/content/sticker.dart"; -import "package:nexus/models/content/topic.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/error_dialog.dart"; -import "package:nexus/widgets/lazy_loading/message_displayname.dart"; -import "package:nexus/widgets/renderers/message.dart"; -import "package:nexus/widgets/reaction_row.dart"; -import "package:nexus/widgets/renderers/membership.dart"; -import "package:nexus/widgets/renderers/generic_event.dart"; - -class EventRenderer extends HookConsumerWidget { - final Event event; - final bool textOnly; - final bool isGrouped; - final int? maxLines; - final VoidCallback? onTapReply; - final IList Function(Event event)? getEventOptions; - const EventRenderer( - this.event, { - this.onTapReply, - this.textOnly = false, - this.isGrouped = false, - this.maxLines, - this.getEventOptions, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final errorStyle = TextStyle(color: colorScheme.error); - final focusNode = useFocusNode(); - useListenable(focusNode); - - final child = event.redactedBy != null || event.relationType == "m.replace" - ? null - : switch (event.content) { - Content(:final parseError?) => Row( - children: [ - ErrorDialog( - "An error occurred while parsing event ${event.eventId}:\n$parseError", - parseError.stackTrace, - ), - ], - ), - - MessageContent() || - EncryptedContent() || - StickerContent() => MessageRenderer( - event, - onTapReply: onTapReply, - isGrouped: isGrouped, - maxLines: maxLines, - textOnly: textOnly, - ), - - MembershipContent content => switch (event.previousContent) { - MembershipContent(:final status) => - status == content.status ? null : MembershipRenderer(event), - _ => MembershipRenderer(event), - }, - - AvatarContent() => GenericEventRenderer(Icons.interests, [ - MessageDisplayname(event), - Text("changed the room avatar"), - ]), - - CreateContent() => GenericEventRenderer(Icons.add, [ - MessageDisplayname(event), - Text("created the room"), - ]), - - PowerLevelsContent() => GenericEventRenderer(Icons.power, [ - MessageDisplayname(event), - Text("changed the room's power levels"), - ]), - - JoinRulesContent() => GenericEventRenderer(Icons.rule, [ - MessageDisplayname(event), - Text("changed the room's join rules"), - ]), - - TopicContent() => GenericEventRenderer(Icons.description, [ - MessageDisplayname(event), - Text("updated the room topic"), - ]), - - HistoryVisibilityContent(:final historyVisibility) => - GenericEventRenderer(Icons.history, [ - MessageDisplayname(event), - Text( - "changed the room's history visibility to ${switch (historyVisibility) { - .invited => "since invited", - .joined => "since joined", - .shared => "all history visible (shared)", - .worldReadable => "all history visible (world readable)", - }}", - ), - ]), - - PinnedEventsContent() => GenericEventRenderer(Icons.push_pin, [ - MessageDisplayname(event), - Text("pinned/unpinned some events"), - ]), - - ServerACLContent() => GenericEventRenderer(Icons.list, [ - MessageDisplayname(event), - Text("updated the server ban list"), - ]), - - CanonicalAliasContent(:final alias, :final altAliases) => - GenericEventRenderer(Icons.numbers, [ - MessageDisplayname(event), - Text(switch ([ - if (event.previousContent case CanonicalAliasContent( - alias: final prevAlias, - altAliases: final prevAltAliases, - )) ...[ - if (prevAlias != alias) - if (alias == null) - "removed the room's canonical alias" - else - "changed the room's canonical alias to $alias", - - if (prevAltAliases - .remove(alias ?? "") - .remove(prevAlias ?? "") != - altAliases.remove(alias ?? "").remove(prevAlias ?? "")) - "changed the room's aliases", - ] else ...[ - if (alias != null) "set the room's canonical alias", - if (altAliases.isNotEmpty) "set the room's aliases", - ], - ]) { - [] => "did something related to room aliases", - List prev => prev.join(" and "), - }), - ]), - _ => null, - }; - - final contextMenuCallback = getEventOptions == null - ? null - : (details) => context.showContextMenu( - globalPosition: details.globalPosition, - children: getEventOptions!(event).toList(), - ); - - return Column( - crossAxisAlignment: .start, - children: [ - if (child != null) ...[ - if (textOnly) - child - else ...[ - Builder( - builder: (context) => FocusableActionDetector( - focusNode: focusNode, - actions: contextMenuCallback == null - ? null - : { - ActivateIntent: CallbackAction( - onInvoke: (_) { - final renderBox = - context.findRenderObject() as RenderBox; - final topLeft = renderBox.localToGlobal( - Offset.zero, - ); - context.showContextMenu( - globalPosition: topLeft, - children: getEventOptions!(event).toList(), - ); - return null; - }, - ), - }, - child: Container( - decoration: BoxDecoration( - color: focusNode.hasPrimaryFocus - ? theme.colorScheme.surfaceContainerHighest - : null, - ), - child: GestureDetector( - onSecondaryTapUp: contextMenuCallback, - onLongPressStart: contextMenuCallback, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 8, - ).copyWith(top: isGrouped ? 0 : 8), - child: child, - ), - ), - ), - ), - ), - - ...[ - if (event.content is! MessageContent) ReactionRow(event), - - if (event.sendError != null && event.sendError != "not sent") - Text( - event.sendError!, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.error, - ), - ), - ].map((child) => Padding(padding: .only(left: 4), child: child)), - ], - ] else if (textOnly) - Text("Unknown event type", style: errorStyle), - ], - ); - } -} diff --git a/lib/widgets/renderers/generic_event.dart b/lib/widgets/renderers/generic_event.dart deleted file mode 100644 index 6dfae53..0000000 --- a/lib/widgets/renderers/generic_event.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:flutter/material.dart"; - -class GenericEventRenderer extends StatelessWidget { - final IconData icon; - final List children; - const GenericEventRenderer(this.icon, this.children, {super.key}); - - @override - Widget build(BuildContext context) => Padding( - padding: .symmetric(vertical: 4), - child: Row( - spacing: 8, - children: [ - Padding(padding: .symmetric(horizontal: 4), child: Icon(icon)), - Expanded(child: Wrap(spacing: 4, children: children)), - ], - ), - ); -} diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart deleted file mode 100644 index b2835b4..0000000 --- a/lib/widgets/renderers/membership.dart +++ /dev/null @@ -1,53 +0,0 @@ -import "package:flutter/material.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/helpers/extensions/string_to_color.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/lazy_loading/message_displayname.dart"; -import "package:nexus/widgets/renderers/generic_event.dart"; - -class MembershipRenderer extends StatelessWidget { - final Event event; - const MembershipRenderer(this.event, {super.key}); - - @override - Widget build(BuildContext context) { - assert( - event.content is MembershipContent, - "Make sure to only pass membership events to MembershipRenderer", - ); - - return switch (event.content) { - MembershipContent content => GenericEventRenderer(Icons.people, [ - InkWell( - onTap: () => context.showUserPopover( - content, - event.stateKey!, - roomId: event.roomId, - ), - child: Text( - overflow: .ellipsis, - content.displayName ?? event.stateKey!.localpart, - maxLines: 1, - style: .new(color: event.sender.colorHash, fontWeight: .bold), - ), - ), - Text( - overflow: .ellipsis, - maxLines: 1, - "${switch (content.status) { - .invite => "was invited to", - .join => "joined", - .leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), - .ban => "was banned from", - .knock => "asked to join", - }} the room${event.sender == event.stateKey ? "" : " by "}", - ), - if (event.sender != event.stateKey) MessageDisplayname(event), - if (content.reason != null) Text("for \"${content.reason}\""), - ]), - _ => SizedBox.shrink(), - }; - } -} diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart deleted file mode 100644 index b731cd0..0000000 --- a/lib/widgets/renderers/message.dart +++ /dev/null @@ -1,307 +0,0 @@ -import "package:collection/collection.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:linkify/linkify.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/event.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/content/encrypted.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/content/sticker.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/file_card.dart"; -import "package:nexus/widgets/html/html.dart"; -import "package:nexus/widgets/lazy_loading/message_avatar.dart"; -import "package:nexus/widgets/lazy_loading/message_displayname.dart"; -import "package:nexus/widgets/linkified_text.dart"; -import "package:nexus/widgets/message_image.dart"; -import "package:nexus/widgets/reaction_row.dart"; -import "package:nexus/widgets/url_preview.dart"; -import "package:timeago/timeago.dart"; -import "package:nexus/widgets/event_preview.dart"; -import "package:nexus/widgets/players/video.dart"; -import "package:nexus/widgets/players/audio.dart"; - -class MessageRenderer extends ConsumerWidget { - final Event event; - final bool textOnly; - final bool isGrouped; - final int? maxLines; - final VoidCallback? onTapReply; - const MessageRenderer( - this.event, { - this.onTapReply, - this.textOnly = false, - this.isGrouped = false, - this.maxLines, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final errorStyle = TextStyle(color: colorScheme.error); - - final timestamp = Tooltip( - message: event.timestamp.toString(), - child: Text( - format(event.timestamp), - maxLines: 1, - overflow: .ellipsis, - style: theme.textTheme.labelSmall?.copyWith(color: Colors.grey), - ), - ); - - final textStyle = TextStyle( - fontSize: event.localContent?.bigEmoji == true ? 32 : null, - fontStyle: event.content is EmoteMessageContent ? .italic : null, - ); - - return Row( - crossAxisAlignment: .start, - mainAxisSize: .min, - spacing: 8, - children: [ - if (!textOnly) - if (isGrouped) - SizedBox(width: 40) - else - MessageAvatar(event, height: 40), - Flexible( - child: Column( - spacing: 4, - crossAxisAlignment: .start, - children: [ - if (!isGrouped && !textOnly) - Row( - spacing: 4, - children: [ - Flexible(child: MessageDisplayname(event)), - Flexible(flex: 0, child: timestamp), - ], - ), - Card( - margin: textOnly ? .zero : .only(bottom: 4), - color: textOnly - ? Colors.transparent - : ref.watch( - ClientStateController.provider.select( - (value) => value?.userId, - ), - ) == - event.sender - ? (event.eventId.startsWith("~") - ? colorScheme.onPrimary - : colorScheme.primaryContainer) - : colorScheme.surfaceContainer, - elevation: textOnly ? 0 : null, - - child: Padding( - padding: textOnly ? .zero : .all(12), - child: Column( - crossAxisAlignment: .start, - children: [ - if (!textOnly && event.replyTo != null) - Card( - margin: .only(bottom: 8), - color: theme.colorScheme.surfaceContainerHigh, - child: InkWell( - onTap: onTapReply, - child: Padding( - padding: .symmetric(vertical: 8, horizontal: 12), - child: switch (ref.watch( - EventController.provider( - .new( - roomId: event.roomId, - eventId: event.replyTo!, - ), - ), - )) { - AsyncData(:final value?) || - AsyncLoading( - :final value?, - ) => EventPreview(value), - AsyncError _ => Text( - "An error occurred while fetching the reply", - style: errorStyle, - ), - _ => Text("Fetching event..."), - }, - ), - ), - ), - switch (event.content) { - EncryptedContent() => Text( - "Unable to decrypt event", - style: errorStyle, - ), - StickerContent(:final body, :final url, :final info) => - textOnly - ? Text( - body, - maxLines: maxLines, - overflow: .ellipsis, - ) - : ConstrainedBox( - constraints: .loose(.square(200)), - child: MessageImage( - url.mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value!.homeserverUrl!, - ), - ), - ), - info: info, - ), - ), - // TODO: Handle locations - // LocationMessageContent(:final body , :final geoUri) => - TextMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - NoticeMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - EmoteMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - ImageMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - VideoMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - AudioMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - FileMessageContent( - :final body, - :final formattedBody, - :final format, - ) => Column( - crossAxisAlignment: .start, - children: [ - format == .html && !textOnly - ? Html( - roomId: event.roomId, - textStyle: textStyle, - formattedBody!.replaceAllMapped( - RegExp( - r"(]*>.*?<\/a>)|(\bhttps?:\/\/[^\s<]+)", - caseSensitive: false, - dotAll: true, - ), - (m) { - // If it's already an tag, leave it unchanged - if (m.group(1) != null) { - return m.group(1)!; - } - - // Otherwise, wrap the bare URL - final url = m.group(2)!; - return "$url"; - }, - ), - ) - : LinkifiedText( - body, - style: textStyle, - maxLines: maxLines, - ), - - if (!textOnly) ...[ - if (event.content - case ImageMessageContent(:final url) || - FileMessageContent(:final url) || - VideoMessageContent(:final url) || - AudioMessageContent(:final url)) - switch (url?.mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value!.homeserverUrl!, - ), - ), - )) { - final url? => ConstrainedBox( - constraints: .loose(.square(500)), - child: switch (event.content) { - VideoMessageContent(:final info) => - VideoPlayer(url, info), - AudioMessageContent(:final info) => - AudioPlayer(url, info), - FileMessageContent( - :final info, - :final filename, - ) => - FileCard(url, info, filename: filename), - ImageMessageContent(:final info) => - MessageImage(url, info: info), - _ => SizedBox.shrink(), - }, - ), - _ => Text( - "Nexus currently cannot handle encrypted media", - style: errorStyle, - ), - }, - - if (event.lastEditRowId != 0) - Text( - "(edited)", - style: theme.textTheme.labelSmall, - ), - - if (linkify(body).firstWhereOrNull( - (element) => element is UrlElement, - ) - case final UrlElement link?) - UrlPreview(link.url), - - ReactionRow(event), - ], - ], - ), - MessageContent(:final body) => - body == null - ? Text( - "This message is redacted", - style: errorStyle, - ) - : Row( - spacing: 8, - mainAxisSize: .min, - children: [ - Text( - "Unknown message type:", - style: errorStyle, - ), - Text(body), - ], - ), - _ => throw Exception("This is impossible"), - }, - ], - ), - ), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/widgets/room_appbar.dart b/lib/widgets/room_appbar.dart deleted file mode 100644 index 4598881..0000000 --- a/lib/widgets/room_appbar.dart +++ /dev/null @@ -1,146 +0,0 @@ -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/widgets/appbar.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/expandable_image.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/widgets/linkified_text.dart"; -import "package:nexus/widgets/room_menu.dart"; - -class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget { - final bool isDesktop; - final void Function(BuildContext context)? onOpenMemberList; - final void Function()? onOpenPinnedMessagesList; - final void Function() onOpenDrawer; - final String? roomId; - const RoomAppbar({ - required this.roomId, - required this.isDesktop, - required this.onOpenDrawer, - this.onOpenMemberList, - this.onOpenPinnedMessagesList, - super.key, - }); - - @override - Size get preferredSize => AppBar().preferredSize; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final room = roomId == null - ? null - : ref.watch(RoomsController.provider.select((value) => value[roomId!])); - - return Appbar( - onTap: room == null - ? null - : () => showDialog( - context: context, - builder: (context) => Dialog( - constraints: .loose(.fromWidth(400)), - child: Padding( - padding: .all(24), - child: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - spacing: 8, - children: [ - Row( - spacing: 12, - mainAxisSize: .min, - children: [ - if (room.metadata?.avatar != null) - ExpandableImage( - room.metadata!.avatar! - .mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value!.homeserverUrl!, - ), - ), - ) - .toString(), - child: AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Room", - height: 64, - fallback: Icon(Icons.numbers), - ), - ), - Expanded( - child: Text( - room.metadata?.name ?? "Unnamed Room", - overflow: .ellipsis, - maxLines: 3, - style: Theme.of(context).textTheme.headlineSmall, - ), - ), - ], - ), - if (room.metadata?.topic?.isNotEmpty == true) - LinkifiedText( - room.metadata!.topic!, - style: Theme.of(context).textTheme.bodyLarge - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ), - leading: isDesktop - ? room == null - ? null - : AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Room", - height: 24, - fallback: Icon(Icons.numbers), - ) - : DrawerButton(onPressed: onOpenDrawer), - scrolledUnderElevation: 0, - title: room == null - ? null - : Column( - crossAxisAlignment: .start, - children: [ - Text( - room.metadata?.name ?? "Unnamed Room", - overflow: .ellipsis, - maxLines: 1, - ), - if (room.metadata?.topic?.isNotEmpty == true) - Text( - room.metadata!.topic!, - maxLines: 1, - overflow: .ellipsis, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - actions: room == null - ? .new() - : .new([ - IconButton( - onPressed: onOpenPinnedMessagesList?.call, - icon: Icon(Icons.push_pin), - tooltip: "Open pinned messages", - ), - IconButton( - onPressed: () => onOpenMemberList?.call(context), - tooltip: "Open member list", - icon: Icon(Icons.people), - ), - RoomMenu(room), - ]), - ); - } -} diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart deleted file mode 100644 index 0fdd7c7..0000000 --- a/lib/widgets/room_chat.dart +++ /dev/null @@ -1,564 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter/services.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:measure_size/measure_size.dart"; -import "package:nexus/controllers/account_data.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/pinned_ids.dart"; -import "package:nexus/controllers/power_level.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/controllers/via.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/composer/composer.dart"; -import "package:nexus/widgets/emoji_picker_button.dart"; -import "package:nexus/widgets/pinned_events_drawer.dart"; -import "package:nexus/widgets/renderers/event.dart"; -import "package:nexus/widgets/member_list.dart"; -import "package:nexus/widgets/room_appbar.dart"; -import "package:nexus/widgets/highlight_wrapper.dart"; -import "package:nexus/widgets/error_dialog.dart"; -import "package:nexus/main.dart"; -import "package:nexus/widgets/loading.dart"; -import "package:super_sliver_list/super_sliver_list.dart"; - -class RoomChat extends HookConsumerWidget { - final bool isDesktop; - final bool showMembersByDefault; - final String? roomId; - const RoomChat({ - required this.roomId, - required this.isDesktop, - required this.showMembersByDefault, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final relatedEvent = useState(null); - final relationType = useState(RelationType.reply); - final highlightedEvent = useState(null); - - final composerSize = useState(64); - - final memberListOpened = useState(showMembersByDefault); - - final userId = ref.watch(ClientStateController.provider)?.userId; - final theme = Theme.of(context); - - final nothing = Center( - child: Text( - "Nothing to see here...", - style: theme.textTheme.headlineMedium, - ), - ); - if (userId == null || this.roomId == null) { - return Scaffold( - appBar: RoomAppbar( - roomId: this.roomId, - isDesktop: isDesktop, - onOpenDrawer: () => Scaffold.of(context).openDrawer(), - ), - body: nothing, - ); - } - - final roomId = this.roomId!; - - final controllerProvider = RoomChatController.provider(roomId); - final notifier = ref.watch(controllerProvider.notifier); - - final client = ref.watch(ClientController.provider.notifier); - - final listController = useRef(ListController()); - final scrollController = useScrollController(); - final controllerData = ref.watch(controllerProvider); - - final topEventBeforeLoad = useState(null); - - Future jumpToId(String eventId) async { - final index = controllerData.value?.indexWhere( - (element) => element.eventId == eventId, - ); - if (index == null) return; - - listController.value.animateToItem( - index: index, - scrollController: scrollController, - alignment: 0.5, - duration: (_) => .new(milliseconds: 700), - curve: (_) => Curves.easeInOut, - ); - highlightedEvent.value = eventId; - await Future.delayed(.new(seconds: 1), () { - if (highlightedEvent.value == eventId) { - highlightedEvent.value = null; - } - }); - } - - Future loadOlder() async { - if (controllerData case AsyncData(:final value?)) { - topEventBeforeLoad.value = value.firstOrNull?.eventId; - await notifier.loadOlder(); - } - } - - useEffect(() { - ref - .read(controllerProvider.future) - .then( - (_) => WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo( - scrollController.position.maxScrollExtent - .000001, - ); - } - }), - ); - - return null; - }, [scrollController.hasClients]); - - useEffect(() { - if (controllerData case AsyncData( - :final value?, - ) when scrollController.hasClients) { - if (topEventBeforeLoad.value != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - final index = value.indexWhere( - (event) => event.eventId == topEventBeforeLoad.value, - ); - if (index != -1) { - listController.value.jumpToItem( - index: index, - scrollController: scrollController, - alignment: 0, - ); - } - } - topEventBeforeLoad.value = null; - }); - } else if (scrollController.position.atEdge && - scrollController.position.pixels != 0) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo( - scrollController.position.maxScrollExtent, - ); - } - }); - } - } - - return null; - }, [controllerData]); - - useEffect(() { - Future listener() async { - if (!scrollController.hasClients || !scrollController.position.atEdge) { - return; - } - - final room = ref.watch( - RoomsController.provider.select((value) => value[roomId]), - ); - if (room == null) return; - - if (scrollController.position.pixels == 0) { - if (room.hasMore) { - await loadOlder(); - } - } else { - await client.markRead(room); - } - } - - scrollController.addListener(listener); - return () => scrollController.removeListener(listener); - }, [roomId, controllerData]); - - final composerNode = useFocusNode( - onKeyEvent: (_, event) { - if (event is KeyDownEvent && event.logicalKey == .escape) { - relatedEvent.value = null; - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - }, - ); - - IList getEventOptions(Event event) { - final danger = theme.colorScheme.error; - final isSentByMe = event.sender == userId; - - return [ - if (ref.watch( - PowerLevelController.provider( - .new(eventType: .reaction, roomId: roomId), - ), - )) - PopupMenuItem( - enabled: false, - child: IconTheme( - data: theme.iconTheme, - child: Row( - children: [ - ...{ - ...ref.watch( - AccountDataController.provider.select( - (value) => IList( - value["m.recent_emoji"] - ?.content["recent_emoji"] ?? - [], - ).map((entry) => entry["emoji"]).toIList(), - ), - ), - "👍", - "🤣", - "😭", - "🤔", - } - .toIList() - .sublist(0, 4) - .map( - (emoji) => IconButton( - onPressed: () async { - Navigator.of(context).pop(); - await notifier - .sendReaction(emoji, event) - .onError(showError); - }, - icon: Text(emoji), - ), - ), - EmojiPickerButton( - context: context, - onPressed: Navigator.of(context).pop, - onSelection: (emoji) => - notifier.sendReaction(emoji, event).onError(showError), - ), - ], - ), - ), - ), - if (ref.watch( - PowerLevelController.provider( - .new(eventType: .message, roomId: roomId), - ), - )) - PopupMenuItem( - onTap: () { - relatedEvent.value = event; - relationType.value = .reply; - composerNode.requestFocus(); - }, - child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")), - ), - if (event.content is MessageContent && isSentByMe) - PopupMenuItem( - onTap: () { - relatedEvent.value = event; - relationType.value = .edit; - composerNode.requestFocus(); - }, - child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")), - ), - if (ref.watch( - PowerLevelController.provider( - .state(eventType: .pinnedEvents, roomId: roomId), - ), - )) - switch (ref - .watch(PinnedIdsController.provider(roomId)) - .contains(event.eventId)) { - bool isPinned => PopupMenuItem( - onTap: () async { - try { - final notifier = ref.read( - PinnedIdsController.provider(roomId).notifier, - ); - if (isPinned) { - await notifier.removePin(event.eventId); - } else { - await notifier.addPin(event.eventId); - } - } catch (error, stackTrace) { - showError(error, stackTrace); - } - }, - child: ListTile( - leading: Icon(Icons.push_pin), - title: Text(isPinned == true ? "Unpin Event" : "Pin Event"), - ), - ), - }, - PopupMenuItem( - onTap: () async { - final room = ref.watch( - RoomsController.provider.select((value) => value[roomId]), - ); - if (room == null) return; - - final vias = ref.watch(ViaController.provider(room)); - - await Clipboard.setData( - ClipboardData( - text: - "matrix:roomid/${room.metadata?.id.substring(1)}/e/${event.eventId}$vias)", - ), - ); - }, - child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), - ), - if (ref.watch( - PowerLevelController.provider( - .redaction(targetUser: event.sender, roomId: roomId), - ), - )) - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (_) { - final deleteReasonController = useTextEditingController(); - return AlertDialog( - title: Text("Delete Message"), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text( - "Are you sure you want to delete this message? This can not be reversed.", - ), - SizedBox(height: 12), - TextField( - controller: deleteReasonController, - textCapitalization: .sentences, - decoration: .new( - labelText: "Reason for deletion (optional)", - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - await notifier - .deleteMessage( - event, - reason: deleteReasonController.text, - ) - .onError(showError); - }, - child: Text("Delete"), - ), - ], - ); - }, - ), - ), - child: ListTile( - leading: Icon(Icons.delete, color: danger), - title: Text("Delete", style: .new(color: danger)), - ), - ), - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (_) { - final reasonController = useTextEditingController(); - return AlertDialog( - title: Text("Report"), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text( - "Report this event to your server administrators, who can take action like banning this server or room.", - ), - - SizedBox(height: 12), - TextField( - controller: reasonController, - textCapitalization: .sentences, - decoration: .new( - labelText: "Reason for report (optional)", - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () { - client.reportEvent( - .new( - roomId: roomId, - eventId: event.eventId, - reason: reasonController.text.isEmpty - ? null - : reasonController.text, - ), - ); - Navigator.of(context).pop(); - }, - child: Text("Report"), - ), - ], - ); - }, - ), - ), - child: ListTile( - leading: Icon(Icons.report, color: danger), - title: Text("Report", style: .new(color: danger)), - ), - ), - ].toIList(); - } - - return Scaffold( - endDrawer: PinnedEventsDrawer( - roomId, - getEventOptions: getEventOptions, - jumpToId: jumpToId, - ), - body: Builder( - builder: (middleContext) => Scaffold( - endDrawer: showMembersByDefault ? null : MemberList(roomId), - appBar: RoomAppbar( - roomId: roomId, - isDesktop: isDesktop, - onOpenDrawer: Scaffold.of(context).openDrawer, - onOpenMemberList: (thisContext) { - memberListOpened.value = !memberListOpened.value; - Scaffold.of(thisContext).openEndDrawer(); - }, - onOpenPinnedMessagesList: () { - Scaffold.of(middleContext).openEndDrawer(); - }, - ), - body: Row( - children: [ - Expanded( - child: Stack( - children: [ - Positioned.fill( - child: Padding( - padding: .symmetric(horizontal: 4), - child: switch (controllerData) { - AsyncData(:final value?) || - AsyncLoading(:final value?) => CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: .symmetric(vertical: 36), - child: Center( - child: ElevatedButton( - onPressed: controllerData is AsyncData - ? loadOlder - : null, - child: Text("Load More"), - ), - ), - ), - ), - - SuperSliverList.builder( - listController: listController.value, - itemCount: value.length, - itemBuilder: (_, index) { - final event = value[index]; - final previousEvent = value.getOrNull( - index - 1, - ); - return HighlightWrapper( - EventRenderer( - event, - onTapReply: () => - jumpToId(event.replyTo!), - getEventOptions: getEventOptions, - isGrouped: - previousEvent?.content - is MessageContent && - previousEvent?.redactedBy == null && - previousEvent?.relationType != - "m.replace" && - "${event.sender}${event.pmp?.id}" == - "${previousEvent?.sender}${previousEvent?.pmp?.id}", - ), - isHighlighted: - highlightedEvent.value == event.eventId, - ); - }, - ), - - SliverPadding( - padding: .only(bottom: composerSize.value), - ), - ], - ), - AsyncData() => nothing, - AsyncLoading() => Loading(), - AsyncError(:final error, :final stackTrace) => - ErrorDialog(error, stackTrace), - }, - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: MeasureSize( - onChange: (size) => composerSize.value = size.height, - child: Composer( - roomId, - node: composerNode, - onSend: - (text, {required shouldMention, required tags}) => - notifier - .send( - text, - tags: tags, - relationType: relationType.value, - shouldMention: shouldMention, - relation: relatedEvent.value, - ) - .onError(showError), - relationType: relationType.value, - relatedEvent: relatedEvent.value, - onDismiss: () => relatedEvent.value = null, - ), - ), - ), - ], - ), - ), - - if (memberListOpened.value == true && showMembersByDefault) - MemberList(roomId), - ], - ), - ), - ), - ); - } -} diff --git a/lib/widgets/room_menu.dart b/lib/widgets/room_menu.dart deleted file mode 100644 index 95936df..0000000 --- a/lib/widgets/room_menu.dart +++ /dev/null @@ -1,136 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter/services.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/via.dart"; -import "package:nexus/models/room.dart"; - -class RoomMenu extends ConsumerWidget { - final Room? room; - final IList children; - const RoomMenu(this.room, {this.children = const IList.empty(), super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final danger = Theme.of(context).colorScheme.error; - final client = ref.watch(ClientController.provider.notifier); - - return PopupMenuButton( - itemBuilder: (_) => [ - PopupMenuItem( - onTap: () async { - if (room != null) await client.markRead(room!); - await Future.wait(children.map((child) => client.markRead(child))); - }, - child: ListTile( - leading: Icon(Icons.check), - title: Text("Mark as Read"), - ), - ), - if (room != null) ...[ - PopupMenuItem( - onTap: () async { - final vias = ref.watch(ViaController.provider(room!)); - - await Clipboard.setData( - .new( - text: - "matrix:roomid/${room!.metadata?.id.substring(1)}$vias)", - ), - ); - }, - child: ListTile( - leading: Icon(Icons.link), - title: Text("Copy Link"), - ), - ), - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text("Leave Room"), - content: Text( - "Are you sure you want to leave \"${room!.metadata?.name ?? "Unnamed Room"}\"?", - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - final snackbar = ScaffoldMessenger.of(context) - .showSnackBar( - .new( - content: Text("Leaving room..."), - duration: Duration(days: 1), - ), - ); - await client.leaveRoom(room!); - snackbar.close(); - }, - child: Text("Leave"), - ), - ], - ), - ), - child: ListTile( - leading: Icon(Icons.logout, color: danger), - title: Text("Leave", style: TextStyle(color: danger)), - ), - ), - ], - - // PopupMenuItem( - // onTap: () => showDialog( - // context: context, - // builder: (context) => HookBuilder( - // builder: (_) { - // final reasonController = useTextEditingController(); - // return AlertDialog( - // title: Text("Report"), - // content: Column( - // mainAxisSize: MainAxisSize.min, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text( - // "Report this room to your server administrators, who can take action like banning this room.", - // ), - - // SizedBox(height: 12), - // FormTextInput( - // required: false, - // capitalize: true, - // controller: reasonController, - // title: "Reason for report (optional)", - // ), - // ], - // ), - // actions: [ - // TextButton( - // onPressed: Navigator.of(context).pop, - // child: Text("Cancel"), - // ), - // TextButton( - // onPressed: () { - // room.client.reportRoom(room.id, reasonController.text); - // Navigator.of(context).pop(); - // }, - // child: Text("Report"), - // ), - // ], - // ); - // }, - // ), - // ), - // child: ListTile( - // leading: Icon(Icons.report, color: danger), - // title: Text("Report", style: TextStyle(color: danger)), - // ), - // ), - ], - ); - } -} diff --git a/lib/widgets/settings/dialog_list_tile.dart b/lib/widgets/settings/dialog_list_tile.dart deleted file mode 100644 index f0a236c..0000000 --- a/lib/widgets/settings/dialog_list_tile.dart +++ /dev/null @@ -1,67 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/widgets/settings/radio_dialog.dart"; - -class DialogListTile extends ConsumerWidget { - final T? initialValue; - final String title; - final Widget? subtitle; - final List options; - final bool required; - final Icon icon; - final void Function(T value)? onChanged; - final String Function(T option) getName; - const DialogListTile({ - super.key, - required this.icon, - required this.title, - required this.initialValue, - required this.options, - required this.onChanged, - required this.getName, - this.subtitle, - this.required = true, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) => FormField( - validator: (value) => - value == null && required == true ? "This field is required." : null, - initialValue: initialValue, - builder: (field) => InputDecorator( - decoration: InputDecoration( - errorText: field.errorText, - contentPadding: EdgeInsets.zero, - enabledBorder: InputBorder.none, - ), - child: ListTile( - enabled: onChanged != null, - onTap: () => showDialog( - context: context, - builder: (context) => RadioDialog( - title: title, - getName: getName, - onChanged: onChanged == null - ? null - : (value) { - field.didChange(value); - onChanged!.call(value); - }, - value: field.value, - options: options, - ), - ), - title: Text(title), - subtitle: subtitle, - leading: icon, - trailing: Chip( - label: Text( - field.value == null ? "None" : getName(field.value as T), - overflow: TextOverflow.ellipsis, - style: onChanged == null ? .new(color: Colors.grey) : null, - ), - ), - ), - ), - ); -} diff --git a/lib/widgets/settings/radio_dialog.dart b/lib/widgets/settings/radio_dialog.dart deleted file mode 100644 index 7695b9d..0000000 --- a/lib/widgets/settings/radio_dialog.dart +++ /dev/null @@ -1,55 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; - -class RadioDialog extends HookWidget { - final T? value; - final String title; - final List options; - final void Function(T value)? onChanged; - final String Function(T option) getName; - const RadioDialog({ - super.key, - required this.title, - required this.value, - required this.options, - required this.onChanged, - required this.getName, - }); - - @override - Widget build(BuildContext context) { - final mutValue = useState(null); - return AlertDialog( - title: Text(title), - content: RadioGroup( - groupValue: mutValue.value ?? value, - onChanged: (value) => mutValue.value = value ?? mutValue.value, - - child: Column( - mainAxisSize: MainAxisSize.min, - children: options - .map( - (option) => RadioListTile( - enabled: onChanged != null, - value: option, - title: Text(getName(option)), - dense: true, - ), - ) - .toList(), - ), - ), - actions: [ - TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")), - if (onChanged != null) - TextButton( - onPressed: () { - if (mutValue.value != null) onChanged!(mutValue.value as T); - Navigator.of(context).pop(); - }, - child: Text("OK"), - ), - ], - ); - } -} diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart deleted file mode 100644 index 1bd6cf5..0000000 --- a/lib/widgets/sidebar.dart +++ /dev/null @@ -1,291 +0,0 @@ -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/spaces.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/pages/settings.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/divider_widget.dart"; -import "package:nexus/widgets/join_dialog.dart"; -import "package:nexus/widgets/room_menu.dart"; - -class Sidebar extends HookConsumerWidget { - final bool isDesktop; - const Sidebar({required this.isDesktop, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedSpaceProvider = KeyController.provider( - KeyController.spaceKey, - ); - final selectedSpaceId = ref.watch(selectedSpaceProvider); - final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier); - - final selectedRoomController = KeyController.provider( - KeyController.roomKey, - ); - final selectedRoomId = ref.watch(selectedRoomController); - final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier); - - final spaces = ref.watch(SpacesController.provider); - final indexOfSelected = spaces.indexWhere( - (space) => space.id == selectedSpaceId, - ); - final selectedIndex = indexOfSelected == -1 ? 0 : indexOfSelected; - - final selectedSpace = - spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ?? - spaces.first; - - final indexOfSelectedRoom = selectedSpace.children - .addAll( - selectedSpace.subSpaces.map((element) => element.children).flattened, - ) - .indexWhere((room) => room.metadata?.id == selectedRoomId); - final selectedRoomIndex = indexOfSelectedRoom == -1 - ? null - : indexOfSelectedRoom; - - List roomsToDestinations(IList rooms) => - rooms - .map( - (room) => NavigationRailM3EDestination( - label: room.metadata?.name ?? "Unnamed Room", - badgeCount: switch (room.metadata?.unreadNotifications) { - 0 || null => room.metadata?.unreadMessages == 0 ? null : 0, - int unread => unread, - }, - icon: AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Room", - fallback: selectedSpaceId == "dms" - ? null - : Icon(Icons.numbers), - ), - ), - ) - .toList(); - - return Drawer( - width: 330, - shape: Border(), - child: Row( - children: [ - Theme( - data: Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemCollapsedHeight: 48, - itemVerticalGap: 0, - ), - ], - ), - child: Container( - color: NavigationRailTokensAdapter(context).containerColor, - padding: EdgeInsets.only(top: 16), - child: NavigationRailM3E( - type: .alwaysCollapse, - labelBehavior: .alwaysHide, - scrollable: true, - onDestinationSelected: (value) { - selectedSpaceIdNotifier.set(spaces[value].id); - selectedRoomIdNotifier.set( - spaces[value].children.firstOrNull?.metadata?.id, - ); - }, - sections: [ - .new( - destinations: spaces - .map( - (space) => NavigationRailM3EDestination( - badgeCount: switch (space.children - .addAll( - space.subSpaces - .map((element) => element.children) - .flattened, - ) - .fold( - 0, - (previousValue, room) => - previousValue + - (room.metadata?.unreadNotifications ?? 0), - )) { - 0 => - space.children - .addAll( - space.subSpaces - .map( - (element) => element.children, - ) - .flattened, - ) - .any( - (room) => - room.metadata?.unreadMessages != - 0, - ) - ? 0 - : null, - int badgeCount => badgeCount, - }, - short: true, - icon: AvatarOrHash( - height: 28, - space.room?.metadata?.avatar, - fallback: space.icon == null - ? null - : Icon(space.icon), - space.title, - ), - label: space.title, - ), - ) - .toList(), - ), - ], - selectedIndex: selectedIndex, - trailingAtBottom: true, - trailing: Padding( - padding: .symmetric(vertical: 16), - child: Column( - spacing: 8, - children: [ - PopupMenuButton( - itemBuilder: (_) => [ - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (_) => JoinDialog(ref), - ), - child: ListTile( - title: Text("Join an existing room (or space)"), - leading: Icon(Icons.numbers), - ), - ), - PopupMenuItem( - onTap: null, - child: ListTile( - title: Text("Create a new room"), - leading: Icon(Icons.add), - ), - ), - ], - icon: Icon(Icons.add), - ), - IconButton( - tooltip: "Explore other rooms", - onPressed: null, - icon: Icon(Icons.explore), - ), - IconButton( - tooltip: "Open settings", - onPressed: () => showDialog( - context: context, - builder: (_) => SettingsPage(), - ), - icon: Icon(Icons.settings), - ), - ], - ), - ), - ), - ), - ), - Expanded( - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - leading: AvatarOrHash( - selectedSpace.room?.metadata?.avatar, - fallback: selectedSpace.icon == null - ? null - : Icon(selectedSpace.icon), - - selectedSpace.title, - ), - title: Text(selectedSpace.title, overflow: .ellipsis), - backgroundColor: Colors.transparent, - actions: [ - RoomMenu( - selectedSpace.room, - children: selectedSpace.children.addAll( - selectedSpace.subSpaces - .map((element) => element.children) - .flattened, - ), - ), - ], - ), - body: Theme( - data: Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemExpandedHeight: 48, - iconLabelGap: 16, - ), - ], - ), - child: NavigationRailM3E( - expandedWidth: double.infinity, - scrollable: true, - background: Colors.transparent, - type: .alwaysExpand, - selectedIndex: selectedRoomIndex ?? 0, - sections: [ - .new( - header: selectedSpace.room == null - ? null - : DividerWidget(Text("Rooms")), - destinations: roomsToDestinations(selectedSpace.children), - ), - for (final subSpace in selectedSpace.subSpaces) - .new( - header: DividerWidget( - Row( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - if (subSpace.room.metadata?.avatar != null) - AvatarOrHash( - subSpace.room.metadata?.avatar, - subSpace.room.metadata?.name ?? - "Unnamed Room", - height: 16, - ), - Flexible( - child: Text( - subSpace.room.metadata?.name ?? - "Unnamed Space", - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - destinations: roomsToDestinations(subSpace.children), - ), - ], - onDestinationSelected: (value) { - final children = selectedSpace.children.addAll( - selectedSpace.subSpaces - .map((element) => element.children) - .flattened, - ); - selectedRoomIdNotifier.set( - children[value].metadata?.id, // - ); - if (!isDesktop) Navigator.of(context).pop(); - }, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/widgets/url_preview.dart b/lib/widgets/url_preview.dart deleted file mode 100644 index 19fe06b..0000000 --- a/lib/widgets/url_preview.dart +++ /dev/null @@ -1,66 +0,0 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/cross_cache.dart"; -import "package:nexus/controllers/url_preview.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/launch_helper.dart"; - -class UrlPreview extends ConsumerWidget { - final String link; - const UrlPreview(this.link, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => ConstrainedBox( - constraints: .loose(.fromWidth(400)), - child: ref - .watch(UrlPreviewController.provider(link)) - .betterWhen( - data: (preview) => preview == null - ? SizedBox.shrink() - : InkWell( - onTap: () => - ref.watch(LaunchHelper.provider).launchUrl(.parse(link)), - child: Card( - margin: .symmetric(vertical: 4), - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - child: Padding( - padding: .all(16), - child: Column( - crossAxisAlignment: .start, - spacing: 4, - children: [ - if (preview.title != null) - Text( - preview.title!, - style: Theme.of(context).textTheme.titleLarge, - ), - if (preview.description != null) ...[ - Text(preview.description!), - SizedBox(height: 4), - ], - if (preview.imageUrl != null) - ClipRRect( - borderRadius: .all(.circular(8)), - child: Image( - errorBuilder: (_, _, _) => SizedBox.shrink(), - width: preview.width, - image: CachedNetworkImage( - preview.imageUrl.toString(), - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), - fit: .fitWidth, - ), - ), - ], - ), - ), - ), - ), - ), - ); -} diff --git a/lib/widgets/user_bottom_sheet.dart b/lib/widgets/user_bottom_sheet.dart deleted file mode 100644 index 55e2502..0000000 --- a/lib/widgets/user_bottom_sheet.dart +++ /dev/null @@ -1,264 +0,0 @@ -import "package:collection/collection.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:intl/intl.dart"; -import "package:m3e_buttons/m3e_buttons.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/power_level.dart"; -import "package:nexus/controllers/profile.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/membership_action.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/main.dart"; -import "package:nexus/widgets/expandable_image.dart"; - -class UserBottomSheet extends ConsumerWidget { - final MembershipContent member; - final String userId; - final String? roomId; - const UserBottomSheet(this.member, this.userId, {this.roomId, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - final client = ref.watch(ClientController.provider.notifier); - - void showMembershipDialog(MembershipAction action) => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (context) { - final actionReasonController = useTextEditingController(); - return AlertDialog( - title: Text("${toBeginningOfSentenceCase(action.name)} $userId"), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text("Are you sure you want to ${action.name} $userId?"), - SizedBox(height: 12), - TextField( - textCapitalization: .sentences, - controller: actionReasonController, - decoration: .new( - labelText: "Reason for ${action.name} (optional)", - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - client - .setMembership( - .new( - userId: userId, - roomId: roomId!, - action: action, - reason: actionReasonController.text, - ), - ) - .onError(showError); - }, - child: Text(toBeginningOfSentenceCase(action.name)), - ), - ], - ); - }, - ), - ); - - return Padding( - padding: .all(42), - child: Column( - spacing: 4, - mainAxisSize: .min, - crossAxisAlignment: .center, - children: [ - Row( - mainAxisAlignment: .end, - children: [ - M3EButton( - onPressed: Navigator.of(context).pop, - child: Icon(Icons.close), - ), - ], - ), - SizedBox(height: 18), - - ExpandableImage( - member.avatarUrl - ?.mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value!.homeserverUrl!, - ), - ), - ) - .toString(), - child: AvatarOrHash( - member.avatarUrl, - member.displayName ?? userId.localpart, - height: 200, - ), - ), - - SizedBox(height: 8), - - SelectableText( - member.displayName ?? userId.localpart, - style: textTheme.headlineLarge, - textAlign: .center, - ), - SelectableText( - userId, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - - ref - .watch(ProfileController.provider(userId)) - .betterWhen( - loading: () => Text(""), - data: (profile) => Column( - children: [ - if (profile.timezone == null && profile.pronouns.isEmpty) - Text(""), - Wrap( - crossAxisAlignment: .center, - alignment: .center, - spacing: 4, - runSpacing: 4, - children: [ - ...profile.pronouns - .where( - // TODO: Check system language (l10n) - (pronoun) => pronoun.language == "en", - ) - .mapIndexed( - (index, pronoun) => [ - if (index != 0) - Icon( - Icons.circle, - size: 4, - color: theme.colorScheme.onSurfaceVariant, - ), - Text( - pronoun.summary, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ) - .flattened, - - if (profile.timezone != null) ...[ - if (profile.pronouns.isNotEmpty) - SizedBox( - height: 16, - child: VerticalDivider( - thickness: 1.5, - width: 4, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - Text( - profile.timezone!, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ], - ), - ], - ), - ), - - SizedBox(height: 8), - if (userId != ref.watch(ClientStateController.provider)?.userId && - roomId != null) ...[ - Row( - children: [ - Expanded( - child: M3EButton.icon( - onPressed: null, - shape: .square, - style: .tonal, - icon: Icon(Icons.message), - label: Text("Message"), - ), - ), - ], - ), - - if (ref.watch( - PowerLevelController.provider( - .membershipAction( - action: .kick, - roomId: roomId!, - targetUser: userId, - ), - ), - ) && - member.status == .join || - member.status == .invite) - Padding( - padding: .only(top: 4), - child: Row( - mainAxisSize: MainAxisSize.max, - spacing: 8, - children: [ - M3EButton.icon( - onPressed: () => showMembershipDialog(.kick), - shape: .square, - icon: Icon(Icons.sports_martial_arts), - label: Text("Kick"), - decoration: .new( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.error, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onError, - ), - ), - ), - - M3EButton.icon( - onPressed: () => showMembershipDialog(.ban), - shape: .square, - icon: Icon(Icons.gavel), - label: Text("Ban"), - decoration: .new( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.errorContainer, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onErrorContainer, - ), - ), - ), - ].map((e) => Expanded(child: e)).toList(), - ), - ), - ], - ], - ), - ); - } -} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 603ea6a..fd8ccf3 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,38 +6,38 @@ #include "generated_plugin_registrant.h" -#include +#include #include -#include -#include -#include #include +#include #include +#include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + g_autoptr(FlPluginRegistrar) dynamic_system_colors_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); - dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + dynamic_color_plugin_register_with_registrar(dynamic_system_colors_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) gtk_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); - gtk_plugin_register_with_registrar(gtk_registrar); - g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); - media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); - g_autoptr(FlPluginRegistrar) media_kit_video_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); - media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); + g_autoptr(FlPluginRegistrar) simple_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SimpleSecureStorageLinuxPlugin"); + simple_secure_storage_linux_plugin_register_with_registrar(simple_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) webcrypto_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WebcryptoPlugin"); + webcrypto_plugin_register_with_registrar(webcrypto_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); + g_autoptr(FlPluginRegistrar) window_size_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin"); + window_size_plugin_register_with_registrar(window_size_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7d8f046..8d79b66 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,17 +3,18 @@ # list(APPEND FLUTTER_PLUGIN_LIST - dynamic_color + dynamic_system_colors file_selector_linux - gtk - media_kit_libs_linux - media_kit_video screen_retriever_linux + simple_secure_storage_linux url_launcher_linux + webcrypto window_manager + window_size ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_vodozemac ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/linux/nexus.federated.nexus.desktop b/linux/nexus.federated.nexus.desktop deleted file mode 100644 index d61734e..0000000 --- a/linux/nexus.federated.nexus.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Name=Nexus -GenericName=Matrix Client -Comment=A simple and user-friendly Matrix client -Exec=nexus %u -Icon=nexus -Terminal=false -Type=Application -Categories=Chat;Network;InstantMessaging; -MimeType=x-scheme-handler/nexus.federated.nexus; \ No newline at end of file diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix deleted file mode 100644 index ae77467..0000000 --- a/linux/nix/devshell.nix +++ /dev/null @@ -1,48 +0,0 @@ -{ pkgs, lib }: -let - android = pkgs.androidenv.composeAndroidPackages { - toolsVersion = "26.1.1"; - platformToolsVersion = "36.0.1"; - buildToolsVersions = [ - "35.0.0" - "36.0.0" - ]; - cmakeVersions = [ "3.22.1" ]; - platformVersions = [ "36" ]; - abiVersions = [ - "armeabi-v7a" - "arm64-v8a" - ]; - includeNDK = true; - ndkVersions = [ "28.2.13676358" ]; - }; -in -pkgs.mkShell { - packages = with pkgs; [ - go - git - jdk17 - libGL - wayland - (flutter.override { - extraPkgConfigPackages = [ - mpv-unwrapped - libass - ]; - }) - android.platform-tools - ]; - - env = rec { - LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.libclang ]; - LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}"; - CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ]; - - ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk"; - ANDROID_SDK_ROOT = ANDROID_HOME; - JAVA_HOME = pkgs.jdk17; - - TOOLS = "${ANDROID_HOME}/build-tools/${"36.0.0"}"; - GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${TOOLS}/aapt2"; - }; -} diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix deleted file mode 100644 index 4e00959..0000000 --- a/linux/nix/pkg/default.nix +++ /dev/null @@ -1,50 +0,0 @@ -{ - lib, - callPackage, - mpv-unwrapped, - libass, - libclang, - flutter, - src, -}: - -flutter.buildFlutterApplication { - pname = "nexus"; - version = "0.1.0"; - inherit src; - - preBuild = '' - cp ${callPackage ./gomuks.nix { inherit src; }}/lib/* . - packageRunCustom nexus generate source/scripts test - packageRun build_runner build - ''; - - buildInputs = [ - mpv-unwrapped - libass - ]; - - env.LIBCLANG_PATH = lib.makeLibraryPath [ libclang ]; - - autoPubspecLock = src + "/pubspec.lock"; - - gitHashes = { - emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo="; - linkify = "sha256-TpMD6+0zyY6i9l+6d8ErnVufmepCv362rCtnbOht/z4="; - navigation_rail_m3e = "sha256-+2awDTQnK58gGRY1nuHckG/jjxarsYSRu9ovR4i4TEc="; - }; - - postInstall = '' - install -D assets/icon.svg $out/share/icons/hicolor/scalable/apps/nexus.svg - install -Dm755 linux/nexus.federated.nexus.desktop -t $out/share/applications - wrapProgram $out/bin/nexus \ - --suffix LD_LIBRARY_PATH : $out/app/nexus/lib - ''; - - meta = { - description = "A simple and user-friendly Matrix client"; - mainProgram = "nexus"; - platforms = lib.platforms.linux; - maintainers = with lib.maintainers; [ quadradical ]; - }; -} diff --git a/linux/nix/pkg/gomuks.nix b/linux/nix/pkg/gomuks.nix deleted file mode 100644 index ad0412e..0000000 --- a/linux/nix/pkg/gomuks.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ - src, - buildGoModule, -}: - -buildGoModule (finalAttrs: { - pname = "gomuks-ffi"; - version = "submodule"; - - doCheck = false; - - src = "${src}/gomuks"; - - vendorHash = "sha256-mNZEHOGByVqK1kSLC/Cf1VEvkDxRen+TmoV5CXvGrZ4="; - - buildPhase = '' - runHook preBuild - - go build -buildmode=c-shared -o libgomuks.so -tags goolm,noheic,sqlite_fts5 ./pkg/ffi - - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - - install -Dm0644 libgomuks.so -t $out/lib - - runHook postInstall - ''; -}) diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 62e5450..58cd859 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -23,13 +23,6 @@ static void first_frame_cb(MyApplication* self, FlView *view) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); - - GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); - if (windows) { - gtk_window_present(GTK_WINDOW(windows->data)); - return; - } - GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); @@ -50,7 +43,6 @@ static void my_application_activate(GApplication* application) { } } #endif - gtk_widget_set_size_request(GTK_WIDGET(window), 250, -1); if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); @@ -100,7 +92,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch g_application_activate(application); *exit_status = 0; - return FALSE; + return TRUE; } // Implements GApplication::startup. @@ -147,6 +139,6 @@ MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, + "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } diff --git a/macos/.gitignore b/macos/.gitignore deleted file mode 100644 index 746adbb..0000000 --- a/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index f18d831..0000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import app_links -import dynamic_color -import file_selector_macos -import media_kit_libs_macos_video -import media_kit_video -import package_info_plus -import screen_retriever_macos -import shared_preferences_foundation -import url_launcher_macos -import wakelock_plus -import window_manager - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) - DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) - FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) - MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) - FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) - WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) -} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index f87523c..0000000 --- a/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* nexus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "nexus.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* nexus.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* nexus.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nexus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nexus"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nexus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nexus"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nexus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nexus"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c1761..0000000 --- a/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f..0000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eb..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb5722..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e7..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632c..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a..0000000 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index b4c5133..0000000 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = nexus - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2026 nexus.federated. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9..0000000 --- a/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49..0000000 --- a/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4..0000000 --- a/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 4e593b0..0000000 --- a/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,16 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.client - - com.apple.security.network.server - - com.apple.security.files.user-selected.read-only - - - diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist deleted file mode 100644 index 36999ec..0000000 --- a/macos/Runner/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - CFBundleURLTypes - - - CFBundleURLName - - auth_callback - CFBundleURLSchemes - - - nexus.federated.nexus - - - - - diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb..0000000 --- a/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1..0000000 --- a/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/nix/android.nix b/nix/android.nix new file mode 100644 index 0000000..f373968 --- /dev/null +++ b/nix/android.nix @@ -0,0 +1,20 @@ +{ + androidenv, +}: +androidenv.composeAndroidPackages { + toolsVersion = "26.1.1"; + platformToolsVersion = "36.0.1"; + buildToolsVersions = [ + "35.0.0" + "36.0.0" + ]; + cmakeVersions = [ "3.22.1" ]; + platformVersions = [ "36" ]; + abiVersions = [ + "armeabi-v7a" + "arm64-v8a" + ]; + includeNDK = true; + ndkVersions = [ "27.0.12077973" ]; + +} diff --git a/nix/fake-rustup.sh b/nix/fake-rustup.sh new file mode 100644 index 0000000..7884c05 --- /dev/null +++ b/nix/fake-rustup.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Fake rustup for nix-managed Rust toolchains + +case "$1" in + run) + if [[ "$2" == "stable" ]]; then + shift 2 + if [[ $# -eq 0 ]]; then + echo "fake rustup: no command given" >&2 + exit 1 + fi + exec "$@" + exit 0 + fi + ;; + + toolchain) + if [[ "$2" == "list" ]]; then + echo "stable (default)" + exit 0 + fi + ;; + + target) + if [[ "$2" == "list" && "$3" == "--toolchain" && "$4" == "stable" && "$5" == "--installed" ]]; then + echo "x86_64-unknown-linux-gnu" + exit 0 + fi + ;; +esac + +echo "fake rustup: the command:" >&2 +echo " rustup $*" >&2 +echo "…is not mocked yet" >&2 +exit 1 \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 3a0aeb5..871bb6f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,82 +5,50 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d url: "https://pub.dev" source: hosted - version: "99.0.0" + version: "91.0.0" analysis_server_plugin: dependency: transitive description: name: analysis_server_plugin - sha256: "3960b28ee740004df39f85d5ebfc91785f7a90e51fd7c9a185e86a36b2f581b4" + sha256: "44adba4d74a2541173bad4c11531d2a4d22810c29c5ddb458a38e9f4d0e5eac7" url: "https://pub.dev" source: hosted - version: "0.3.14" + version: "0.3.4" analyzer: - dependency: transitive + dependency: "direct overridden" description: name: analyzer - sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "8.4.1" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: "445b77e2054fa3e8c8a8ef1b5e9e6b23bb8028fffd34b5e60eaef315b7750674" + sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.1.11" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "0057a98d64d7bb872b0c87dff6e73d2c2d80c77156e7a03f127a26f8aa240649" + sha256: "6645a029da947ffd823d98118f385d4bd26b54eb069c006b22e0b94e451814b5" url: "https://pub.dev" source: hosted - version: "0.14.8" - app_links: - dependency: "direct main" - description: - name: app_links - sha256: f8db46d2ea9ff6f3a37191a7fd5b7813da1253ace8c32c1b9eadace41d1188ea - url: "https://pub.dev" - source: hosted - version: "7.2.1" - app_links_linux: - dependency: transitive - description: - name: app_links_linux - sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - sha256: "7546f09a6e93f4a2df2fe2bd40a5c6c64310ac461b036d82b43033be7a59f809" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - app_links_web: - dependency: transitive - description: - name: app_links_web - sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" - source: hosted - version: "1.0.4" + version: "0.13.11" archive: dependency: transitive description: name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.9" + version: "4.0.7" args: dependency: transitive description: @@ -93,10 +61,26 @@ packages: dependency: transitive description: name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.13.0" + base58check: + dependency: transitive + description: + name: base58check + sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + blurhash_dart: + dependency: transitive + description: + name: blurhash_dart + sha256: "43955b6c2e30a7d440028d1af0fa185852f3534b795cc6eb81fbf397b464409f" + url: "https://pub.dev" + source: hosted + version: "1.2.1" boolean_selector: dependency: transitive description: @@ -109,34 +93,42 @@ packages: dependency: transitive description: name: build - sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" + sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.3" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" build_config: dependency: transitive description: name: build_config - sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.2.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.1.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" + sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" url: "https://pub.dev" source: hosted - version: "2.15.1" + version: "2.10.4" built_collection: dependency: transitive description: @@ -149,31 +141,23 @@ packages: dependency: transitive description: name: built_value - sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" url: "https://pub.dev" source: hosted - version: "8.12.6" - button_m3e: + version: "8.12.1" + canonical_json: dependency: transitive description: - name: button_m3e - sha256: "6754ddeb9068ad2005bd26d5ceabc41268029465095686d7d228296c2e706909" + name: canonical_json + sha256: d6be1dd66b420c6ac9f42e3693e09edf4ff6edfee26cb4c28c1c019fdb8c0c15 url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "1.1.2" characters: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted version: "1.4.0" @@ -185,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" cli_config: dependency: transitive description: @@ -201,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" + clipboard: + dependency: "direct main" + description: + name: clipboard + sha256: "619f4e9e946cfd637ac994f49af356bb590ab88b0c4aded03204ee566fd69d9e" + url: "https://pub.dev" + source: hosted + version: "3.0.8" clock: dependency: transitive description: @@ -209,14 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - code_assets: - dependency: "direct main" + code_builder: + dependency: transitive description: - name: code_assets - sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "4.11.0" collection: dependency: "direct main" description: @@ -245,12 +245,12 @@ packages: dependency: transitive description: name: coverage - sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "1.15.1" + version: "1.15.0" cross_cache: - dependency: "direct main" + dependency: transitive description: name: cross_cache sha256: "4983a16603cc99b0a14de6a772fa8ee4533411f46f3c423f1386fea7566049c5" @@ -261,10 +261,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" url: "https://pub.dev" source: hosted - version: "0.3.5+4" + version: "0.3.5+1" crypto: dependency: transitive description: @@ -281,79 +281,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "751ee9440920f808266c3ec2553420dea56d3c7837dd2d62af76b11be3fcece5" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+8.4.0" dart_style: dependency: transitive description: name: dart_style - sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98 + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b url: "https://pub.dev" source: hosted - version: "3.1.8" + version: "3.1.3" dbus: dependency: transitive description: name: dbus - sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.14" + version: "0.7.11" + diffutil_dart: + dependency: transitive + description: + name: diffutil_dart + sha256: "5e74883aedf87f3b703cb85e815bdc1ed9208b33501556e4a8a5572af9845c81" + url: "https://pub.dev" + source: hosted + version: "4.0.1" dio: dependency: transitive description: name: dio - sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0 + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.9.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4 + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" url: "https://pub.dev" source: hosted - version: "2.2.0" - dynamic_color: + version: "2.1.1" + dynamic_system_colors: dependency: "direct main" description: - name: dynamic_color - sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" + name: dynamic_system_colors + sha256: "43794e658fa88cbdec9f397dd1afd2eb69b6c9717e99b93b16ba37c3aa3b3a8c" url: "https://pub.dev" source: hosted - version: "1.8.1" - dynamic_polls: - dependency: "direct main" - description: - name: dynamic_polls - sha256: "72ff19cdf041ad8dcfa76adaebb216d005f40b278d955e6e0c7bcb769215fabe" - url: "https://pub.dev" - source: hosted - version: "0.0.7" - emoji_text_field: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "5f7baaf8a6f059ec3ab8ff0f5d02339b00bf6997" - url: "https://github.com/Henry-Hiles/emoji_text_field" - source: git - version: "1.0.0" - equatable: - dependency: transitive - description: - name: equatable - sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - fab_m3e: - dependency: transitive - description: - name: fab_m3e - sha256: e4f5abfa3c8c092005449d56dcac45b85e2dbe9c32789d672c5ed71428e43b59 - url: "https://pub.dev" - source: hosted - version: "0.1.1" + version: "1.8.0" fake_async: dependency: transitive description: @@ -366,34 +365,18 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "58cec99fc068427c71901e82d4b31b232240ebe6e61200993c2cb91bcada0ff6" + sha256: "19f70498af299cbce5ff919dbbecd5abfd9d0c28139004f68d3810ce23dedfb3" url: "https://pub.dev" source: hosted - version: "11.2.0" + version: "11.1.0" ffi: - dependency: "direct main" - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - ffi_leak_tracker: dependency: transitive description: - name: ffi_leak_tracker - sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "0.1.2" - ffigen: - dependency: "direct main" - description: - name: ffigen - sha256: b7803707faeec4ce3c1b0c2274906504b796e3b70ad573577e72333bd1c9b3ba - url: "https://pub.dev" - source: hosted - version: "20.1.1" + version: "2.1.4" file: dependency: transitive description: @@ -402,30 +385,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - file_selector: + file_picker: dependency: "direct main" description: - name: file_selector - sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a + name: file_picker + sha256: d974b6ba2606371ac71dd94254beefb6fa81185bde0b59bdc1df09885da85fde url: "https://pub.dev" source: hosted - version: "1.1.0" - file_selector_android: - dependency: transitive - description: - name: file_selector_android - sha256: "6a26687fa65cbc28a5345c7ae6f227e89f0b47740978a4c475b1a625da7a331b" - url: "https://pub.dev" - source: hosted - version: "0.5.2+8" - file_selector_ios: - dependency: transitive - description: - name: file_selector_ios - sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca - url: "https://pub.dev" - source: hosted - version: "0.5.3+5" + version: "10.3.8" file_selector_linux: dependency: transitive description: @@ -450,14 +417,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" - file_selector_web: - dependency: transitive - description: - name: file_selector_web - sha256: "73181fbc5257776d8ecaa6a94ab3c8e920ad143b9132a6d984a9271dfc6928d3" - url: "https://pub.dev" - source: hosted - version: "0.9.5" file_selector_windows: dependency: transitive description: @@ -479,14 +438,23 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_blurhash: + flutter_chat_core: dependency: "direct main" description: - name: flutter_blurhash - sha256: e97b9aff13b9930bbaa74d0d899fec76e3f320aba3190322dcc5d32104e3d25d + name: flutter_chat_core + sha256: "8c46790f64f106bf6e610e2a7324b3844320e9e295867c06d45d9deb134d848d" url: "https://pub.dev" source: hosted - version: "0.9.1" + version: "2.9.0" + flutter_chat_ui: + dependency: "direct main" + description: + path: "packages/flutter_chat_ui" + ref: HEAD + resolved-ref: "6cfbadbf364251dd3c6a986e20c9d97636ad3412" + url: "https://github.com/Henry-Hiles/flutter_chat_ui" + source: git + version: "2.9.1" flutter_hooks: dependency: "direct main" description: @@ -503,14 +471,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.14.4" - flutter_linkify: + flutter_link_previewer: dependency: "direct main" description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" + path: "packages/flutter_link_previewer" + ref: HEAD + resolved-ref: "6cfbadbf364251dd3c6a986e20c9d97636ad3412" + url: "https://github.com/Henry-Hiles/flutter_chat_ui" + source: git + version: "4.1.2" flutter_lints: dependency: "direct dev" description: @@ -524,35 +493,59 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_math_fork: + dependency: transitive + description: + name: flutter_math_fork + sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" + url: "https://pub.dev" + source: hosted + version: "0.7.4" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 url: "https://pub.dev" source: hosted - version: "2.0.35" + version: "2.0.33" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e" + sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.1.0" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.3" flutter_test: dependency: transitive description: flutter source: sdk version: "0.0.0" + flutter_vodozemac: + dependency: "direct main" + description: + name: flutter_vodozemac + sha256: "16d4b44dd338689441fe42a80d0184e5c864e9563823de9e7e6371620d2c0590" + url: "https://pub.dev" + source: hosted + version: "0.4.1" flutter_web_plugins: dependency: transitive description: flutter @@ -562,26 +555,59 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html_core - sha256: "7ff010b116f6abc16429923e616fbc727f3f65ef4cee12ffdb280aeecbc21e7f" + sha256: "1120ee6ed3509ceff2d55aa6c6cbc7b6b1291434422de2411b5a59364dd6ff03" url: "https://pub.dev" source: hosted - version: "0.17.2" + version: "0.17.0" fluttertagger: dependency: "direct main" description: name: fluttertagger - sha256: "04514674b41a063b97901aedf6970d0675b828bd723a0fb9f9dba89b91953382" + sha256: "3df0132bdd431a7279da78ea70500ea1e767fa093f43f32785b757c10c6a0fcc" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.1" + flyer_chat_file_message: + dependency: "direct main" + description: + name: flyer_chat_file_message + sha256: "96c5c25908cd671dda1963ade03e188e6a14bba6b116e73fac329f1abefc9ad1" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flyer_chat_image_message: + dependency: "direct main" + description: + name: flyer_chat_image_message + sha256: "04730c9373c9c7315ba0e1a360c67ac5f6c7ec8a700ffe2d2dc00e29b7f8ff90" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flyer_chat_system_message: + dependency: "direct main" + description: + name: flyer_chat_system_message + sha256: d254f85be55949f8eb1a4a9a9b1c5b54ffed0c9a39dfa7e4fa6a6358bdb5d45a + url: "https://pub.dev" + source: hosted + version: "2.2.0" + flyer_chat_text_message: + dependency: "direct main" + description: + path: "packages/flyer_chat_text_message" + ref: HEAD + resolved-ref: "6cfbadbf364251dd3c6a986e20c9d97636ad3412" + url: "https://github.com/Henry-Hiles/flutter_chat_ui" + source: git + version: "2.5.2" freezed: dependency: "direct dev" description: name: freezed - sha256: "8599ba37236328ff6f97269ecce875d251a367eb2929c9bbf9b811b2ce56c74e" + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" url: "https://pub.dev" source: hosted - version: "3.2.6-dev.1" + version: "3.2.3" freezed_annotation: dependency: "direct main" description: @@ -598,14 +624,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - get_x_storage: - dependency: transitive - description: - name: get_x_storage - sha256: "69e4412dd70e25a4991623c10bf72e3b12106f2cb4353a2d167353947597f3aa" - url: "https://pub.dev" - source: hosted - version: "0.0.9" glob: dependency: transitive description: @@ -614,6 +632,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + gpt_markdown: + dependency: transitive + description: + name: gpt_markdown + sha256: "9b88dfaffea644070b648c204ca4a55745a49f4ad0b58ed0ab70913ad593c7a1" + url: "https://pub.dev" + source: hosted + version: "1.1.5" graphs: dependency: transitive description: @@ -622,30 +648,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - gtk: - dependency: transitive - description: - name: gtk - sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - hooks: - dependency: "direct main" - description: - name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" - url: "https://pub.dev" - source: hosted - version: "2.0.2" hooks_riverpod: dependency: "direct main" description: name: hooks_riverpod - sha256: dcaf59f0f41489ff08ce61438ada564d67f40cfa37cc7c8589da78fb600c2edc + sha256: b880efcd17757af0aa242e5dceac2fb781a014c22a32435a5daa8f17e9d5d8a9 url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.1.0" html: dependency: transitive description: @@ -654,8 +664,16 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.6" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: - dependency: "direct main" + dependency: transitive description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" @@ -678,46 +696,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" - icon_button_m3e: - dependency: transitive - description: - name: icon_button_m3e - sha256: c4524d6141a468679821bbb635b833ac6831925d8a6ae4a4511430b0e4ab9c67 - url: "https://pub.dev" - source: hosted - version: "0.2.1" idb_shim: dependency: transitive description: name: idb_shim - sha256: "3448298f244bc76a14aca71461eeab6add2b8a877930521c42c2e8b71b644590" + sha256: "071f3b05032fa62e60ca15db9939f8afbaf403b37e67747ac88f858c3e999228" url: "https://pub.dev" source: hosted - version: "2.9.6+2" + version: "2.6.7+1" image: dependency: transitive description: name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.7.2" image_picker: dependency: "direct main" description: name: image_picker - sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.2.1" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" + sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677" url: "https://pub.dev" source: hosted - version: "0.8.13+19" + version: "0.8.13+10" image_picker_for_web: dependency: transitive description: @@ -730,10 +740,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" url: "https://pub.dev" source: hosted - version: "0.8.13+6" + version: "0.8.13+3" image_picker_linux: dependency: transitive description: @@ -782,22 +792,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" json_annotation: dependency: "direct main" description: name: json_annotation - sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.12.0" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: ffcd10cde35a93b2abbbcc26bd9971f4ca93763e8abe78d855e3c4177797e501 + sha256: "6b253f7851cf1626a05c8b49c792e04a14897349798c03798137f2b5f7e0b5b1" url: "https://pub.dev" source: hosted - version: "6.14.0" + version: "6.11.3" + just_throttle_it: + dependency: transitive + description: + name: just_throttle_it + sha256: af2d0c1e5c7f4e0bef79a55edf3d74c180908253f89203467bc432730f5fac5b + url: "https://pub.dev" + source: hosted + version: "3.0.1" leak_tracker: dependency: transitive description: @@ -822,23 +848,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - linkify: - dependency: "direct main" - description: - path: "." - ref: "fix/consecutive-periods-loose-url" - resolved-ref: f2f42b06f84dbbd03fb7c3741f7fbacbe5459a4b - url: "https://github.com/appelladev/linkify" - source: git - version: "5.0.0" lints: dependency: transitive description: name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.0.0" logging: dependency: transitive description: @@ -847,126 +864,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - m3e_buttons: - dependency: "direct main" - description: - name: m3e_buttons - sha256: af1b19bd5ec7327d20b5a57c6c91e1ed30ee15a4dbade781c0cdbc72e6b04b5e - url: "https://pub.dev" - source: hosted - version: "0.0.4" - m3e_card_list: - dependency: "direct main" - description: - name: m3e_card_list - sha256: d4aba0123cccda40ac80789befa8d355e1dc16aa7dcee910157690b0546d78d6 - url: "https://pub.dev" - source: hosted - version: "0.1.0" - m3e_design: + markdown: dependency: transitive description: - name: m3e_design - sha256: "15ff0ef4c43553d855c5e866a9aee8231d44919fe2bb354b1259337bdfd659b4" + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "7.3.0" matcher: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" - measure_size: + version: "0.11.1" + matrix: dependency: "direct main" description: - name: measure_size - sha256: "4b2de7b29567501434902a2f4080cf12a8bc7038b2eb97dfae91b71791620b68" + name: matrix + sha256: fb116ee89f6871441f22f76a988db15cfcfb6dfac97e3e2d654c240080015707 url: "https://pub.dev" source: hosted - version: "5.0.2" - media_kit: + version: "4.1.0" + mention_tag_text_field: dependency: "direct main" description: - name: media_kit - sha256: ae9e79597500c7ad6083a3c7b7b7544ddabfceacce7ae5c9709b0ec16a5d6643 + name: mention_tag_text_field + sha256: ba7b9d8003e0f340a65c6dcdb7770f4340f653ae1612a9e31e11d12f7f1dd80f url: "https://pub.dev" source: hosted - version: "1.2.6" - media_kit_libs_android_video: - dependency: transitive - description: - name: media_kit_libs_android_video - sha256: "3f6274e5ab2de512c286a25c327288601ee445ed8ac319e0ef0b66148bd8f76c" - url: "https://pub.dev" - source: hosted - version: "1.3.8" - media_kit_libs_ios_video: - dependency: transitive - description: - name: media_kit_libs_ios_video - sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 - url: "https://pub.dev" - source: hosted - version: "1.1.4" - media_kit_libs_linux: - dependency: transitive - description: - name: media_kit_libs_linux - sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - media_kit_libs_macos_video: - dependency: transitive - description: - name: media_kit_libs_macos_video - sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d - url: "https://pub.dev" - source: hosted - version: "1.1.4" - media_kit_libs_video: - dependency: "direct main" - description: - name: media_kit_libs_video - sha256: "2b235b5dac79c6020e01eef5022c6cc85fedc0df1738aadc6ea489daa12a92a9" - url: "https://pub.dev" - source: hosted - version: "1.0.7" - media_kit_libs_windows_video: - dependency: transitive - description: - name: media_kit_libs_windows_video - sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab - url: "https://pub.dev" - source: hosted - version: "1.0.11" - media_kit_video: - dependency: "direct main" - description: - name: media_kit_video - sha256: afaa509e7b7e0bf247557a3a740cde903a52c34ace9810f94500e127bd7b043d - url: "https://pub.dev" - source: hosted - version: "2.0.1" + version: "0.0.9" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.16.0" mime: dependency: transitive description: @@ -975,23 +920,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - motor: + nested: dependency: transitive description: - name: motor - sha256: cbd49f21b00e568c2b1a55f134ed803614a107782f4fea7769693bca32940c58 + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" url: "https://pub.dev" source: hosted - version: "1.1.0" - navigation_rail_m3e: - dependency: "direct main" - description: - path: "packages/navigation_rail_m3e" - ref: HEAD - resolved-ref: "667b0bc8526fd53296778903b6ef3f22424f3aa4" - url: "https://github.com/Henry-Hiles/material_3_expressive" - source: git - version: "0.3.5" + version: "1.0.0" node_preamble: dependency: transitive description: @@ -1000,14 +936,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" - url: "https://pub.dev" - source: hosted - version: "9.4.1" package_config: dependency: transitive description: @@ -1016,22 +944,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" - url: "https://pub.dev" - source: hosted - version: "10.2.1" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 - url: "https://pub.dev" - source: hosted - version: "4.1.0" path: dependency: "direct main" description: @@ -1052,42 +964,42 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.5" path_provider_android: - dependency: "direct overridden" + dependency: transitive description: name: path_provider_android - sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e url: "https://pub.dev" source: hosted - version: "2.2.23" + version: "2.2.22" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.5.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -1100,10 +1012,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "7.0.1" platform: dependency: transitive description: @@ -1132,10 +1044,18 @@ packages: dependency: transitive description: name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.0.3" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1152,46 +1072,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - quiver: + punycode: dependency: transitive description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + name: punycode + sha256: "39b874cc1f78b94e57db17e74b3f2ba2a96e25c0bebdcc8a571614dccda0ff0c" url: "https://pub.dev" source: hosted - version: "3.2.2" - record_use: + version: "1.0.0" + random_string: dependency: transitive description: - name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + name: random_string + sha256: "03b52435aae8cbdd1056cf91bfc5bf845e9706724dd35ae2e99fa14a1ef79d02" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "2.3.1" riverpod: dependency: transitive description: name: riverpod - sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76" + sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.1.0" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "3e275138862ccc22ed61444a1f9a840f753094c367f28f4123f50289cd204d68" + sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0" url: "https://pub.dev" source: hosted - version: "1.0.0-dev.10" + version: "1.0.0-dev.8" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "166f29492228dc471b6fe294092560ccd5545ed15c9fc155622455b58b2451a9" + sha256: "4d2eb0d19bbe7e3323bd0ce4553b2e6170d161a13914bfdd85a3612329edcb43" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.0" rxdart: dependency: transitive description: @@ -1200,78 +1120,94 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" - safe_local_storage: - dependency: transitive - description: - name: safe_local_storage - sha256: "494b982d5edb71030650ea463d939670e91b232b588323dc75229d2c5f23e7b7" - url: "https://pub.dev" - source: hosted - version: "2.0.6" screen_retriever: dependency: transitive description: name: screen_retriever - sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6 + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.0" screen_retriever_linux: dependency: transitive description: name: screen_retriever_linux - sha256: "7b52006a5ceae1f3d5af7f77188c3290d6e7d8ded16d99809bea84967c65c257" + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.0" screen_retriever_macos: dependency: transitive description: name: screen_retriever_macos - sha256: a1489b99cce597c45a54b9aae1cd94c8d4705353b7e0bb2457a6e4de44e0ad8a + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.0" screen_retriever_platform_interface: dependency: transitive description: name: screen_retriever_platform_interface - sha256: "94a5535277510a63184ca178ce12a1449bc0b38618879aa1c18bf57369c5064a" + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.0" screen_retriever_windows: dependency: transitive description: name: screen_retriever_windows - sha256: dafc6922b0bfbf1d48cf3ccbf519b4fff47bdcb820da1728ea6db675fecc9324 + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.0" + scrollview_observer: + dependency: transitive + description: + name: scrollview_observer + sha256: c2f713509f18f88f637b2084b47a90c91fb1ef066d5d82d2cf3194d8509dc6ab + url: "https://pub.dev" + source: hosted + version: "1.26.2" + sdp_transform: + dependency: transitive + description: + name: sdp_transform + sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45" + url: "https://pub.dev" + source: hosted + version: "0.3.2" sembast: dependency: transitive description: name: sembast - sha256: a58b26925e23071cf0f4754d8449aabe829e9a9930a872fb18e6ae4c2c88e025 + sha256: c8063c3146c3c8d5f5b04230de7682c768440a575fbda2634f14d22f263197c3 url: "https://pub.dev" source: hosted - version: "3.8.9+1" + version: "3.8.5+2" + sembast_web: + dependency: transitive + description: + name: sembast_web + sha256: "0362c7c241ad6546d3e27b4cfffaae505e5a9661e238dbcdd176756cc960fe7a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" url: "https://pub.dev" source: hosted - version: "2.5.5" + version: "2.5.4" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" url: "https://pub.dev" source: hosted - version: "2.4.27" + version: "2.4.18" shared_preferences_foundation: dependency: transitive description: @@ -1292,10 +1228,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: @@ -1344,27 +1280,91 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + simple_secure_storage: + dependency: "direct main" + description: + name: simple_secure_storage + sha256: ca823a355bb7bb0e9b969876508e7d3a5dc0d1fb2dcb681c85b6e315f1e876e9 + url: "https://pub.dev" + source: hosted + version: "0.3.7" + simple_secure_storage_android: + dependency: transitive + description: + name: simple_secure_storage_android + sha256: "50fb27267755843af039da116d0e545f313ae329ef8838101880802259e0f741" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + simple_secure_storage_darwin: + dependency: transitive + description: + name: simple_secure_storage_darwin + sha256: "8bd2ffcc62b478957ce20046bb96618b91a11e74af5d9fe2b4b229117bad18a7" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + simple_secure_storage_linux: + dependency: transitive + description: + name: simple_secure_storage_linux + sha256: a7b7dccfaf496c27f882c26634ac083f2f545c0a4ca0818534c6261205a83686 + url: "https://pub.dev" + source: hosted + version: "0.2.5" + simple_secure_storage_platform_interface: + dependency: transitive + description: + name: simple_secure_storage_platform_interface + sha256: "04fd4ce4c2b97c01a12eba46f51e3075a793d11f13340d06a64eb9b45a463ca5" + url: "https://pub.dev" + source: hosted + version: "0.2.3" + simple_secure_storage_web: + dependency: transitive + description: + name: simple_secure_storage_web + sha256: "63a3474a9931ab2587e01d22e7e95c0b7cc31338c0fafed5db9d1d798d1d3e0e" + url: "https://pub.dev" + source: hosted + version: "0.2.3" + simple_secure_storage_windows: + dependency: transitive + description: + name: simple_secure_storage_windows + sha256: cf31d2a97c26cf854aeb3c9774cd253f6600fb3fdfc6d807d480afae678cef10 + url: "https://pub.dev" + source: hosted + version: "0.3.2" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" - source_gen: + slugify: dependency: transitive description: - name: source_gen - sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + name: slugify + sha256: b272501565cb28050cac2d96b7bf28a2d24c8dae359280361d124f3093d337c3 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "2.0.0" + source_gen: + dependency: "direct overridden" + description: + name: source_gen + sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" + url: "https://pub.dev" + source: hosted + version: "4.1.1" source_helper: dependency: transitive description: name: source_helper - sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae" + sha256: e82b1996c63da42aa3e6a34cc1ec17427728a1baf72ed017717a5669a7123f0d url: "https://pub.dev" source: hosted - version: "1.3.12" + version: "1.3.9" source_map_stack_trace: dependency: transitive description: @@ -1385,10 +1385,34 @@ packages: dependency: transitive description: name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.2" + version: "1.10.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: "9faa2fedc5385ef238ce772589f7718c24cdddd27419b609bb9c6f703ea27988" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" + url: "https://pub.dev" + source: hosted + version: "2.9.4" stack_trace: dependency: transitive description: @@ -1429,22 +1453,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - super_sliver_list: - dependency: "direct main" - description: - name: super_sliver_list - sha256: b1e1e64d08ce40e459b9bb5d9f8e361617c26b8c9f3bb967760b0f436b6e3f56 - url: "https://pub.dev" - source: hosted - version: "0.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.4.0" term_glyph: dependency: transitive description: @@ -1457,34 +1473,42 @@ packages: dependency: transitive description: name: test - sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.31.0" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.17" - timeago: - dependency: "direct main" + version: "0.6.11" + thumbhash: + dependency: transitive description: - name: timeago - sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e + name: thumbhash + sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37" url: "https://pub.dev" source: hosted - version: "3.7.1" + version: "0.1.0+1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -1493,38 +1517,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - universal_html: + unorm_dart: dependency: transitive description: - name: universal_html - sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 + name: unorm_dart + sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" url: "https://pub.dev" source: hosted - version: "2.3.0" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.dev" - source: hosted - version: "2.3.1" - universal_platform: - dependency: transitive - description: - name: universal_platform - sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - uri_parser: - dependency: transitive - description: - name: uri_parser - sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce" - url: "https://pub.dev" - source: hosted - version: "3.0.2" + version: "0.2.0" url_launcher: dependency: "direct main" description: @@ -1537,18 +1537,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" url: "https://pub.dev" source: hosted - version: "6.3.32" + version: "6.3.28" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.3.6" url_launcher_linux: dependency: transitive description: @@ -1577,10 +1577,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1593,18 +1593,18 @@ packages: dependency: transitive description: name: uuid - sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.5.2" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -1617,10 +1617,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.1.19" vector_math: dependency: transitive description: @@ -1633,34 +1633,26 @@ packages: dependency: transitive description: name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.2.0" - wakelock_plus: - dependency: transitive + version: "15.0.2" + vodozemac: + dependency: "direct main" description: - name: wakelock_plus - sha256: "824c5bba0f800e86d32e57d3d1843c531f090005cc89d9a837933e6601093d53" + name: vodozemac + sha256: "39144e20740807731871c9248d811ed5a037b21d0aa9ffcfa630954de74139d9" url: "https://pub.dev" source: hosted - version: "1.6.1" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b - url: "https://pub.dev" - source: hosted - version: "1.5.1" + version: "0.4.0" watcher: dependency: transitive description: name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.0" web: dependency: transitive description: @@ -1685,6 +1677,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webcrypto: + dependency: transitive + description: + name: webcrypto + sha256: "6b43001c4110856ff7fa5e5e65e7b2d44bec1d8b54a4d84d5fa2c7622267c5c1" + url: "https://pub.dev" + source: hosted + version: "0.6.0" webkit_inspection_protocol: dependency: transitive description: @@ -1693,24 +1693,41 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: "2e604a31703ad26781782fb14fa8a4ee621154ee2c513d2b9938e486fa695233" + url: "https://pub.dev" + source: hosted + version: "1.3.0" win32: dependency: transitive description: name: win32 - sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "5.15.0" window_manager: dependency: "direct main" description: name: window_manager - sha256: "05c231fd7b23d2380f14c5cc10b7b93d60d4fa4a2fb4e0f032de27e44b5560e9" + sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" url: "https://pub.dev" source: hosted - version: "0.5.2" - xdg_directories: + version: "0.5.1" + window_size: dependency: "direct main" + description: + path: "plugins/window_size" + ref: HEAD + resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 + url: "https://github.com/google/flutter-desktop-embedding" + source: git + version: "0.1.0" + xdg_directories: + dependency: transitive description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" @@ -1737,10 +1754,10 @@ packages: dependency: transitive description: name: yaml_edit - sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488" + sha256: ec709065bb2c911b336853b67f3732dd13e0336bd065cc2f1061d7610ddf45e3 url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.3" sdks: - dart: ">=3.12.2 <4.0.0" - flutter: ">=3.44.0" + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2669fea..9551407 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,99 +1,86 @@ name: nexus description: "Yet another Matrix client" -version: 0.1.0 +version: 1.0.0 publish_to: none flutter: - fonts: - - family: fallback-emoji - fonts: - - asset: assets/fonts/NotoColorEmoji.ttf - - family: fallback-sans - fonts: - - asset: assets/fonts/Roboto.ttf assets: - assets/ uses-material-design: true environment: - sdk: "^3.12.2" + sdk: "^3.9.2" dependency_overrides: - path_provider_android: 2.2.23 # Pinned to avoid JNI - linkify: - git: - url: https://github.com/appelladev/linkify - ref: fix/consecutive-periods-loose-url + analyzer: ^8.4.0 + source_gen: ^4.0.2 dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter - flutter_riverpod: 3.3.2 - hooks_riverpod: 3.3.2 - intl: 0.20.2 - fast_immutable_collections: 11.2.0 - path_provider: 2.1.6 - url_launcher: 6.3.2 - freezed_annotation: 3.1.0 - image_picker: ^1.2.3 - path: 1.9.1 - dynamic_color: 1.8.1 - collection: 1.19.1 - window_manager: 0.5.2 - color_hash: 1.0.1 - flutter_widget_from_html_core: 0.17.2 - flutter_svg: 2.3.0 - json_annotation: 4.12.0 - shared_preferences: 2.5.5 - fluttertagger: 2.3.2 - dynamic_polls: 0.0.7 - flutter_hooks: 0.21.3+1 - cross_cache: 1.1.0 - ffi: 2.2.0 - hooks: 2.0.2 - code_assets: 1.2.1 - ffigen: 20.1.1 - timeago: 3.7.1 - http: 1.6.0 - flutter_linkify: 6.0.0 - linkify: 5.0.0 - emoji_text_field: + flutter_hooks: ^0.21.2 + flutter_riverpod: ^3.0.3 + hooks_riverpod: ^3.0.3 + intl: ^0.20.1 + fast_immutable_collections: ^11.0.0 + path_provider: ^2.1.3 + url_launcher: ^6.2.6 + freezed_annotation: ^3.1.0 + image_picker: ^1.1.2 + file_picker: ^10.3.3 + path: ^1.9.0 + dynamic_system_colors: ^1.8.0 + collection: ^1.19.1 + window_manager: ^0.5.1 + window_size: git: - url: https://github.com/Henry-Hiles/emoji_text_field - flutter_blurhash: 0.9.1 - super_sliver_list: 0.4.1 - media_kit: 1.2.6 - media_kit_video: 2.0.1 - media_kit_libs_video: 1.0.7 - measure_size: 5.0.2 - m3e_buttons: 0.0.4 - navigation_rail_m3e: + url: https://github.com/google/flutter-desktop-embedding + path: plugins/window_size + flutter_chat_core: ^2.0.0 + flyer_chat_image_message: ^2.2.2 + flyer_chat_system_message: ^2.1.13 + flyer_chat_file_message: ^2.3.1 + flyer_chat_text_message: git: - url: https://github.com/Henry-Hiles/material_3_expressive - path: packages/navigation_rail_m3e - m3e_card_list: 0.1.0 - xdg_directories: 1.1.0 - package_info_plus: 10.2.1 - app_links: 7.2.1 - file_selector: ^1.1.0 + url: https://github.com/Henry-Hiles/flutter_chat_ui + path: packages/flyer_chat_text_message + flutter_chat_ui: + git: + url: https://github.com/Henry-Hiles/flutter_chat_ui + path: packages/flutter_chat_ui + flutter_link_previewer: + git: + url: https://github.com/Henry-Hiles/flutter_chat_ui + path: packages/flutter_link_previewer + matrix: ^4.1.0 + sqflite_common_ffi: ^2.3.6 + color_hash: ^1.0.1 + flutter_vodozemac: ^0.4.1 + flutter_widget_from_html_core: ^0.17.0 + flutter_svg: ^2.2.2 + simple_secure_storage: ^0.3.6 + json_annotation: ^4.9.0 + vodozemac: ^0.4.0 + clipboard: ^3.0.8 + shared_preferences: ^2.5.3 + mention_tag_text_field: ^0.0.9 + fluttertagger: ^2.3.1 dev_dependencies: - build_runner: ^2.15.1 - flutter_lints: 6.0.0 - freezed: 3.2.6-dev.1 - riverpod_lint: 3.1.4 - flutter_launcher_icons: 0.14.4 - json_serializable: 6.14.0 + build_runner: ^2.4.11 + custom_lint: ^0.8.0 + flutter_lints: ^6.0.0 + freezed: ^3.2.3 + riverpod_lint: ^3.0.3 + flutter_launcher_icons: ^0.14.1 + json_serializable: ^6.11.1 flutter_launcher_icons: ios: true android: true image_path: assets/icon.png - adaptive_icon_background: assets/background.png + adaptive_icon_background: "#000000" adaptive_icon_foreground: assets/foreground.png - adaptive_icon_monochrome: assets/monochrome.png - remove_alpha_ios: true - windows: - generate: true \ No newline at end of file + remove_alpha_ios: true \ No newline at end of file diff --git a/scripts/generate.dart b/scripts/generate.dart deleted file mode 100644 index 0b0cf7c..0000000 --- a/scripts/generate.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "dart:io"; -import "package:ffigen/ffigen.dart"; -import "package:path/path.dart"; -import "package:nexus/helpers/extensions/get_xcode_sdk.dart"; - -void main(List args) async { - final repoDir = Directory.fromUri(Platform.script.resolve("../gomuks")); - - print("Generating FFI Bindings..."); - - final libclangPath = Platform.environment["LIBCLANG_PATH"]; - FfiGenerator( - output: Output( - dartFile: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"), - ), - headers: Headers( - entryPoints: [File(join(repoDir.path, "pkg", "ffi", "gomuksffi.h")).uri], - compilerOptions: [ - "--no-warnings", - if (Platform.isMacOS) "-I${await getXCodeSDK()}/usr/include", - ], - ), - functions: Functions.includeAll, - ).generate( - libclangDylib: libclangPath == null - ? null - : Uri.file(join(libclangPath, "libclang.so")), - ); - print("Done!"); -} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0694802..9d7af86 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,30 +6,30 @@ #include "generated_plugin_registrant.h" -#include -#include +#include #include -#include -#include #include +#include #include +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - AppLinksPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("AppLinksPluginCApi")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); - MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); - MediaKitVideoPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + SimpleSecureStorageWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SimpleSecureStorageWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WebcryptoPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WebcryptoPlugin")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); + WindowSizePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowSizePlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 238ef93..dcf3309 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,17 +3,18 @@ # list(APPEND FLUTTER_PLUGIN_LIST - app_links - dynamic_color + dynamic_system_colors file_selector_windows - media_kit_libs_windows_video - media_kit_video screen_retriever_windows + simple_secure_storage_windows url_launcher_windows + webcrypto window_manager + window_size ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_vodozemac ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/windows/installer.iss b/windows/installer.iss deleted file mode 100644 index 22f4dcf..0000000 --- a/windows/installer.iss +++ /dev/null @@ -1,21 +0,0 @@ -[Setup] -AppName=Nexus -AppVersion=1.0.0 -DefaultDirName={pf}\Nexus -DefaultGroupName=Nexus -OutputDir=dist -OutputBaseFilename=Nexus-Setup -Compression=lzma -SolidCompression=yes -ArchitecturesInstallIn64BitMode=x64 - -[Files] -Source: "..\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion - -[Icons] -Name: "{group}\Nexus"; Filename: "{app}\nexus.exe" -Name: "{commondesktop}\Nexus"; Filename: "{app}\nexus.exe" - -[Registry] -Root: HKCU; Subkey: "Software\Classes\nexus.federated.nexus"; ValueType: string; ValueName: "URL Protocol"; ValueData: "" -Root: HKCU; Subkey: "Software\Classes\nexus.federated.nexus\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\nexus.exe"" ""%1""" \ No newline at end of file diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 590e663..453f9cf 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -1,17 +1,12 @@ #include #include #include -#include "app_links/app_links_plugin_c_api.h" + #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { - // Forward the link to an existing instance, if any, then exit. - // You may ignore the result if you need to create another window. - if (SendAppLinkToInstance()) { - return EXIT_SUCCESS; - } // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index f8a91f7..c04e20c 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ