diff --git a/.github/workflows/linux-release-beta.yml b/.github/workflows/linux-release-beta.yml new file mode 100644 index 00000000..16833dee --- /dev/null +++ b/.github/workflows/linux-release-beta.yml @@ -0,0 +1,58 @@ +name: Linux Release Debug +# Do this whenever someone pushes to the main branch +on: + push: + branches: ["next"] +jobs: + build: + # Normally you should build this stuff on older versions, but somehow older Ubuntu didn't work, don't want to debug + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + # Get flutter downloaded + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + + # Just to make sure + - run: flutter --version + + # Get the Rust toolchain + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + + # Make sure Rust is installed + - run: rustc --version + + # Get all the dependencies + - name: Get dependencies + run: flutter pub get + + - name: Update apt sources and stuff + run: sudo apt update + + - name: Install all dependencies with apt + run: sudo apt install cmake ninja-build libgtk-3-dev libunwind-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio libsodium-dev + + - name: More dependencies (GStreamer fucked the last line) + run: sudo apt install jackd libasound2-dev libjack-jackd2-dev libayatana-appindicator3-dev libopus-dev libsecret-1-dev + + # Runs a set of commands using the runners shell + - name: Start release build + run: flutter build linux --release -v + + - name: List directory (in case I'm stupid rn) + run: ls -a + + - uses: actions/upload-artifact@v4 + with: + name: linux-build + path: ./build/linux/x64/release/bundle/ + retention-days: 14 + compression-level: 6 + overwrite: false diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index a174e9fd..1b1e7fe4 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -5,8 +5,8 @@ on: branches: ["main"] jobs: build: - # Normally you should build this stuff on older versions, but I don't want people to have old shit, so we're gonna do it this way - runs-on: ubuntu-22.04 + # Normally you should build this stuff on older versions, but somehow older Ubuntu didn't work, don't want to debug + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -20,6 +20,15 @@ jobs: # Just to make sure - run: flutter --version + # Get the Rust toolchain + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + + # Make sure Rust is installed + - run: rustc --version + # Get all the dependencies - name: Get dependencies run: flutter pub get @@ -31,7 +40,7 @@ jobs: run: sudo apt install cmake ninja-build libgtk-3-dev libunwind-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio libsodium-dev - name: More dependencies (GStreamer fucked the last line) - run: sudo apt install libayatana-appindicator3-dev libsecret-1-dev + run: sudo apt install jackd libasound2-dev libjack-jackd2-dev libayatana-appindicator3-dev libopus-dev libsecret-1-dev # Runs a set of commands using the runners shell - name: Start release build diff --git a/.github/workflows/windows-release-beta.yml b/.github/workflows/windows-release-beta.yml new file mode 100644 index 00000000..1dbdb8f3 --- /dev/null +++ b/.github/workflows/windows-release-beta.yml @@ -0,0 +1,48 @@ +name: Windows Release Debug +# Do this whenever someone pushes to the main branch +on: + push: + branches: ["next"] +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + + # Get flutter downloaded + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + + # Just to make sure + - run: flutter --version + + # Get the Rust toolchain + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + + # Make sure Rust is installed + - run: rustc --version + + # Get all the dependencies + - name: Get dependencies + run: flutter pub get + + # Runs a set of commands using the runners shell + - name: Start release build + run: flutter build windows --release -v + + - name: List directory (in case I'm stupid rn) + run: ls + + - uses: actions/upload-artifact@v4 + with: + name: windows-build + path: ./build/windows/x64/runner/Release/ + retention-days: 14 + compression-level: 6 + overwrite: false diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 43458414..cac042f5 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -19,6 +19,15 @@ jobs: # Just to make sure - run: flutter --version + # Get the Rust toolchain + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + + # Make sure Rust is installed + - run: rustc --version + # Get all the dependencies - name: Get dependencies run: flutter pub get diff --git a/.metadata b/.metadata index 2d1be89a..af33b996 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" + revision: "09de023485e95e6d1225c2baa44b8feb85e0d45f" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - - platform: android - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - - platform: ios - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - - platform: linux - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + create_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f + base_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f - platform: macos - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - - platform: web - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - - platform: windows - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + create_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f + base_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f # User provided section diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b7271b..b8b1bacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,43 @@ ## 0.6.0 +### Major changes + +- Compatability with protocol v8 +- Added voice calling in Spaces using Lightwire, our own audio engine +- Added connecting to Studio, our WebRTC implementation, in Spaces +- Added a new conversation type called Square + - Multiple conversations (called Topics) can be created inside of them + - Spaces created inside of it will be displayed in the sidebar + +### Minor changes and fixes + +- Fixed that you couldn't invite people to a Space +- Fixed same addresses sometimes not being recognized +- Rewrote the entire code using new state management for a better structure + - It now uses [signals](https://pub.dev/packages/signals) instead of GetX + - It's now devided into services and controllers for a better overview + - Tabletop's architecture still needs to be improved +- Fixed Warp crashing when sharing invalid ports +- Fixed Warp allowing to share already shared ports +- Fixed being able to add accounts that are already friends as a friend +- Rewrote the entire vault synchronization for better performance and maintainability +- Rewrote the text formatting detection to make it more extensible and also more stable (\*\*\*\* no longer breaks the app) + - Regression: Links are no longer clickable (TODO: Fix before 1.0.0 Beta) +- Fixed cards in rotated inventories having an incorrect rotation +- Fixed not creating a new inventory when the old one has been deleted +- Fixed the "Edit title" button not actually doing anything +- Changed member and search sidebar to one consistent design +- Fixed member and search sidebar both being open when searching in group chats +- Fixed search not being scoped to individual conversations (and basically unusable) +- You can now choose how many dots appear +- All creation buttons are now in one menu in the sidebar +- Added a right click context menu in the sidebar +- More reliable notification handling +- The chat view now opens where the newest messages are + +## 0.6.0 + ### Architecture changes and new features - This release of Liphium is compatible with protocol v7 diff --git a/README.md b/README.md index a9c29043..5dbd1b85 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # Liphium: Decentralization for everyone -Welcome to the repository of the app used to interact with [station](https://github.com/Liphium/station), our decentralized backend service. With this app you can share files of any size, chat with anyone who has a "town" (what we call our servers) and even go into Spaces with them where you can play card games or share Minecraft servers. This is probably the quickest introduction I've ever given about the app and if you want to know more: [Our website exists](https://liphium.com). +Welcome to the repository of the app used to interact with [station](https://github.com/Liphium/station), our decentralized backend service. With this app you can share files of any size, chat with anyone who has a "town" (what we call our servers) and even go into Spaces with them where you can play card games, share Minecraft servers or just talk with them. This is probably the quickest introduction I've ever given about the app and if you want to know more: [Our website exists](https://liphium.com). -You can find a lot of information about this app [on our help & resources page](https://liphium.com/docs). Be sure to check it out, and if your question isn't answered, you can always contact us through Discord or Email (available on our website). +You can find a lot of information about the app [in the help & resources page](https://liphium.com/docs). If you want to know what's next for Liphium, check out [the roadmap](https://liphium.dev/roadmap). If your question isn't answered, you can always join the [Discord](https://liphium.dev/discord) or send an E-Mail (both available on our website). + +## Download + +You can always find up-to-date information about where Liphium is available on [the downloads page](https://liphium.com/docs/general/download/). At the time of writing this Liphium is only available on Windows through the Microsoft Store. We're working on expanding to more platforms, but there's only so much one fellow can do. If you want to help pushing out Liphium to more platforms, join the Discord and contact any developer on there. We'd really appreciate the help. ## The goal @@ -10,8 +14,9 @@ With Liphium, the attempt is to hide all of the decentralization magic and give ## This repository -With all that said, this repository contains the main app for Liphium. It's one codebase for both mobile and desktop platforms making use of the [Flutter](https://flutter.dev) framework for at least somewhat seamless experiences across platforms. The app looks the same and has the same categories for things like Settings on all platforms to make explaining it easier than ever. All settings are the same in the same order across every platform. This consistency is key to making a good app (in my opinion) and makes writing documentation and more a lot easier. Every single piece of code can be re-used making adding features quite easy across the board. +This repository contains all of the code available for Liphium's client app. ## Contributing -Hey, I appreciate that you want to contribute, but I'm sorry to report that all of the guides are just not there yet. If you are really curious about the app, you can reach out to me over on our Discord server (you can find the invite on [our website](https://liphium.com)). Contribution guides and more will be available at some point, but for now I just wanna develop this app alone and make sure there is some documentation because I don't want your experience making contributions to be really bad and I also want you to know where what is and why it's that way. The code quality is also still really bad and rough in some places, I'd also like to fix all of that before allowing contributions. So, just wait for now and let me cook. +I appreciate that you want to contribute, the problem is just that there are no guides on how to navigate this gigantic codebase and before I sent you down this big rabbit hole I want to actually provide decent information on where things are and how things work. If you just want to help fix some typos, feel free to create a pull request. If you have a feature you want to request or have a bug that's bothering you, open an issue. If you want to help ship Liphium to more platforms, either open an issue or join the Discord server and contact me through there. Once guides and more are available I'll also make an announcement somewhere. + diff --git a/analysis_options.yaml b/analysis_options.yaml index 9a004f00..1dd2f732 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -28,3 +28,6 @@ linter: # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +formatter: + page_width: 120 diff --git a/assets/NotoColorEmoji-Regular.ttf b/assets/NotoColorEmoji-Regular.ttf deleted file mode 100644 index 05b42fdc..00000000 Binary files a/assets/NotoColorEmoji-Regular.ttf and /dev/null differ diff --git a/assets/OpenSans.ttf b/assets/OpenSans.ttf deleted file mode 100644 index 99233ba1..00000000 Binary files a/assets/OpenSans.ttf and /dev/null differ diff --git a/assets/Roboto-Regular.ttf b/assets/Roboto-Regular.ttf deleted file mode 100644 index 3033308a..00000000 Binary files a/assets/Roboto-Regular.ttf and /dev/null differ diff --git a/drift_schemas/main/drift_schema_v3.json b/drift_schemas/main/drift_schema_v3.json new file mode 100644 index 00000000..11a95dc0 --- /dev/null +++ b/drift_schemas/main/drift_schema_v3.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"conversation","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ConversationType.values)","dart_type_name":"ConversationType"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"key","getter_name":"key","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_version","getter_name":"lastVersion","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"read_at","getter_name":"readAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_token","getter_name":"senderToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_address","getter_name":"senderAddress","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"conversation","getter_name":"conversation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"edited","getter_name":"edited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"edited\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"verified","getter_name":"verified","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"verified\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"verified\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"member","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"conversation_id","getter_name":"conversationId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"role_id","getter_name":"roleId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"setting","was_declared_in_moor":false,"columns":[{"name":"key","getter_name":"key","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"value","getter_name":"value","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["key"]}},{"id":4,"references":[],"type":"table","data":{"name":"friend","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":5,"references":[],"type":"table","data":{"name":"request","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"self","getter_name":"self","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"self\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"self\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":6,"references":[],"type":"table","data":{"name":"unknown_profile","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":7,"references":[],"type":"table","data":{"name":"profile","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"picture_container","getter_name":"pictureContainer","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":8,"references":[],"type":"table","data":{"name":"trusted_link","was_declared_in_moor":false,"columns":[{"name":"domain","getter_name":"domain","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["domain"]}},{"id":9,"references":[],"type":"table","data":{"name":"library_entry","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(LibraryEntryType.values)","dart_type_name":"LibraryEntryType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":10,"references":[0],"type":"index","data":{"on":0,"name":"idx_conversation_updated","sql":null,"unique":false,"columns":["updated_at"]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"idx_message_created","sql":null,"unique":false,"columns":["created_at"]}},{"id":12,"references":[4],"type":"index","data":{"on":4,"name":"idx_friends_updated","sql":null,"unique":false,"columns":["updated_at"]}},{"id":13,"references":[9],"type":"index","data":{"on":9,"name":"idx_library_entry_created","sql":null,"unique":false,"columns":["created_at"]}}]} \ No newline at end of file diff --git a/drift_schemas/main/drift_schema_v4.json b/drift_schemas/main/drift_schema_v4.json new file mode 100644 index 00000000..e08604a0 --- /dev/null +++ b/drift_schemas/main/drift_schema_v4.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"conversation","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ConversationType.values)","dart_type_name":"ConversationType"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"key","getter_name":"key","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_version","getter_name":"lastVersion","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"read_at","getter_name":"readAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_token","getter_name":"senderToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_address","getter_name":"senderAddress","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"conversation","getter_name":"conversation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"edited","getter_name":"edited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"edited\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"verified","getter_name":"verified","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"verified\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"verified\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"member","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"conversation_id","getter_name":"conversationId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"role_id","getter_name":"roleId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"setting","was_declared_in_moor":false,"columns":[{"name":"key","getter_name":"key","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"value","getter_name":"value","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["key"]}},{"id":4,"references":[],"type":"table","data":{"name":"friend","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":5,"references":[],"type":"table","data":{"name":"request","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"self","getter_name":"self","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"self\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"self\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":6,"references":[],"type":"table","data":{"name":"unknown_profile","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_fetched","getter_name":"lastFetched","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":7,"references":[],"type":"table","data":{"name":"profile","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"picture_container","getter_name":"pictureContainer","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":8,"references":[],"type":"table","data":{"name":"trusted_link","was_declared_in_moor":false,"columns":[{"name":"domain","getter_name":"domain","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["domain"]}},{"id":9,"references":[],"type":"table","data":{"name":"library_entry","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(LibraryEntryType.values)","dart_type_name":"LibraryEntryType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"identifier_hash","getter_name":"identifierHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'to-migrate\\'')","default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":10,"references":[0],"type":"index","data":{"on":0,"name":"idx_conversation_updated","sql":null,"unique":false,"columns":["updated_at"]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"idx_message_created","sql":null,"unique":false,"columns":["created_at"]}},{"id":12,"references":[4],"type":"index","data":{"on":4,"name":"idx_friends_updated","sql":null,"unique":false,"columns":["updated_at"]}},{"id":13,"references":[5],"type":"index","data":{"on":5,"name":"idx_requests_updated","sql":null,"unique":false,"columns":["updated_at"]}},{"id":14,"references":[6],"type":"index","data":{"on":6,"name":"idx_unknown_profiles_last_fetched","sql":null,"unique":false,"columns":["last_fetched"]}},{"id":15,"references":[9],"type":"index","data":{"on":9,"name":"idx_library_entry_created","sql":null,"unique":false,"columns":["created_at"]}},{"id":16,"references":[9],"type":"index","data":{"on":9,"name":"idx_library_entry_idhash","sql":null,"unique":false,"columns":["identifier_hash"]}}]} \ No newline at end of file diff --git a/drift_schemas/main/drift_schema_v5.json b/drift_schemas/main/drift_schema_v5.json new file mode 100644 index 00000000..da58299f --- /dev/null +++ b/drift_schemas/main/drift_schema_v5.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"conversation","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ConversationType.values)","dart_type_name":"ConversationType"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"key","getter_name":"key","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_version","getter_name":"lastVersion","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reads","getter_name":"reads","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_token","getter_name":"senderToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_address","getter_name":"senderAddress","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"conversation","getter_name":"conversation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"edited","getter_name":"edited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"edited\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"verified","getter_name":"verified","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"verified\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"verified\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"member","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"conversation_id","getter_name":"conversationId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"role_id","getter_name":"roleId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"setting","was_declared_in_moor":false,"columns":[{"name":"key","getter_name":"key","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"value","getter_name":"value","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["key"]}},{"id":4,"references":[],"type":"table","data":{"name":"friend","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":5,"references":[],"type":"table","data":{"name":"request","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"self","getter_name":"self","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"self\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"self\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"vault_id","getter_name":"vaultId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":6,"references":[],"type":"table","data":{"name":"unknown_profile","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"keys","getter_name":"keys","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_fetched","getter_name":"lastFetched","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":7,"references":[],"type":"table","data":{"name":"profile","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"picture_container","getter_name":"pictureContainer","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":8,"references":[],"type":"table","data":{"name":"trusted_link","was_declared_in_moor":false,"columns":[{"name":"domain","getter_name":"domain","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["domain"]}},{"id":9,"references":[],"type":"table","data":{"name":"library_entry","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(LibraryEntryType.values)","dart_type_name":"LibraryEntryType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"identifier_hash","getter_name":"identifierHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'to-migrate\\'')","default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":10,"references":[0],"type":"index","data":{"on":0,"name":"idx_conversation_updated","sql":null,"unique":false,"columns":["updated_at"]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"idx_message_created","sql":null,"unique":false,"columns":["created_at"]}},{"id":12,"references":[4],"type":"index","data":{"on":4,"name":"idx_friends_updated","sql":null,"unique":false,"columns":["updated_at"]}},{"id":13,"references":[5],"type":"index","data":{"on":5,"name":"idx_requests_updated","sql":null,"unique":false,"columns":["updated_at"]}},{"id":14,"references":[6],"type":"index","data":{"on":6,"name":"idx_unknown_profiles_last_fetched","sql":null,"unique":false,"columns":["last_fetched"]}},{"id":15,"references":[9],"type":"index","data":{"on":9,"name":"idx_library_entry_created","sql":null,"unique":false,"columns":["created_at"]}},{"id":16,"references":[9],"type":"index","data":{"on":9,"name":"idx_library_entry_idhash","sql":null,"unique":false,"columns":["identifier_hash"]}}]} \ No newline at end of file diff --git a/flutter_rust_bridge.yaml b/flutter_rust_bridge.yaml new file mode 100644 index 00000000..2ddbad69 --- /dev/null +++ b/flutter_rust_bridge.yaml @@ -0,0 +1,3 @@ +rust_input: crate::api +rust_root: libspaceship/ +dart_output: lib/src/rust \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart index 1b42d5f3..1cf1a659 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -3,16 +3,17 @@ import 'package:chat_interface/theme/theme_manager.dart'; import 'package:chat_interface/translations/translations.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ChatApp extends StatelessWidget { const ChatApp({super.key}); @override Widget build(BuildContext context) { - return Obx(() { + return Watch((ctx) { return GetMaterialApp( title: 'Liphium', - theme: Get.find().currentTheme.value, + theme: ThemeManager.currentTheme.value, translations: MainTranslations(), locale: Get.deviceLocale, fallbackLocale: const Locale("en", "US"), diff --git a/lib/connection/chat/setup_listener.dart b/lib/connection/chat/setup_listener.dart deleted file mode 100644 index 127f4062..00000000 --- a/lib/connection/chat/setup_listener.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; - -import 'package:chat_interface/connection/connection.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/messaging.dart'; -import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/controller/current/steps/account_step.dart'; -import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/theme/ui/profile/status_renderer.dart'; -import 'package:chat_interface/util/web.dart'; -import 'package:get/get.dart'; -import 'package:drift/drift.dart'; - -import '../../util/logging_framework.dart'; - -void setupSetupListeners() { - //* New status - connector.listen("setup", (event) { - final data = event.data["data"]! as String; - final controller = Get.find(); - - if (data == "" || data == "-") { - controller.status.value = ""; - controller.type.value = statusOnline; - subscribeToConversations(); - return; - } - - // Decrypt status with profile key - controller.fromStatusJson(decryptSymmetric(data, profileKey)); - - subscribeToConversations(); - }, afterSetup: true); -} - -// status is going to be encrypted in this function -Future subscribeToConversations({StatusController? controller}) async { - // Encrypt status with profile key - controller ??= Get.find(); - - // Subscribe to all conversations - final tokens = >[]; - for (var conversation in Get.find().conversations.values) { - tokens.add(conversation.token.toMap()); - } - - // Subscribe - unawaited(_sub(controller.statusPacket(), controller.sharedContentPacket(), tokens, deletions: true)); - return true; -} - -void subscribeToConversation(ConversationToken token, {StatusController? controller, deletions = true}) { - // Encrypt status with profile key - controller ??= Get.find(); - - // Subscribe to all conversations - final tokens = >[token.toMap()]; - - // Subscribe - unawaited(_sub(controller.statusPacket(), controller.sharedContentPacket(), tokens, deletions: deletions)); -} - -Future _sub(String status, String statusData, List> tokens, {deletions = false}) async { - // Get the maximum value of the conversation update timestamps - final max = db.conversation.updatedAt.max(); - final query = db.selectOnly(db.conversation)..addColumns([max]); - final maxValue = await query.map((row) => row.read(max)).getSingleOrNull(); - - connector.sendAction( - ServerAction("conv_sub", { - "tokens": tokens, - "status": status, - "sync": maxValue?.toInt() ?? 0, - "data": statusData, - }), handler: (event) { - if (!event.data["success"]) { - sendLog("ERROR WHILE SUBSCRIBING: ${event.data["message"]}"); - return; - } - Get.find().statusLoading.value = false; - Get.find().finishedLoading( - basePath, - event.data["info"], - deletions ? (event.data["missing"] ?? []) : [], - false, - ); - }); -} diff --git a/lib/connection/spaces/space_connection.dart b/lib/connection/spaces/space_connection.dart deleted file mode 100644 index f99ff0ba..00000000 --- a/lib/connection/spaces/space_connection.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:chat_interface/connection/connection.dart'; -import 'package:chat_interface/connection/messaging.dart'; -import 'package:chat_interface/connection/spaces/space_message_listener.dart'; -import 'package:chat_interface/connection/spaces/tabletop_listener.dart'; -import 'package:chat_interface/connection/spaces/warp_listener.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; -import 'package:chat_interface/main.dart'; -import 'package:chat_interface/util/popups.dart'; -import 'package:get/get.dart'; - -/// Connector the the space node. -Connector spaceConnector = Connector(); - -/// Connect to the space node. -Future createSpaceConnection(String domain, String token) async { - return await spaceConnector.connect("${isHttps ? "wss://" : "ws://"}$domain/gateway", token, restart: false, onDone: ((error) { - if (error) { - showErrorPopup("error", "spaces.connection_error".tr); - } - Get.find().leaveCall(error: error); - })); -} - -/// Setup listeners for space events. -void setupSpaceListeners() { - // Listen for room data changes - spaceConnector.listen("room_data", (event) => handleRoomData(event)); // Sent on change - spaceConnector.listen("room_info", (event) => handleRoomData(event)); // Sent on join - - setupTabletopListeners(); - setupSpaceMessageListeners(); - WarpListener.setupWarpListeners(); -} - -void handleRoomData(Event event) { - final controller = Get.find(); - controller.start.value = DateTime.fromMillisecondsSinceEpoch(event.data["start"]); - - // Update members - Get.find().onMembersChanged(event.data["members"]); -} diff --git a/lib/connection/spaces/space_message_listener.dart b/lib/connection/spaces/space_message_listener.dart deleted file mode 100644 index ae5a85af..00000000 --- a/lib/connection/spaces/space_message_listener.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:chat_interface/connection/spaces/space_connection.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_message_controller.dart'; -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:get/get.dart'; - -void setupSpaceMessageListeners() { - // Listen for deletions - spaceConnector.listen("msg", (event) async { - // Make sure we're actually in a space right now - if (!Get.find().inSpace.value || SpacesController.key == null) { - sendLog("WARNING: received space message even though not in space"); - return; - } - - // Unpack the message in a different isolate (to prevent lag) - final message = await SpacesMessageProvider.unpackMessageInIsolate(event.data["msg"]); - - // Check if there are too many attachments - if (message.attachments.length > 5) { - sendLog("WARNING: invalid message, more than 5 attachments"); - return; - } - - // Tell the controller about the message - Get.find().addMessage(message); - }); -} diff --git a/lib/connection/spaces/tabletop_listener.dart b/lib/connection/spaces/tabletop_listener.dart deleted file mode 100644 index cf2cdbce..00000000 --- a/lib/connection/spaces/tabletop_listener.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'dart:ui'; - -import 'package:chat_interface/connection/spaces/space_connection.dart'; -import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:get/get.dart'; - -void setupTabletopListeners() { - final controller = Get.find(); - - spaceConnector.listen("table_obj", (event) { - for (var obj in event.data["obj"]) { - controller.addObject(controller.newObject( - TableObjectType.values[obj["t"]], - obj["id"] as String, - obj["o"] as int, - Offset((obj["x"] as num).toDouble(), (obj["y"] as num).toDouble()), - Size((obj["w"] as num).toDouble(), (obj["h"] as num).toDouble()), - (obj["r"] as num).toDouble(), - obj["d"], - )); - } - }); - - // Listen for creations - spaceConnector.listen("tobj_created", (event) { - if (event.data["c"] == SpaceMemberController.ownId) { - return; - } - - controller.addObject(controller.newObject( - TableObjectType.values[event.data["type"]], - event.data["id"], - event.data["o"], - Offset((event.data["x"] as num).toDouble(), (event.data["y"] as num).toDouble()), - Size((event.data["w"] as num).toDouble(), (event.data["h"] as num).toDouble()), - (event.data["r"] as num).toDouble(), - event.data["data"], - )); - }); - - // Listen for a new order of an object - spaceConnector.listen( - "tobj_order", - (event) { - var objectId = event.data["o"]; - var newOrder = (event.data["or"] as num).toInt(); - controller.setOrder(objectId, newOrder); - }, - ); - - // Listen for cursor movements - spaceConnector.listen("tc_moved", (event) { - controller.updateCursor( - event.data["c"], - Offset((event.data["x"] as num).toDouble(), (event.data["y"] as num).toDouble()), - (event.data["col"] as num).toDouble(), - ); - }); - - // Listen for deletions - spaceConnector.listen("tobj_deleted", (event) { - controller.removeObject(id: event.data["id"]); - }); - - // Listen for moves - spaceConnector.listen("tobj_moved", (event) { - final object = controller.objects[event.data["id"]]; - if (object == null || object == controller.heldObject) { - return; - } - object.move(Offset((event.data["x"] as num).toDouble(), (event.data["y"] as num).toDouble())); - }); - - // Listen for rotations - spaceConnector.listen("tobj_rotated", (event) { - final object = controller.objects[event.data["id"]]; - if (object == null) { - return; - } - object.rotate((event.data["r"] as num).toDouble()); - }); - - // Listen for modifications - spaceConnector.listen("tobj_modified", (event) { - final object = controller.objects[event.data["id"]]; - if (object == null) { - return; - } - object.decryptData(event.data["data"]); - sendLog(event.data["w"]); - object.size = Size((event.data["w"] as num).toDouble(), (event.data["h"] as num).toDouble()); - }); - - // Listen for when edits are allowed - spaceConnector.listen("tobj_mqueue_allowed", (event) { - final object = controller.objects[event.data["id"]]; - if (object == null) { - sendLog("object not found, modification can't be done"); - return; - } - object.dataCallback?.call(); - }); -} diff --git a/lib/connection/spaces/warp_listener.dart b/lib/connection/spaces/warp_listener.dart deleted file mode 100644 index 284bae3a..00000000 --- a/lib/connection/spaces/warp_listener.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:convert'; - -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/spaces/space_connection.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; -import 'package:chat_interface/controller/spaces/warp_controller.dart'; -import 'package:get/get.dart'; - -class WarpListener { - static void setupWarpListeners() { - // Listen for new Warps created on the server - spaceConnector.listen("wp_new", (event) { - // Add the container to the list of Warps on the server - final controller = Get.find(); - final container = WarpShareContainer( - id: event.data["w"], - account: controller.members[event.data["h"]]!.friend, - port: event.data["p"] as int, - ); - Get.find().warps.add(container); - }); - - // Listen for the Warps that end - spaceConnector.listen("wp_end", (event) { - Get.find().onWarpEnd(event.data["w"]); - }); - - // Listen for packets meant for the local server (hoster -> current client) - spaceConnector.listen("wp_to", (event) { - // Get the Warp and make sure it's not null - final warp = Get.find().activeWarps[event.data["w"]]; - if (warp == null) { - return; - } - - // Decrypt the content and forward - final decrypted = decryptSymmetricBytes(base64Decode(event.data["p"]), SpacesController.key!); - warp.forwardPacketToSocket(event.data["c"], decrypted, event.data["s"]); - }); - - // Listen for packets meant for the local server (current client -> hoster) - spaceConnector.listen("wp_back", (event) { - // Get the Warp and make sure it's not null - final warp = Get.find().sharedWarps[event.data["w"]]; - if (warp == null) { - return; - } - - // Decrypt the content and handle receiving - final decrypted = decryptSymmetricBytes(base64Decode(event.data["p"]), SpacesController.key!); - warp.receivePacketFromClient(event.data["s"], event.data["c"], decrypted, event.data["sq"]); - }); - - // Listen for clients disconnecting from the shared server (as hoster) - spaceConnector.listen("wp_disconnected", (event) { - // Get the Warp and make sure it's not null - final warp = Get.find().sharedWarps[event.data["w"]]; - if (warp == null) { - return; - } - - // Decrypt the content and handle receiving - warp.handleDisconnect(event.data["c"]); - }); - } -} diff --git a/lib/controller/account/friend_controller.dart b/lib/controller/account/friend_controller.dart new file mode 100644 index 00000000..046197a6 --- /dev/null +++ b/lib/controller/account/friend_controller.dart @@ -0,0 +1,310 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui' as ui; + +import 'package:chat_interface/main.dart'; +import 'package:chat_interface/services/chat/friends_service.dart'; +import 'package:chat_interface/services/chat/requests_service.dart'; +import 'package:chat_interface/services/chat/vault_versioning_service.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/services/chat/profile_picture_helper.dart'; +import 'package:chat_interface/controller/account/requests_controller.dart'; +import 'package:chat_interface/controller/conversation/attachment_controller.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/controller/current/steps/account_step.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; +import 'package:chat_interface/standards/server_stored_information.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:drift/drift.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; +import 'package:sodium_libs/sodium_libs.dart'; + +part '../../services/chat/friends_vault.dart'; + +class FriendController { + static final friends = mapSignal({}); + + static Future loadFriends() async { + for (FriendData data in await db.friend.select().get()) { + friends[LPHAddress.from(data.id)] = Friend.fromEntity(data); + } + return true; + } + + static void addSelf() { + friends[StatusController.ownAddress] = Friend.me(); + } + + static void reset() { + friends.clear(); + } + + static void addOrUpdate(Friend friend) { + if (friends[friend.id] != null) { + friends[friend.id]!.copyFrom(friend); + } else { + friends[friend.id] = friend; + } + } + + static Friend getFriend(LPHAddress address) { + if (StatusController.ownAddress == address) return Friend.me(); + return friends[address] ?? Friend.unknown(address); + } +} + +class Friend { + LPHAddress id; + String name; + String vaultId; + KeyStorage _keyStorage; + bool unknown; + Timer? _timer; + int updatedAt; + + /// Get the key storage of the friend (future because the key storage of the current client may still be loading). + Future getKeys() async { + if (id == StatusController.ownAddress) { + await AccountStep.keyCompleter?.future; + return _keyStorage; + } + + return _keyStorage; + } + + /// Set the key storage of the friend. + /// + /// Should only be used for updating the key storage of the current client. + void setKeyStorage(KeyStorage storage) { + assert(id == StatusController.ownAddress); + _keyStorage = storage; + } + + // Display name of the friend + final displayName = signal(""); + + /// Loading state for open conversation buttons + final openConversationLoading = signal(false); + + Friend( + this.id, + this.name, + String displayName, + this.vaultId, + this._keyStorage, + this.updatedAt, { + this.unknown = false, + }) { + this.displayName.value = displayName; + } + + /// The friend for a system component (used in system messages for members) + factory Friend.system() { + return Friend(LPHAddress(basePath, "system"), "system", "system", "", KeyStorage.empty(), 0); + } + + /// Own account as a friend (used to make implementations simpler) + factory Friend.me() { + return Friend( + StatusController.ownAddress, + StatusController.name.value, + StatusController.displayName.value, + "", + KeyStorage.empty(), + 0, + ); + } + + /// Used for unknown accounts where only an id is known + factory Friend.unknown(LPHAddress address) { + var shownId = "removed".tr; + if (address.id.length >= 5) { + shownId = address.id.substring(0, 5); + } + final friend = Friend(address, "lph-$shownId", "lph-$shownId", "", KeyStorage.empty(), 0); + friend.unknown = true; + return friend; + } + + /// Convert the database entity to the actual type + factory Friend.fromEntity(FriendData data) { + return Friend( + LPHAddress.from(data.id), + fromDbEncrypted(data.name), + fromDbEncrypted(data.displayName), + fromDbEncrypted(data.vaultId), + KeyStorage.fromJson(jsonDecode(fromDbEncrypted(data.keys))), + data.updatedAt.toInt(), + ); + } + + /// Convert a json to a friend (used for friends vault) + factory Friend.fromStoredPayload(String id, int updatedAt, Map json) { + return Friend(LPHAddress.from(json["id"]), json["name"], json["dname"], id, KeyStorage.fromJson(json), updatedAt); + } + + // Convert to a stored payload for the friends vault + Future toStoredPayload() async { + final reqPayload = { + "rq": false, // If it is a request or not (requests are stored in the same place) + "id": id.encode(), + "name": name, + "dname": displayName.value, + }; + reqPayload.addAll((await getKeys()).toJson()); + + return jsonEncode(reqPayload); + } + + /// Copy of all of the values from another friend into this one. + Future copyFrom(Friend friend) async { + id = friend.id; + vaultId = friend.vaultId; + _keyStorage = await friend.getKeys(); + displayName.value = friend.displayName.value; + name = friend.name; + updatedAt = friend.updatedAt; + } + + /// Copy this friend for editing. + Future copy() async { + return Friend(id, name, displayName.value, vaultId, await getKeys(), updatedAt); + } + + // Check if vault id is known (this would require a restart of the app) + bool canBeDeleted() => vaultId != ""; + + Future entity() async { + return FriendData( + id: id.encode(), + name: dbEncrypted(name), + displayName: dbEncrypted(displayName.value), + vaultId: dbEncrypted(vaultId), + keys: dbEncrypted(jsonEncode((await getKeys()).toJson())), + updatedAt: BigInt.from(updatedAt), + ); + } + + //* Status + final status = signal(""); + bool answerStatus = true; + final statusType = signal(0); + + Future loadStatus(String message) async { + message = decryptSymmetric(message, (await getKeys()).profileKey); + final data = jsonDecode(message); + try { + status.value = utf8.decode(base64Decode(data["s"])); + } catch (e) { + status.value = ""; + } + statusType.value = data["t"]; + + if (id != StatusController.ownAddress) { + _timer?.cancel(); + _timer = Timer(const Duration(minutes: 2), () { + setOffline(); + answerStatus = true; + _timer = null; + }); + } + } + + void setOffline() { + status.value = ""; + statusType.value = 0; + StatusController.sharedContent.remove(id); + } + + //* Profile picture + AttachmentContainer? profilePicture; + final profilePictureImage = signal(null); + bool profilePictureDataNull = false; + DateTime lastProfilePictureUpdate = DateTime.fromMillisecondsSinceEpoch(0); + + /// Update the profile picture of this friend + Future updateProfilePicture(AttachmentContainer? picture) async { + if (picture == null) { + // Delete the profile picture if it is null + await db.profile.insertOnConflictUpdate(ProfileData(id: id.encode(), pictureContainer: "", data: "")); + + // Update the friend as well + profilePicture = null; + profilePictureImage.value = null; + profilePictureDataNull = true; + } else { + // Set a new profile picture if it is valid + await db.profile.insertOnConflictUpdate( + ProfileData(id: id.encode(), pictureContainer: dbEncrypted(jsonEncode(picture.toJson())), data: ""), + ); + + // Update in the local cache (for this friend) + profilePicture = picture; + profilePictureImage.value = await ProfileHelper.loadImageFromBytes(await picture.file!.readAsBytes()); + } + } + + /// Load the profile picture of this friend + Future loadProfilePicture() async { + if (unknown) { + return; + } + + // Check if we should check for changes to the profile picture + if (DateTime.now().difference(lastProfilePictureUpdate).inMinutes >= 5) { + lastProfilePictureUpdate = DateTime.now(); + + // Query the server for updates + final result = await ProfileHelper.downloadProfilePicture(this); + if (result != null) { + return; + } + + // Set the profile picture image to null to make it reload + profilePictureDataNull = false; + profilePictureImage.value = null; + } + + // Return if image is already loaded + if (profilePictureImage.value != null || profilePictureDataNull) return; + + // Load the image + final data = await ProfileHelper.getProfileDataLocal(id.encode()); + if (data == null) { + profilePictureDataNull = true; // To prevent this thing from constantly loading again + return; + } + profilePictureDataNull = false; + + // Check if there is a profile picture + if (data.pictureContainer == "") { + profilePictureDataNull = true; + return; + } + + // Load the profile picture + final json = jsonDecode(fromDbEncrypted(data.pictureContainer)); + final type = await AttachmentController.checkLocations(json["i"], StorageType.permanent); + profilePicture = AttachmentController.fromJson(type, json); + + // Make sure the file actually exists + if (!await doesFileExist(profilePicture!.file!)) { + return; + } + + profilePictureImage.value = await ProfileHelper.loadImageFromBytes(await profilePicture!.file!.readAsBytes()); + + sendLog("Profile picture set for $name"); + } + + /// Remove the friend. Just calls [FriendsService.remove] for you. + /// + /// Returns an error if there was one. + Future remove({bool removeAction = true}) { + return FriendsService.remove(this, removeAction: removeAction); + } +} diff --git a/lib/controller/account/friends/friend_controller.dart b/lib/controller/account/friends/friend_controller.dart deleted file mode 100644 index a19564fa..00000000 --- a/lib/controller/account/friends/friend_controller.dart +++ /dev/null @@ -1,386 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:ui' as ui; - -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/connection/encryption/hash.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/chat/stored_actions_listener.dart'; -import 'package:chat_interface/controller/account/profile_picture_helper.dart'; -import 'package:chat_interface/controller/account/friends/requests_controller.dart'; -import 'package:chat_interface/controller/account/unknown_controller.dart'; -import 'package:chat_interface/controller/conversation/attachment_controller.dart'; -import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/controller/current/steps/account_step.dart'; -import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/database/database_entities.dart' as dbe; -import 'package:chat_interface/pages/status/setup/instance_setup.dart'; -import 'package:chat_interface/standards/server_stored_information.dart'; -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:chat_interface/util/popups.dart'; -import 'package:chat_interface/util/web.dart'; -import 'package:drift/drift.dart'; -import 'package:get/get.dart'; -import 'package:sodium_libs/sodium_libs.dart'; - -part 'friends_vault.dart'; - -class FriendController extends GetxController { - final friends = {}.obs; - - Future loadFriends() async { - for (FriendData data in await db.friend.select().get()) { - friends[LPHAddress.from(data.id)] = Friend.fromEntity(data); - } - return true; - } - - void addSelf() { - friends[StatusController.ownAddress] = Friend.me(); - } - - void reset() { - friends.clear(); - } - - // Add friend (also sends data to server vault) - Future addFromRequest(Request request) async { - sendLog("adding friend from request ${request.friend.id}"); - - // Query the guy - final guy = await Get.find().loadUnknownProfile(request.id); - if (guy == null) { - sendLog("friend request is invalid cause couldn't find sender"); - return false; - } - - // Check if the guy in the request has the same name and stuff (in base64 cause otherwise it doesn't work, thanks dart) - if (base64Encode(request.keyStorage.publicKey) != base64Encode(guy.publicKey) || - base64Encode(guy.signatureKey) != base64Encode(request.keyStorage.signatureKey)) { - sendLog("friend request has invalid keys"); - return false; - } - - // Set name and display name from the server - request.displayName = guy.displayName; - request.name = guy.name; - - // Remove from requests controller - await Get.find().deleteSentRequest(request); - - // Remove request from server - final friendsVault = await FriendsVault.remove(request.vaultId); - if (!friendsVault) { - add(request.friend); // Add regardless cause restart of the app fixes not being able to remove the guy - return false; - } - - // Add friend to vault - final id = await FriendsVault.store( - request.friend.toStoredPayload(), - lastPacket: request.updatedAt, - errorPopup: true, - prefix: "friend", - ); - - // Don't add if something failed - if (id == null) { - return false; - } - - // Add friend to database with vault id - request.vaultId = id; - add(request.friend); - - return true; - } - - void add(Friend friend) { - friends[friend.id] = friend; - if (friend.id != StatusController.ownAddress) { - db.friend.insertOnConflictUpdate(friend.entity()); - } - } - - Future remove(Friend friend, {removal = true}) async { - if (removal) { - friends.remove(friend.id); - } - await db.friend.deleteWhere((tbl) => tbl.id.equals(friend.id.encode())); - return true; - } - - Friend getFriend(LPHAddress address) { - if (StatusController.ownAddress == address) return Friend.me(); - return friends[address] ?? Friend.unknown(address); - } -} - -class Friend { - LPHAddress id; - String name; - String vaultId; - KeyStorage keyStorage; - bool unknown; - Timer? _timer; - int updatedAt; - - // Display name of the friend - final displayName = "".obs; - - void updateDisplayName(String displayName) { - if (id == StatusController.ownAddress) { - return; - } - this.displayName.value = displayName; - db.friend.insertOnConflictUpdate(entity()); - } - - /// Loading state for open conversation buttons - final openConversationLoading = false.obs; - - Friend(this.id, this.name, String displayName, this.vaultId, this.keyStorage, this.updatedAt, {this.unknown = false}) { - this.displayName.value = displayName; - } - - /// The friend for a system component (used in system messages for members) - factory Friend.system() { - return Friend(LPHAddress(basePath, "system"), "system", "system", "", KeyStorage.empty(), 0); - } - - /// Own account as a friend (used to make implementations simpler) - factory Friend.me([StatusController? controller]) { - controller ??= Get.find(); - return Friend( - StatusController.ownAddress, - controller.name.value, - controller.displayName.value, - "", - KeyStorage.empty(), - 0, - ); - } - - /// Used for unknown accounts where only an id is known - factory Friend.unknown(LPHAddress address) { - var shownId = "removed".tr; - if (address.id.length >= 5) { - shownId = address.id.substring(0, 5); - } - final friend = Friend(address, "lph-$shownId", "lph-$shownId", "", KeyStorage.empty(), 0); - friend.unknown = true; - return friend; - } - - /// Convert the database entity to the actual type - factory Friend.fromEntity(FriendData data) { - return Friend( - LPHAddress.from(data.id), - fromDbEncrypted(data.name), - fromDbEncrypted(data.displayName), - fromDbEncrypted(data.vaultId), - KeyStorage.fromJson(jsonDecode(fromDbEncrypted(data.keys))), - data.updatedAt.toInt(), - ); - } - - /// Convert a json to a friend (used for friends vault) - factory Friend.fromStoredPayload(Map json, int updatedAt) { - return Friend( - LPHAddress.from(json["id"]), - json["name"], - json["dname"], - "", - KeyStorage.fromJson(json), - updatedAt, - ); - } - - // Convert to a stored payload for the friends vault - String toStoredPayload() { - final reqPayload = { - "rq": false, // If it is a request or not (requests are stored in the same place) - "id": id.encode(), - "name": name, - "dname": displayName.value, - }; - reqPayload.addAll(keyStorage.toJson()); - - return jsonEncode(reqPayload); - } - - // Check if vault id is known (this would require a restart of the app) - bool canBeDeleted() => vaultId != ""; - - FriendData entity() => FriendData( - id: id.encode(), - name: dbEncrypted(name), - displayName: dbEncrypted(displayName.value), - vaultId: dbEncrypted(vaultId), - keys: dbEncrypted(jsonEncode(keyStorage.toJson())), - updatedAt: BigInt.from(updatedAt), - ); - - // Update in database - Future update() async { - if (id == StatusController.ownAddress || unknown) { - return false; - } - await FriendsVault.remove(vaultId); - final result = await FriendsVault.store(toStoredPayload()); - if (result == null) { - sendLog("FRIEND CONFLICT: Couldn't update in vault!"); - return true; - } - vaultId = result; - await db.friend.insertOnConflictUpdate(entity()); - return true; - } - - //* Status - final status = "".obs; - bool answerStatus = true; - final statusType = 0.obs; - - void loadStatus(String message) { - message = decryptSymmetric(message, keyStorage.profileKey); - final data = jsonDecode(message); - try { - status.value = utf8.decode(base64Decode(data["s"])); - } catch (e) { - status.value = ""; - } - statusType.value = data["t"]; - - if (id != StatusController.ownAddress) { - _timer?.cancel(); - _timer = Timer(const Duration(minutes: 2), () { - setOffline(); - answerStatus = true; - _timer = null; - }); - } - } - - void setOffline() { - status.value = ""; - statusType.value = 0; - Get.find().sharedContent.remove(id); - } - - //* Profile picture - AttachmentContainer? profilePicture; - final profilePictureImage = Rx(null); - bool profilePictureDataNull = false; - DateTime lastProfilePictureUpdate = DateTime.fromMillisecondsSinceEpoch(0); - - /// Update the profile picture of this friend - Future updateProfilePicture(AttachmentContainer? picture) async { - if (picture == null) { - // Delete the profile picture if it is null - await db.profile.insertOnConflictUpdate(ProfileData( - id: id.encode(), - pictureContainer: "", - data: "", - )); - - // Update the friend as well - profilePicture = null; - profilePictureImage.value = null; - profilePictureDataNull = true; - } else { - // Set a new profile picture if it is valid - await db.profile.insertOnConflictUpdate(ProfileData( - id: id.encode(), - pictureContainer: dbEncrypted(jsonEncode(picture.toJson())), - data: "", - )); - - // Update in the local cache (for this friend) - profilePicture = picture; - profilePictureImage.value = await ProfileHelper.loadImageFromBytes(await picture.file!.readAsBytes()); - } - } - - /// Load the profile picture of this friend - Future loadProfilePicture() async { - if (unknown) { - return; - } - - // Check if we should check for changes to the profile picture - if (DateTime.now().difference(lastProfilePictureUpdate).inMinutes >= 5) { - lastProfilePictureUpdate = DateTime.now(); - - // Query the server for updates - final result = await ProfileHelper.downloadProfilePicture(this); - if (result != null) { - return; - } - - // Set the profile picture image to null to make it reload - profilePictureDataNull = false; - profilePictureImage.value = null; - } - - // Return if image is already loaded - if (profilePictureImage.value != null || profilePictureDataNull) return; - - // Load the image - final data = await ProfileHelper.getProfileDataLocal(id.encode()); - if (data == null) { - profilePictureDataNull = true; // To prevent this thing from constantly loading again - return; - } - profilePictureDataNull = false; - - // Check if there is a profile picture - if (data.pictureContainer == "") { - profilePictureDataNull = true; - return; - } - - // Load the profile picture - final json = jsonDecode(fromDbEncrypted(data.pictureContainer)); - final type = await AttachmentController.checkLocations(json["i"], StorageType.permanent); - profilePicture = Get.find().fromJson(type, json); - - // Make sure the file actually exists - if (!await doesFileExist(profilePicture!.file!)) { - return; - } - - profilePictureImage.value = await ProfileHelper.loadImageFromBytes(await profilePicture!.file!.readAsBytes()); - } - - //* Remove friend - Future remove(RxBool loading, {bool removeAction = true}) async { - loading.value = true; - - // Remove the friend from the friends vault and local storage - await FriendsVault.remove(vaultId); - await db.friend.deleteWhere((tbl) => tbl.id.equals(id.encode())); - Get.find().friends.remove(id); - - if (removeAction) { - // Send the other guy a notice that he's been removed from your friends list - await sendAuthenticatedStoredAction(this, authenticatedStoredAction("fr_rem", {})); - } - - // Leave direct message conversations with the guy in them - var toRemove = []; - final controller = Get.find(); - for (var conversation in controller.conversations.values) { - if (conversation.members.values.any((mem) => mem.address == id) && conversation.type == dbe.ConversationType.directMessage) { - toRemove.add(conversation.id); - } - } - for (var key in toRemove) { - await controller.conversations[key]!.delete(); - } - - loading.value = false; - return true; - } -} diff --git a/lib/controller/account/friends/friends_vault.dart b/lib/controller/account/friends/friends_vault.dart deleted file mode 100644 index 3f355fe8..00000000 --- a/lib/controller/account/friends/friends_vault.dart +++ /dev/null @@ -1,108 +0,0 @@ -part of 'friend_controller.dart'; - -class FriendsVault { - /// Store friend in vault (returns id of the friend in the vault if successful) - static Future store(String data, {errorPopup = false, prefix = "", lastPacket = 0}) async { - final hash = hashSha(data); - final payload = encryptSymmetric(data, vaultKey); - - final json = await postAuthorizedJSON("/account/friends/add", { - "hash": hash, - "payload": payload, - "receive_date": encryptDate(DateTime.fromMillisecondsSinceEpoch(lastPacket)), - "send_date": encryptDate(DateTime.fromMillisecondsSinceEpoch(0)), - }); - - if (!json["success"]) { - if (errorPopup) { - showErrorPopup("error", json["error"]); - } - return null; - } - - return json["id"]; - } - - /// Remove friend from vault (returns true if successful) - static Future remove(String id, {errorPopup = false}) async { - final json = await postAuthorizedJSON("/account/friends/remove", { - "id": id, - }); - - return json["success"] as bool; - } - - /// Encrypt a date with server-side information - static String encryptDate(DateTime time) { - return ServerStoredInfo(time.millisecondsSinceEpoch.toString()).transform(); - } - - /// Decrypt a date with server-side information - static DateTime decryptDate(String text) { - final info = ServerStoredInfo.untransform(text); - return DateTime.fromMillisecondsSinceEpoch(int.parse(info.text)); - } - - /// Get the last date a new message was sent to the friend (for replay attack prevention) - static Future lastReceiveDate(String id) async { - final json = await postAuthorizedJSON("/account/friends/get_receive_date", { - "id": id, - }); - - if (!json["success"]) { - sendLog("COULDN'T GET THE RECEIVE DATE FOR $id: ${json["error"]}"); - return null; - } - - return decryptDate(json["date"]); - } - - /// Set a new receive date (for replay attack prevention) - static Future setReceiveDate(String id, DateTime received) async { - final json = await postAuthorizedJSON("/account/friends/update_receive_date", { - "id": id, - "date": encryptDate(received), - }); - - if (!json["success"]) { - sendLog("COULDN'T SAVE THE NEW RECEIVE DATE ${json["error"]}"); - return false; - } - - return true; - } -} - -/// Class for storing all keys for a friend -class KeyStorage { - late String profileKeyPacked; - String storedActionKey; - Uint8List publicKey; - Uint8List signatureKey; - - KeyStorage.empty() - : publicKey = Uint8List(0), - signatureKey = Uint8List(0), - profileKeyPacked = "unbreathable_was_here_but_2024", - storedActionKey = "unbreathable_was_here"; - KeyStorage(this.publicKey, this.signatureKey, SecureKey profileKey, this.storedActionKey) { - profileKeyPacked = packageSymmetricKey(profileKey); - unpackedProfileKey = profileKey; - } - KeyStorage.fromJson(Map json) - : publicKey = unpackagePublicKey(json["pub"]), - profileKeyPacked = json["pf"] ?? "", - signatureKey = unpackagePublicKey(json["sg"]), - storedActionKey = json["sa"] ?? ""; - - Map toJson() { - return {"pub": packagePublicKey(publicKey), "pf": profileKeyPacked, "sg": packagePublicKey(signatureKey), "sa": storedActionKey}; - } - - // Just so we don't break the API anywhere yk - SecureKey? unpackedProfileKey; - SecureKey get profileKey { - unpackedProfileKey ??= unpackageSymmetricKey(profileKeyPacked); - return unpackedProfileKey!; - } -} diff --git a/lib/controller/account/friends/requests_controller.dart b/lib/controller/account/friends/requests_controller.dart deleted file mode 100644 index caa77c8a..00000000 --- a/lib/controller/account/friends/requests_controller.dart +++ /dev/null @@ -1,288 +0,0 @@ -import 'dart:convert'; - -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/chat/stored_actions_listener.dart'; -import 'package:chat_interface/controller/account/unknown_controller.dart'; -import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/controller/current/steps/account_step.dart'; -import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/controller/current/tasks/friend_sync_task.dart'; -import 'package:chat_interface/controller/current/steps/stored_actions_step.dart'; -import 'package:chat_interface/controller/current/steps/key_step.dart'; -import 'package:chat_interface/pages/status/setup/instance_setup.dart'; -import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:chat_interface/util/popups.dart'; -import 'package:chat_interface/util/web.dart'; -import 'package:drift/drift.dart'; -import 'package:get/get.dart'; - -import 'friend_controller.dart'; - -class RequestController extends GetxController { - final requestsSent = {}.obs; - final requests = {}.obs; - - void reset() { - requests.clear(); - } - - Future loadRequests() async { - for (RequestData data in await db.request.select().get()) { - final address = LPHAddress.from(data.id); - if (data.self) { - requestsSent[address] = Request.fromEntity(data); - } else { - requests[address] == Request.fromEntity(data); - } - } - - return true; - } - - void addSentRequest(Request request) { - requestsSent[request.id] = request; - db.request.insertOnConflictUpdate(request.entity(true)); - } - - void addRequest(Request request) { - requests[request.id] = request; - db.request.insertOnConflictUpdate(request.entity(false)); - } - - Future deleteSentRequest(Request request, {removal = true}) async { - if (removal) { - requestsSent.remove(request.id); - } - await db.request.deleteWhere((tbl) => tbl.id.equals(request.id.encode())); - return true; - } - - Future deleteRequest(Request request, {removal = true}) async { - if (removal) { - requests.remove(request.id); - } - await db.request.deleteWhere((tbl) => tbl.id.equals(request.id.encode())); - return true; - } -} - -final requestsLoading = false.obs; - -/// Send a new friend request to an account by name -Future newFriendRequest(String name, Function(String) success) async { - requestsLoading.value = true; - - final controller = Get.find(); - if (name == controller.name.value || LPHAddress.from(name) == StatusController.ownAddress) { - showErrorPopup("request.self", "request.self.text".tr); - requestsLoading.value = false; - return; - } - - // Get the unknown account from the name parameter - UnknownAccount? profile; - if (name.contains("@")) { - // If it is an address, get it by using the id - profile = await Get.find().loadUnknownProfile(LPHAddress.from(name)); - } else { - // If it is a name, then get it from the current instance by name - profile = await Get.find().getUnknownProfileByName(name); - } - - // Check if the profile is valid - if (profile == null) { - showErrorPopup("request.not.found", "request.not.found.text".tr); - requestsLoading.value = false; - return; - } - - //* Prompt with confirm popup - var declined = true; - await showConfirmPopup(ConfirmWindow( - title: "request.confirm.title".tr, - text: "request.confirm.text".trParams({ - "username": "${profile.displayName} (${profile.name})", - }), - onConfirm: () async { - declined = false; - await sendFriendRequest(controller, profile!.name, profile.displayName, profile.id, profile.publicKey, profile.signatureKey, success); - }, - onDecline: () { - declined = true; - }, - )); - - requestsLoading.value = !declined; - return; -} - -/// Send a friend request to an account -Future sendFriendRequest( - StatusController controller, - String name, - String displayName, - LPHAddress address, - Uint8List publicKey, - Uint8List signatureKey, - Function(String) success, -) async { - if (friendsVaultRefreshing.value) { - requestsLoading.value = false; - return; - } - - // Encrypt friend request - sendLog("OWN STORED ACTION KEY: $storedActionKey"); - final payload = storedAction("fr_rq", { - "ad": StatusController.ownAddress.encode(), - "name": controller.name.value, - "dname": controller.displayName.value, - "s": encryptAsymmetricAuth(publicKey, asymmetricKeyPair.secretKey, name), - "pub": packagePublicKey(asymmetricKeyPair.publicKey), - "sg": packagePublicKey(signatureKeyPair.publicKey), - "pf": packageSymmetricKey(profileKey), - "sa": storedActionKey, - }); - - // Send stored action - final result = await sendStoredAction(address, publicKey, payload); - if (result != null) { - showErrorPopup("error", result); - requestsLoading.value = false; - return; - } - - // Accept friend request if there is one from the other user - final requestController = Get.find(); - final requestSent = requestController.requests[address]; - if (requestSent != null) { - final result = await Get.find().addFromRequest(requestSent); - if (result) { - await requestController.deleteRequest(requestSent); - } else { - showErrorPopup("error", "requests.error".tr); - } - success("request.accepted"); - } else { - // Save friend request in own vault - var request = Request(address, name, displayName, "", KeyStorage(publicKey, signatureKey, profileKey, ""), DateTime.now().millisecondsSinceEpoch); - final vaultId = await FriendsVault.store( - request.toStoredPayload(true), - errorPopup: true, - prefix: "request", - ); - - if (vaultId == null) { - requestsLoading.value = false; - return; - } - - // This had me in a mental breakdown, but then I ended up fixing it in 10 minutes LMFAO - request.vaultId = vaultId; - - RequestController requestController = Get.find(); - requestController.addSentRequest(request); - success("request.sent"); - } - - requestsLoading.value = false; - return; -} - -class Request { - final LPHAddress id; - String name; - String displayName; - String vaultId; - int updatedAt; - final KeyStorage keyStorage; - final loading = false.obs; - - Request(this.id, this.name, this.displayName, this.vaultId, this.keyStorage, this.updatedAt); - - /// Get a request from the database object - factory Request.fromEntity(RequestData data) { - return Request( - LPHAddress.from(data.id), - fromDbEncrypted(data.name), - fromDbEncrypted(data.displayName), - fromDbEncrypted(data.vaultId), - KeyStorage.fromJson(jsonDecode(fromDbEncrypted(data.keys))), - data.updatedAt.toInt(), - ); - } - - /// Get a request from a stored payload in the database - factory Request.fromStoredPayload(Map json, int updatedAt) { - return Request( - LPHAddress.from(json["id"]), - json["name"], - json["display_name"], - "", - KeyStorage.fromJson(json), - updatedAt, - ); - } - - // Convert to a payload for the friends vault (on the server) - String toStoredPayload(bool self) { - final reqPayload = { - "rq": true, - "id": id.encode(), - "self": self, - "name": name, - "display_name": displayName, - }; - reqPayload.addAll(keyStorage.toJson()); - - return jsonEncode(reqPayload); - } - - /// Convert a request object to the equivalent database object - RequestData entity(bool self) => RequestData( - id: id.encode(), - name: dbEncrypted(name), - displayName: dbEncrypted(displayName), - vaultId: dbEncrypted(vaultId), - keys: dbEncrypted(jsonEncode(keyStorage.toJson())), - self: self, - updatedAt: BigInt.from(updatedAt), - ); - - /// Convert a request to a friend (for when the request is accepted) - Friend get friend => Friend(id, name, displayName, vaultId, keyStorage, updatedAt); - - // Accept friend request - void accept(Function(String) success) { - // Send a request to the same guy (this thing will detect that the request already exist and then add him, this avoids code duplication) - sendFriendRequest(Get.find(), name, displayName, id, keyStorage.publicKey, keyStorage.signatureKey, (msg) async { - success(msg); - }); - } - - // Decline friend request - Future ignore() async { - // Delete from friends vault - await FriendsVault.remove(vaultId); - - // Delete from requests - final requestController = Get.find(); - await requestController.deleteRequest(this); - } - - // Cancel friend request (only for sent requests) - Future cancel() async { - // Delete from friends vault - await FriendsVault.remove(vaultId); - - // Delete from sent requests - final requestController = Get.find(); - await requestController.deleteSentRequest(this); - } - - void save(bool self) { - db.request.insertOnConflictUpdate(entity(self)); - } -} diff --git a/lib/controller/account/requests_controller.dart b/lib/controller/account/requests_controller.dart new file mode 100644 index 00000000..bf4a8870 --- /dev/null +++ b/lib/controller/account/requests_controller.dart @@ -0,0 +1,215 @@ +import 'dart:convert'; + +import 'package:chat_interface/services/chat/requests_service.dart'; +import 'package:chat_interface/services/chat/unknown_service.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; +import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:drift/drift.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +import 'friend_controller.dart'; + +class RequestController { + static final requestsLoading = signal(false); + static final requestsSent = mapSignal({}); + static final requests = mapSignal({}); + + static void reset() { + requests.clear(); + } + + static Future loadRequests() async { + for (RequestData data in await db.request.select().get()) { + final address = LPHAddress.from(data.id); + if (data.self) { + requestsSent[address] = Request.fromEntity(data); + } else { + requests[address] == Request.fromEntity(data); + } + } + + return true; + } + + static void addSentRequestOrUpdate(Request request) { + if (requestsSent[request.id] != null) { + requestsSent[request.id]!.copyFrom(request); + } else { + requestsSent[request.id] = request; + } + } + + static void addRequestOrUpdate(Request request) { + if (requests[request.id] != null) { + requests[request.id]!.copyFrom(request); + } else { + requests[request.id] = request; + } + } + + static Future deleteSentRequest(Request request, {removal = true}) async { + if (removal) { + requestsSent.remove(request.id); + } + await db.request.deleteWhere((tbl) => tbl.id.equals(request.id.encode())); + return true; + } + + static Future deleteRequest(Request request, {removal = true}) async { + if (removal) { + requests.remove(request.id); + } + await db.request.deleteWhere((tbl) => tbl.id.equals(request.id.encode())); + return true; + } +} + +/// Send a new friend request to an account by name +Future newFriendRequest(String name, Function(String) success) async { + RequestController.requestsLoading.value = true; + + if (name == StatusController.name.value || LPHAddress.from(name) == StatusController.ownAddress) { + showErrorPopup("request.self", "request.self.text".tr); + RequestController.requestsLoading.value = false; + return; + } + + // Get the unknown account from the name parameter + UnknownAccount? profile; + if (name.contains("@")) { + // If it is an address, get it by using the id + profile = await UnknownService.loadUnknownProfile(LPHAddress.from(name)); + } else { + // If it is a name, then get it from the current instance by name + profile = await UnknownService.getUnknownProfileByName(name); + } + + // Check if the profile is valid + if (profile == null) { + showErrorPopup("request.not.found", "request.not.found.text".tr); + RequestController.requestsLoading.value = false; + return; + } + + // Make sure the person is not already a friend + if (FriendController.friends.keys.any((a) => a == profile!.id)) { + showErrorPopup("request.friend.exists", "request.friend.exists.text".tr); + RequestController.requestsLoading.value = false; + return; + } + + // Ask the user if they really want to send the friend requests (mostly cause of security concerns) + await showConfirmPopup( + ConfirmWindow( + title: "request.confirm.title".tr, + text: "request.confirm.text".trParams({"username": "${profile.displayName} (${profile.name})"}), + onConfirm: () async { + await RequestsService.sendOrAcceptFriendRequest(profile!); + }, + onDecline: () {}, + ), + ); + + RequestController.requestsLoading.value = false; + return; +} + +class Request { + LPHAddress id; + String name; + String displayName; + String vaultId; + int updatedAt; + KeyStorage keyStorage; + final loading = signal(false); + + Request(this.id, this.name, this.displayName, this.vaultId, this.keyStorage, this.updatedAt); + + /// Get a request from the database object. + factory Request.fromEntity(RequestData data) { + return Request( + LPHAddress.from(data.id), + fromDbEncrypted(data.name), + fromDbEncrypted(data.displayName), + fromDbEncrypted(data.vaultId), + KeyStorage.fromJson(jsonDecode(fromDbEncrypted(data.keys))), + data.updatedAt.toInt(), + ); + } + + /// Get a request from a stored payload in the database. + factory Request.fromStoredPayload(String id, int updatedAt, Map json) { + return Request( + LPHAddress.from(json["id"]), + json["name"], + json["display_name"], + "", + KeyStorage.fromJson(json), + updatedAt, + ); + } + + /// Convert to a payload for the friends vault (on the server). + String toStoredPayload(bool self) { + final reqPayload = { + "rq": true, + "id": id.encode(), + "self": self, + "name": name, + "display_name": displayName, + }; + reqPayload.addAll(keyStorage.toJson()); + + return jsonEncode(reqPayload); + } + + /// Convert the request to an unknown account (for accepting the friend request). + UnknownAccount toUnknownAccount() { + return UnknownAccount(id, name, displayName, keyStorage.signatureKey, keyStorage.publicKey); + } + + /// Convert a request object to the equivalent database object. + RequestData entity(bool self) => RequestData( + id: id.encode(), + name: dbEncrypted(name), + displayName: dbEncrypted(displayName), + vaultId: dbEncrypted(vaultId), + keys: dbEncrypted(jsonEncode(keyStorage.toJson())), + self: self, + updatedAt: BigInt.from(updatedAt), + ); + + /// Copy all data from another request into this one. + void copyFrom(Request request) { + id = request.id; + name = request.name; + displayName = request.displayName; + vaultId = request.vaultId; + updatedAt = request.updatedAt; + keyStorage = request.keyStorage; + } + + /// Convert a request to a friend (for when the request is accepted) + Friend get friend => Friend(id, name, displayName, vaultId, keyStorage, updatedAt); + + /// Accept the friend request. + /// + /// The first element is an error if there was one. + /// The second element is what happened if successfull (request.accepted, or sth else). + Future<(String?, String?)> accept() async { + // Send a request to the same guy (this thing will detect that the request already exist and then add him, this avoids code duplication) + return RequestsService.sendOrAcceptFriendRequest(toUnknownAccount()); + } + + /// Delete a friend request. + /// + /// Returns an error if there was one. + Future delete() async { + return FriendsVault.remove(vaultId); + } +} diff --git a/lib/controller/account/writing_controller.dart b/lib/controller/account/writing_controller.dart deleted file mode 100644 index c8b56b7b..00000000 --- a/lib/controller/account/writing_controller.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:get/get.dart'; - -class WritingController extends GetxController { - // Conversation: [Users] - final writing = >{}.obs; - - // User: Conversation - final writingUser = {}; - - void init(String id) { - if (writing[id] != null) return; - writing[id] = []; - } - - void add(String id, String userId) { - if (writingUser[userId] != null) { - remove(writingUser[userId]!, userId); - } - - if (writing[id] == null) { - writing[id] = [userId]; - } else { - writing[id] = [...writing[id]!, userId]; - } - - writingUser[userId] = id; - } - - void remove(String id, String userId) { - if (writing[id] == null) { - writing[id] = []; - } else { - writing[id]!.remove(userId); - writing[id] = [...writing[id]!]; - } - - writingUser.remove(userId); - } -} diff --git a/lib/controller/controller_manager.dart b/lib/controller/controller_manager.dart index f6571547..443a721e 100644 --- a/lib/controller/controller_manager.dart +++ b/lib/controller/controller_manager.dart @@ -1,52 +1,9 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/account/friends/requests_controller.dart'; -import 'package:chat_interface/controller/account/unknown_controller.dart'; -import 'package:chat_interface/controller/account/writing_controller.dart'; -import 'package:chat_interface/controller/conversation/attachment_controller.dart'; -import 'package:chat_interface/controller/conversation/message_search_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_message_controller.dart'; -import 'package:chat_interface/controller/spaces/warp_controller.dart'; -import 'package:chat_interface/controller/conversation/zap_share_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; -import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; -import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/database/trusted_links.dart'; import 'package:chat_interface/pages/settings/data/settings_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; -import 'package:chat_interface/theme/components/transitions/transition_controller.dart'; -import 'package:chat_interface/theme/theme_manager.dart'; -import 'package:get/get.dart'; void initializeControllers() { - // Conversation controls - Get.put(MessageController()); - Get.put(UnknownController()); - Get.put(AttachmentController()); - Get.put(ConversationController()); - Get.put(MessageSearchController()); - - // Account controls - Get.put(RequestController()); - Get.put(FriendController()); - Get.put(WritingController()); - Get.put(ZapShareController()); - - // App controls - Get.put(ConnectionController()); - Get.put(StatusController()); - Get.put(SettingController()); - Get.put(ThemeManager()); - Get.put(TransitionController()); - - // Space controls - Get.put(SpacesController()); - Get.put(SpaceMemberController()); - Get.put(TabletopController()); - Get.put(SpacesMessageController()); - Get.put(WarpController()); - + ConnectionController.init(); + SettingController.init(); TrustedLinkHelper.init(); } diff --git a/lib/controller/conversation/attachment_controller.dart b/lib/controller/conversation/attachment_controller.dart index 96b37313..5ed54014 100644 --- a/lib/controller/conversation/attachment_controller.dart +++ b/lib/controller/conversation/attachment_controller.dart @@ -4,8 +4,8 @@ import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/database/trusted_links.dart'; @@ -23,14 +23,17 @@ import 'package:dio/dio.dart' as dio_rs; import 'package:liphium_bridge/liphium_bridge.dart'; import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:sodium_libs/sodium_libs.dart'; import 'package:path/path.dart' as path; -class AttachmentController extends GetxController { - final attachments = {}; +class AttachmentController { + static final attachments = mapSignal({}); - // Upload a file - Future uploadFile( + /// Upload a file. + /// + /// Returns a response. + static Future uploadFile( UploadData data, StorageType type, String tag, { @@ -40,35 +43,34 @@ class AttachmentController extends GetxController { String? fileName, }) async { // Check if there is a connection before doing this - if (!Get.find().connected.value) { + if (!ConnectionController.connected.value) { if (popups) { showErrorPopup("error", "error.no_connection".tr); } return FileUploadResponse("error.no_connection".tr, null); } + // Encrypt the file bytes ??= await data.file.readAsBytes(); final key = randomSymmetricKey(); final encrypted = encryptSymmetricBytes(bytes, key); final name = encryptSymmetric(fileName ?? path.basename(data.file.path), key); - // Upload file + // Create the data we send to the server final formData = dio_rs.FormData.fromMap({ "file": dio_rs.MultipartFile.fromBytes(encrypted, filename: name), "name": name, "tag": tag, "key": encryptAsymmetricAnonymous(asymmetricKeyPair.publicKey, packageSymmetricKey(key)), - "extension": path.extension(data.file.path).substring(1) + "extension": path.extension(data.file.path).substring(1), }); + // Upload the file to the server final res = await dio.post( ownServer("/account/files/upload"), data: formData, options: dio_rs.Options( - headers: { - "Content-Type": "multipart/form-data", - "Authorization": "Bearer $sessionToken", - }, + headers: {"Content-Type": "multipart/form-data", "Authorization": "Bearer $sessionToken"}, validateStatus: (status) => true, ), onSendProgress: (count, total) { @@ -76,11 +78,9 @@ class AttachmentController extends GetxController { sendLog(data.progress.value); }, ); - if (res.statusCode != 200) { return FileUploadResponse("server.error.code".trParams({"code": res.statusCode.toString()}), null); } - final json = res.data; if (!json["success"]) { return FileUploadResponse(json["error"], null); @@ -103,15 +103,14 @@ class AttachmentController extends GetxController { url: json["url"], key: key, ); - sendLog("UPLOADED ATTACHMENT: ${container.id}"); container.downloaded.value = FileSettings.isMediaFile(json["id"]); attachments[container.id] = container; return FileUploadResponse("success", container); } - /// Download an attachment - Future downloadAttachment( + /// Download an attachment. + static Future downloadAttachment( AttachmentContainer container, { bool retry = false, bool popups = true, @@ -144,12 +143,10 @@ class AttachmentController extends GetxController { } sendLog("Downloading ${container.name}..."); - final maxSize = Get.find().settings[FileSettings.maxFileSize]!.getValue(); + final maxSize = SettingController.settings[FileSettings.maxFileSize]!.getValue(); // Check the file size to make sure it isn't over the limit - final json = await postAddress(container.url, "/account/file_info/info", { - "id": container.id, - }); + final json = await postAddress(container.url, "/account/file_info/info", {"id": container.id}); if (!json["success"]) { if (popups) { @@ -181,10 +178,7 @@ class AttachmentController extends GetxController { // Download and show progress final res = await dio.get( serverPath(container.url, "/account/files_unencrypted/download/${container.id}").toString(), - options: dio_rs.Options( - responseType: dio_rs.ResponseType.bytes, - validateStatus: (status) => true, - ), + options: dio_rs.Options(responseType: dio_rs.ResponseType.bytes, validateStatus: (status) => true), onReceiveProgress: (count, total) { container.percentage.value = count / total; }, @@ -219,14 +213,14 @@ class AttachmentController extends GetxController { } /// Delete a file - Future deleteFile(AttachmentContainer container, {popup = false}) async { + static Future deleteFile(AttachmentContainer container, {popup = false}) async { return await deleteFileFromPath(container.id, container.file, popup: popup); } /// Delete a file based on a path and an id - Future deleteFileFromPath(String id, XFile? file, {popup = false}) async { + static Future deleteFileFromPath(String id, XFile? file, {popup = false}) async { // Check if there is a connection before doing this - if (!Get.find().connected.value) { + if (!ConnectionController.connected.value) { if (popup) { showErrorPopup("error", "error.no_connection".tr); } @@ -239,9 +233,7 @@ class AttachmentController extends GetxController { attachments.remove(id); // Delete from server - final json = await postAuthorizedJSON("/account/files/delete", { - "id": id, - }); + final json = await postAuthorizedJSON("/account/files/delete", {"id": id}); if (!json["success"]) { if (popup) { showErrorPopup("error", json["error"]); @@ -254,11 +246,11 @@ class AttachmentController extends GetxController { } /// Clean the cache until the size is below the max cache size - Future cleanUpCache() async { + static Future cleanUpCache() async { // Move into isolate in the future? - final cacheType = Get.find().settings[FileSettings.fileCacheType]!.getValue(); + final cacheType = SettingController.settings[FileSettings.fileCacheType]!.getValue(); if (cacheType == 0) return; - final maxSize = Get.find().settings[FileSettings.maxCacheSize]!.getValue() * 1000 * 1000; // Convert to bytes + final maxSize = SettingController.settings[FileSettings.maxCacheSize]!.getValue() * 1000 * 1000; // Convert to bytes final dir = Directory(getFilePathForType(StorageType.temporary)); final files = await dir.list().toList(); var cacheSize = files.fold(0, (previousValue, element) => previousValue + element.statSync().size); @@ -277,7 +269,7 @@ class AttachmentController extends GetxController { } // Delete all files from the device - Future deleteAllFiles() async { + static Future deleteAllFiles() async { var dir = XDirectory(_pathTemporary); await dir.delete(recursive: true); dir = XDirectory(_pathCache); @@ -360,8 +352,23 @@ class AttachmentController extends GetxController { return null; } + /// Get an attachment container from a string. + /// + /// Also handles it when the container is a remote image. + static Future fromString(String data) async { + // Check if the container is a remote container + if (data.isURL) { + return AttachmentContainer.remoteImage(data); + } + + // Decode the attachment container like regular + final json = jsonDecode(data); + final type = await AttachmentController.checkLocations(json["i"], StorageType.temporary); + return AttachmentController.fromJson(type, json); + } + /// Get an attachment container from json - AttachmentContainer fromJson(StorageType type, Map json, [Sodium? sodium]) { + static AttachmentContainer fromJson(StorageType type, Map json, [Sodium? sodium]) { var container = attachments[json["i"]]; if (container != null) { return container; @@ -423,11 +430,11 @@ class AttachmentContainer { String get name => fileName ?? id; // Download status - final downloading = false.obs; - final downloaded = false.obs; - final error = false.obs; - final unsafeLocation = false.obs; - final percentage = 0.0.obs; + final downloading = signal(false); + final downloaded = signal(false); + final error = signal(false); + final unsafeLocation = signal(false); + final percentage = signal(0.0); void errorHappened(bool unsafe) { error.value = true; @@ -506,14 +513,7 @@ class AttachmentContainer { } AttachmentContainer.remoteImage(String url) - : this( - storageType: StorageType.cache, - id: "", - fileName: "", - size: 0, - url: url, - key: null, - ); + : this(storageType: StorageType.cache, id: "", fileName: "", size: 0, url: url, key: null); String toAttachment() { switch (attachmentType) { diff --git a/lib/controller/conversation/conversation_actions.dart b/lib/controller/conversation/conversation_actions.dart deleted file mode 100644 index 996c20b5..00000000 --- a/lib/controller/conversation/conversation_actions.dart +++ /dev/null @@ -1,153 +0,0 @@ -part of 'conversation_controller.dart'; - -class MemberContainer { - late final LPHAddress id; - - MemberContainer(this.id); - MemberContainer.fromJson(Map json) : id = LPHAddress.from(json["id"]); - - MemberContainer.decrypt(String cipherText, SecureKey key) { - final json = jsonDecode(decryptSymmetric(cipherText, key)); - id = LPHAddress.from(json["id"]); - } - String encrypted(SecureKey key) => encryptSymmetric(jsonEncode({"id": id.encode()}), key); -} - -class ConversationToken { - final LPHAddress id; - final String token; - - ConversationToken(this.id, this.token); - ConversationToken.fromJson(Map json) - : id = LPHAddress.from(json["id"]), - token = json["token"]; - - String toJson() => jsonEncode(toMap()); - Map toMap() => {"id": id.encode(), "token": token}; -} - -class ConversationContainer { - late final String name; - - ConversationContainer(this.name); - ConversationContainer.fromJson(Map json) : name = json["name"]; - - ConversationContainer.decrypt(String cipherText, SecureKey key) { - final json = jsonDecode(decryptSymmetric(cipherText, key)); - name = json["name"]; - } - String encrypted(SecureKey key) => encryptSymmetric(jsonEncode({"name": name}), key); - - Map toJson() => {"name": name}; -} - -const directMessagePrefix = "DM_"; - -// Wrapper for consistent DM and Group conversation handling -Future openDirectMessage(Friend friend) async { - final conversation = Get.find().conversations.values.firstWhere( - (element) => element.members.length == 2 && element.members.values.any((element) => element.address == friend.id), - orElse: () => Conversation(LPHAddress.error(), "", model.ConversationType.directMessage, ConversationToken(LPHAddress.error(), ""), - ConversationContainer(""), "", 0, 0), - ); - if (!conversation.id.isError()) { - await Get.find().selectConversation(conversation); - return true; - } - - return _openConversation([friend], directMessagePrefix + friend.id.id); -} - -Future openGroupConversation(List friends, String name) { - return _openConversation(friends, name); -} - -// Open conversation with a group of friends -Future _openConversation(List friends, String name) async { - if (Get.find().conversations.length >= specialConstants["max_conversation_amount"]!) { - showErrorPopup("conversations.error".tr, "conversations.amount".trParams({"amount": specialConstants["max_conversation_amount"].toString()})); - return false; - } - - // Prepare the conversation - final conversationKey = randomSymmetricKey(); - final ownMemberContainer = MemberContainer(StatusController.ownAddress).encrypted(conversationKey); - final memberContainers = {}; - for (final friend in friends) { - final container = MemberContainer(friend.id); - memberContainers[friend.id] = container.encrypted(conversationKey); - } - final conversationContainer = ConversationContainer(name); - final encryptedData = conversationContainer.encrypted(conversationKey); - - if (name.length > specialConstants[Constants.specialConstantMaxConversationNameLength]!) { - showErrorPopup("conversations.error".tr, - "conversations.name.length".trParams({"length": specialConstants[Constants.specialConstantMaxConversationNameLength].toString()})); - return false; - } - - if (friends.length > specialConstants[Constants.specialConstantMaxConversationMembers]!) { - showErrorPopup("conversations.error".tr, - "conversations.members.size".trParams({"size": specialConstants[Constants.specialConstantMaxConversationMembers].toString()})); - return false; - } - - // Create the conversation - final body = await postNodeJSON("/conversations/open", { - "accountData": ownMemberContainer, - "members": memberContainers.values.toList(), - "data": encryptedData, - }); - if (!body["success"]) { - showErrorPopup("error".tr, "error.unknown".tr); - return false; - } - - //* Send the stuff to all other members - final conversationController = Get.find(); - - final packagedKey = packageSymmetricKey(conversationKey); - final convId = LPHAddress.from(body["conversation"]); - final conversation = Conversation(convId, "", model.ConversationType.values[body["type"]], ConversationToken.fromJson(body["admin_token"]), - conversationContainer, packagedKey, 0, DateTime.now().millisecondsSinceEpoch); - final members = []; - for (var friend in friends) { - final token = ConversationToken.fromJson(body["tokens"][hashSha(memberContainers[friend.id]!)]); - await sendAuthenticatedStoredAction(friend, _conversationPayload(convId, token, packagedKey, friend)); - members.add(Member(token.id, friend.id, MemberRole.user)); - } - - // Add the new conversation and subscribe - await conversationController.addCreated(conversation, members, admin: Member(conversation.token.id, StatusController.ownAddress, MemberRole.admin)); - subscribeToConversation(conversation.token, deletions: false); - - return true; -} - -/// Add a friend to a conversation by generating a new token for them and then -/// sending it to them by using a authenticated stored action. -Future addToConversation(Conversation conv, Friend friend) async { - // Generate a new conversation token for the friend - final json = await postNodeJSON("/conversations/generate_token", { - "token": conv.token.toMap(), - "data": MemberContainer(friend.id).encrypted(conv.key), - }); - - if (!json["success"]) { - return false; - } - - final result = await sendAuthenticatedStoredAction( - friend, _conversationPayload(conv.id, ConversationToken.fromJson(json), packageSymmetricKey(conv.key), friend)); - return result; -} - -Map _conversationPayload(LPHAddress id, ConversationToken token, String packagedKey, Friend friend) { - final signature = signMessage(signatureKeyPair.secretKey, "$id${friend.id}"); - return authenticatedStoredAction("conv", { - "id": id.encode(), - "sg": signature, - "token": token.toJson(), - "key": packagedKey, - }); -} diff --git a/lib/controller/conversation/conversation_controller.dart b/lib/controller/conversation/conversation_controller.dart index 06bc2949..4adee53f 100644 --- a/lib/controller/conversation/conversation_controller.dart +++ b/lib/controller/conversation/conversation_controller.dart @@ -1,172 +1,122 @@ +import 'dart:async'; import 'dart:convert'; -import 'package:chat_interface/connection/encryption/hash.dart'; -import 'package:chat_interface/connection/encryption/signatures.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/chat/setup_listener.dart'; -import 'package:chat_interface/connection/chat/stored_actions_listener.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; +import 'package:chat_interface/services/chat/conversation_member.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/database/database_entities.dart' as model; import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/controller/current/tasks/vault_sync_task.dart'; -import 'package:chat_interface/controller/current/steps/key_step.dart'; import 'package:chat_interface/pages/status/setup/instance_setup.dart'; -import 'package:chat_interface/util/constants.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/web.dart'; -import 'package:drift/drift.dart' as drift; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:sodium_libs/sodium_libs.dart'; -import 'member_controller.dart'; - -part 'conversation_actions.dart'; - -class ConversationController extends GetxController { - final loaded = false.obs; - final order = [].obs; // List of conversation IDs in order of last updated - final conversations = {}; +class ConversationController { + static final loaded = signal(false); + static final order = listSignal([]); // List of conversation IDs in order of last updated + static final conversations = mapSignal({}); + static final notificationMap = mapSignal({}); int newConvs = 0; - /// Add a conversation to the cache - Future add(Conversation conversation, {loadMembers = true}) async { - // Load members from the database - if (conversation.members.isEmpty && loadMembers) { - final members = await (db.select(db.member)..where((tbl) => tbl.conversationId.equals(conversation.id.encode()))).get(); - - for (var member in members) { - conversation.addMember(Member.fromData(member)); - } - } - - // Insert into cache - _insertToOrder(conversation.id); - conversations[conversation.id] = conversation; - - return true; - } - - /// Add a new conversation and refresh members (also subscribes) - Future addFromVault(Conversation conversation) async { - // Insert it into cache - await add(conversation, loadMembers: false); - - // Insert into database - conversation.save(saveMembers: false); - - // Subscribe to conversation - subscribeToConversation(conversation.token); - - return true; + /// Add a conversation to the cache. + static void add(Conversation conversation) { + batch(() { + conversations[conversation.id] = conversation; + _insertToOrder(conversation.id); + }); } - /// Add a conversation to the cache and local database (after created) - Future addCreated(Conversation conversation, List members, {Member? admin}) async { - conversations[conversation.id] = conversation; + /// Re-evaluate the order of [conversation] in the sidebar. + static void reorder(Conversation conversation) { _insertToOrder(conversation.id); - - for (var member in members) { - conversation.addMember(member); - } - if (admin != null) { - conversation.addMember(admin); - } - - // Add to vault - final vaultId = await addToVault(Constants.vaultConversationTag, conversation.toJson()); - if (vaultId == null) { - // TODO: refresh the vault or something - sendLog("COULDNT STORE IN VAULT; SOMETHING WENT WRONG"); - return false; - } - conversation.vaultId = vaultId; - sendLog("STORED IN VAULT: $vaultId"); - - // Store in database - await db.conversation.insertOnConflictUpdate(conversation.entity); - for (var member in conversation.members.values) { - await db.member.insertOnConflictUpdate(member.toData(conversation.id)); - } - - return true; } - void updateMessageRead(LPHAddress conversation, {bool increment = true, required int messageSendTime}) { - (db.conversation.update()..where((tbl) => tbl.id.equals(conversation.encode()))) - .write(ConversationCompanion(updatedAt: drift.Value(BigInt.from(DateTime.now().millisecondsSinceEpoch)))); - - // Swap in the map - _insertToOrder(conversation); - conversations[conversation]!.updatedAt.value = DateTime.now().millisecondsSinceEpoch; - if (increment && conversations[conversation]!.readAt.value < messageSendTime) { - conversations[conversation]!.notificationCount.value += 1; - } + /// Update the notification count of a conversation in the UI. + static void updateNotificationCount( + LPHAddress conversation, + int notificationCount, { + String extra = "", + int? messageSendTime, + }) { + notificationMap[ConversationService.withExtra(conversation.encode(), extra)] = notificationCount; } /// Called when a subscription is finished to make sure conversations are properly sorted and up to date. /// /// Called later for all conversations from other servers since they are streamed in after. - Future finishedLoading( + static Future finishedLoading( String server, Map conversationInfo, List deleted, bool error, ) async { - // Sort the conversations - order.sort((a, b) => conversations[b]!.updatedAt.value.compareTo(conversations[a]!.updatedAt.value)); - // Delete all the conversations that should be deleted - var toRemove = []; - final controller = Get.find(); - for (var conversation in controller.conversations.values) { + for (var conversation in conversations.values) { if (deleted.contains(conversation.token.id.encode())) { - toRemove.add(conversation.id); + unawaited( + ConversationService.delete(conversation.id, vaultId: conversation.vaultId, token: conversation.token), + ); } } - for (var key in toRemove) { - sendLog("deleting $key"); - await controller.conversations[key]!.delete(popup: false); - } - - // Update all the conversations - for (var conversation in conversations.values) { - if (!isSameServer(conversation.id.server, server)) { - continue; - } - - // Get conversation info - final info = (conversationInfo[conversation.id.encode()] ?? {}) as Map; - final version = (info["v"] ?? 0) as int; - conversation.notificationCount.value = (info["n"] ?? 0) as int; - conversation.readAt.value = (info["r"] ?? 0) as int; - // Set an error if there is one - if (error) { - conversation.error.value = "other.server.error".tr; + // Start a new batch to modify all the state at once + batch(() { + // Update all the conversations + for (var conversation in conversations.values) { + if (!isSameServer(conversation.id.server, server)) { + continue; + } + + // Get conversation info + final info = (conversationInfo[conversation.id.encode()] ?? {}) as Map; + final version = (info["v"] ?? 0) as int; + + // Handle the new reads + conversation.reads = ConversationReads.fromContainer(info["r"] ?? ""); + unawaited(ConversationService.evaluateNotificationCount(conversation)); + + // Set an error if there is one + if (error) { + conversation.error.value = "other.server.error".tr; + } + + // Check if the current version of the conversation is up to date + if (conversation.lastVersion != version) { + unawaited(ConversationService.fetchNewestVersion(conversation)); + } } - // Check if the current version of the conversation is up to date - sendLog("version ${conversation.id} client: ${conversation.lastVersion}, server: $version"); - if (conversation.lastVersion != version) { - sendLog("conversation version updated"); - await conversation.fetchData(); - } - } - - loaded.value = true; + loaded.value = true; + }); } - void _insertToOrder(LPHAddress id) { - if (order.contains(id)) { + /// Insert a conversation into the ordered list of conversations (performance could be improved using binary search). + static void _insertToOrder(LPHAddress id) { + batch(() { + // Remove it from the order order.remove(id); - } - order.insert(0, id); + + // Dirty insert the conversation + final updatedAt = conversations[id]!.updatedAt; + var index = 0; + for (var id in order) { + if (updatedAt > conversations[id]!.updatedAt) { + break; + } + index++; + } + order.insert(index, id); + }); } - void removeConversation(LPHAddress id) { + /// Remove a conversation from the cache. + static void removeConversation(LPHAddress id) { conversations.remove(id); order.remove(id); } @@ -179,11 +129,11 @@ class Conversation { final ConversationToken token; ConversationContainer container; int lastVersion; - final updatedAt = 0.obs; - final readAt = 0.obs; - final notificationCount = 0.obs; - final containerSub = ConversationContainer("").obs; // Data subscription - final error = Rx(null); + int updatedAt = 0; + ConversationReads reads = ConversationReads.fromContainer(""); + final notificationCount = signal(0); + final containerSub = signal(ConversationContainer("")); // Data subscription + final error = signal(null); String packedKey; SecureKey? _cachedKey; @@ -192,35 +142,46 @@ class Conversation { return _cachedKey!; } - final membersLoading = false.obs; - final members = {}.obs; // Token ID -> Member - - Conversation(this.id, this.vaultId, this.type, this.token, this.container, this.packedKey, this.lastVersion, int updatedAt) { + final membersLoading = signal(false); + final members = mapSignal({}); // Token ID -> Member + + Conversation( + this.id, + this.vaultId, + this.type, + this.token, + this.container, + this.packedKey, + this.lastVersion, + this.updatedAt, + this.reads, + ) { containerSub.value = container; - this.updatedAt.value = updatedAt; } Conversation.fromJson(Map json, String vaultId) - : this( - LPHAddress.from(json["id"]), - vaultId, - model.ConversationType.values[json["type"]], - ConversationToken.fromJson(json["token"]), - ConversationContainer.fromJson(json["data"]), - json["key"], - json["update"] ?? DateTime.now().millisecondsSinceEpoch, - 0, // This shouldn't matter, just makes sure the data is fetched - ); + : this( + LPHAddress.from(json["id"]), + vaultId, + model.ConversationType.values[json["type"]], + ConversationToken.fromJson(json["token"]), + ConversationContainer.fromJson(json["data"]), + json["key"], + 0, // This shouldn't matter, just makes sure the data is fetched + 0, + ConversationReads.fromContainer(""), + ); Conversation.fromData(ConversationData data) - : this( - LPHAddress.from(data.id), - fromDbEncrypted(data.vaultId), - data.type, - ConversationToken.fromJson(jsonDecode(fromDbEncrypted(data.token))), - ConversationContainer.fromJson(jsonDecode(fromDbEncrypted(data.data))), - fromDbEncrypted(data.key), - data.lastVersion.toInt(), - data.updatedAt.toInt(), - ); + : this( + LPHAddress.from(data.id), + fromDbEncrypted(data.vaultId), + data.type, + ConversationToken.fromJson(jsonDecode(fromDbEncrypted(data.token))), + ConversationContainer.fromJson(jsonDecode(fromDbEncrypted(data.data))), + fromDbEncrypted(data.key), + data.lastVersion.toInt(), + data.updatedAt.toInt(), + ConversationReads.fromLocalContainer(data.reads), + ); /// Copy a conversation without the `key`. /// @@ -234,8 +195,9 @@ class Conversation { conversation.token, conversation.container, "", - conversation.updatedAt.value, conversation.lastVersion, + conversation.updatedAt, + conversation.reads, ); // Copy all the members @@ -248,40 +210,34 @@ class Conversation { members[member.tokenId] = member; } - bool get isGroup => type == model.ConversationType.group; + bool get isGroup => type == model.ConversationType.group || type == model.ConversationType.square; /// Only works for direct messages String get dmName { final member = members.values.firstWhere( (element) => element.address != StatusController.ownAddress, - orElse: () => Member( - LPHAddress.error(), - LPHAddress.error(), - MemberRole.user, - ), + orElse: () => Member(LPHAddress.error(), LPHAddress.error(), MemberRole.user), ); - return Get.find().friends[member.address]?.displayName.value ?? container.name; + return FriendController.friends[member.address]?.displayName.value ?? container.name; } /// Only works for direct messages Friend get otherMember { final member = members.values.firstWhere( (element) => element.address != StatusController.ownAddress, - orElse: () => Member( - LPHAddress.error(), - LPHAddress.error(), - MemberRole.user, - ), + orElse: () => Member(LPHAddress.error(), LPHAddress.error(), MemberRole.user), ); - return Get.find().friends[member.address] ?? Friend.unknown(LPHAddress("-", container.name)); + return FriendController.friends[member.address] ?? Friend.unknown(LPHAddress("-", container.name)); } /// Check if a conversation is broken (borked) bool get borked => !isGroup && - Get.find().friends[members.values - .firstWhere((element) => element.address != StatusController.ownAddress, - orElse: () => Member(LPHAddress.error(), LPHAddress.error(), MemberRole.user)) + FriendController.friends[members.values + .firstWhere( + (element) => element.address != StatusController.ownAddress, + orElse: () => Member(LPHAddress.error(), LPHAddress.error(), MemberRole.user), + ) .address] == null; @@ -291,112 +247,49 @@ class Conversation { vaultId: dbEncrypted(vaultId), type: type, data: dbEncrypted(jsonEncode(container.toJson())), - token: dbEncrypted(token.toJson()), + token: dbEncrypted(token.toJson(id)), key: dbEncrypted(packageSymmetricKey(key)), lastVersion: BigInt.from(lastVersion), - updatedAt: BigInt.from(updatedAt.value), - readAt: BigInt.from(readAt.value), + updatedAt: BigInt.from(updatedAt), + reads: reads.toLocalContainer(), ); } String toJson() => jsonEncode({ - "id": id.encode(), - "type": type.index, - "token": token.toMap(), - "key": packageSymmetricKey(key), - "update": updatedAt.value.toInt(), - "data": container.toJson(), - }); - - // Delete conversation from vault and database - Future delete({bool request = true, bool popup = true}) async { - final err = await removeFromVault(vaultId); - if (err != null) { - sendLog("Error deleting conversation from vault: $err"); - if (popup) showErrorPopup("error".tr, "error.not_delete_conversation".tr); + "id": id.encode(), + "type": type.index, + "token": token.toMap(id), + "key": packageSymmetricKey(key), + "data": container.toJson(), + }); + + /// Delete conversation from vault and database. + /// + /// Shows an error popup when there was an error. + Future delete({bool leaveRequest = true}) async { + // Check if the vault id has been synchronized yet + if (vaultId == "") { + showErrorPopup("error", "conversation.delete_error".tr); + sendLog("ERROR: Can't delete conversation yet: no vault id"); return; } - if (request) { - final json = await postNodeJSON("/conversations/leave", { - "token": token.toMap(), - }); - - if (!json["success"]) { - sendLog("Error deleting conversation from vault: ${json["error"]}"); - if (popup) showErrorPopup("error".tr, "error.not_delete_conversation".tr); - // Don't return here, should remove from the local vault regardless - } + // Delete the conversation + final error = await ConversationService.delete(id, vaultId: vaultId, token: leaveRequest ? token : null); + if (error != null) { + showErrorPopup("error", error); + sendLog("ERROR: Can't delete conversation: $error"); } - - await db.conversation.deleteWhere((tbl) => tbl.id.equals(id.encode())); - await db.member.deleteWhere((tbl) => tbl.conversationId.equals(id.encode())); - Get.find().unselectConversation(id: id); - Get.find().removeConversation(id); } - /// Save the entire conversation to the local database. - /// - /// By default members are also overwritten. Can be disabled by setting `saveMembers` to `false`. - void save({saveMembers = true}) { - db.conversation.insertOnConflictUpdate(entity); - if (saveMembers) { - for (var member in members.values) { - db.member.insertOnConflictUpdate(member.toData(id)); - } + IconData getIconForConversation() { + switch (type) { + case model.ConversationType.directMessage: + return Icons.person; + case model.ConversationType.group: + return Icons.people; + case model.ConversationType.square: + return Icons.public; } } - - /// Fetch all data about a conversation from the server and update it in the local database. - /// - /// Also compares the current version with the new version that was sent and doesn't refresh - /// in case it's not nessecary. Can be disabled by setting `refreshAnyway` to `false`. - Future fetchData() async { - if (membersLoading.value) { - return false; - } - - // Get the data from the server - membersLoading.value = true; - final json = await postNodeJSON("/conversations/data", { - "token": token.toMap(), - }); - - if (!json["success"]) { - sendLog("SOMETHING WENT WRONG KINDA WITH MEMBER FETCHING ${json["error"]}"); - // TODO: Add to some sort of error collection - return false; - } - - // Update to the latest version - sendLog("PULLED VERSION ${json["version"]}"); - lastVersion = json["version"]; - - // Update the container - container = ConversationContainer.decrypt(json["data"], key); - containerSub.value = container; - - // Update the members - final members = {}; - for (var memberData in json["members"]) { - sendLog(memberData); - final memberContainer = MemberContainer.decrypt(memberData["data"], key); - final address = LPHAddress.from(memberData["id"]); - members[address] = Member(address, memberContainer.id, MemberRole.fromValue(memberData["rank"])); - } - - // Load the members into the database - for (var currentMember in this.members.values) { - if (!members.containsKey(currentMember.tokenId)) { - await db.member.deleteWhere((tbl) => tbl.id.equals(currentMember.tokenId.encode())); - } - } - - // Set the members and save the conversation - this.members.value = members; - membersLoading.value = false; - save(); - - return true; - } } diff --git a/lib/controller/conversation/member_controller.dart b/lib/controller/conversation/member_controller.dart deleted file mode 100644 index 3394cee9..00000000 --- a/lib/controller/conversation/member_controller.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/pages/status/setup/instance_setup.dart'; -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:chat_interface/util/web.dart'; -import 'package:get/get.dart'; - -class MemberController extends GetxController { - final members = [].obs; - - Future loadConversation(RxBool loading, String id) async { - loading.value = true; - - final membersDb = await (db.select(db.member)..where((tbl) => tbl.conversationId.equals(id))).get(); - - members.clear(); - members.addAll(membersDb.map((e) => Member.fromData(e))); - } -} - -class Member { - final LPHAddress tokenId; // Token id - final LPHAddress address; // Account id - final MemberRole role; - - Member(this.tokenId, this.address, this.role); - Member.unknown(this.address) - : tokenId = LPHAddress.error(), - role = MemberRole.user; - Member.fromJson(Map json) - : tokenId = LPHAddress.from(json['id']), - address = LPHAddress.from(json['address']), - role = MemberRole.fromValue(json['role']); - - Member.fromData(MemberData data) - : this( - LPHAddress.from(data.id), - LPHAddress.from(fromDbEncrypted(data.accountId)), - MemberRole.fromValue(data.roleId), - ); - - MemberData toData(LPHAddress conversation) => MemberData( - id: tokenId.encode(), - accountId: dbEncrypted(address.encode()), - roleId: role.value, - conversationId: conversation.encode(), - ); - - Friend getFriend([FriendController? controller]) { - if (StatusController.ownAddress == address) return Friend.me(); - controller ??= Get.find(); - return controller.friends[address] ?? Friend.unknown(address); - } - - Future promote(LPHAddress conversationId) async { - final conversation = Get.find().conversations[conversationId]!; - final json = await postNodeJSON("/conversations/promote_token", { - "token": conversation.token.toMap(), - "data": tokenId.encode(), - }); - - if (!json["success"]) { - return false; - } - - return true; - } - - Future demote(LPHAddress conversationId) async { - final conversation = Get.find().conversations[conversationId]!; - final json = await postNodeJSON("/conversations/demote_token", { - "token": conversation.token.toMap(), - "data": tokenId.encode(), - }); - - if (!json["success"]) { - return false; - } - - return true; - } - - Future remove(LPHAddress conversationId) async { - final conversation = Get.find().conversations[conversationId]!; - final json = await postNodeJSON("/conversations/kick_member", { - "token": conversation.token.toMap(), - "data": tokenId.encode(), - }); - - if (!json["success"]) { - sendLog(json["error"]); - return false; - } - - return true; - } -} - -enum MemberRole { - admin(2), - moderator(1), - user(0); - - final int value; - - const MemberRole(this.value); - - bool lowerOrEqual(MemberRole role) { - return value <= role.value; - } - - bool higherOrEqual(MemberRole role) { - return value >= role.value; - } - - bool higherThan(MemberRole role) { - return value > role.value; - } - - bool lowerThan(MemberRole role) { - return value < role.value; - } - - static MemberRole fromValue(int value) { - switch (value) { - case 2: - return MemberRole.admin; - case 1: - return MemberRole.moderator; - case 0: - return MemberRole.user; - } - return MemberRole.user; - } -} diff --git a/lib/controller/conversation/message_controller.dart b/lib/controller/conversation/message_controller.dart index 4d106a14..1aae4ba7 100644 --- a/lib/controller/conversation/message_controller.dart +++ b/lib/controller/conversation/message_controller.dart @@ -1,517 +1,120 @@ import 'dart:async'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/chat/message_listener.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; +import 'package:chat_interface/pages/chat/components/conversations/conversation_members_bar.dart'; +import 'package:chat_interface/pages/settings/data/settings_controller.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; -import 'package:chat_interface/controller/spaces/ringing_manager.dart'; import 'package:chat_interface/controller/conversation/system_messages.dart'; -import 'package:chat_interface/controller/current/connection_controller.dart'; -import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/main.dart'; import 'package:chat_interface/pages/chat/messages_page.dart'; -import 'package:chat_interface/pages/status/setup/instance_setup.dart'; -import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:get/get.dart'; -import 'package:sodium_libs/sodium_libs.dart'; -import 'package:drift/drift.dart'; +import 'package:signals/signals_flutter.dart'; -enum OpenTabType { - conversation, - space, - townsquare; -} - -class MessageController extends GetxController { +class MessageController { // Constants - Message? hoveredMessage; - AttachmentContainer? hoveredAttachment; + static Message? hoveredMessage; + static AttachmentContainer? hoveredAttachment; static LPHAddress systemSender = LPHAddress("liphium.com", "6969"); - final showSearch = false.obs; - final hideSidebar = false.obs; - final loaded = false.obs; - final currentOpenType = OpenTabType.conversation.obs; - final currentProvider = Rx(null); - - void toggleSearchView() { - showSearch.toggle(); - if (Get.width <= 1200) { - if (showSearch.value) { - hideSidebar.value = true; - } else { - hideSidebar.value = false; - } - } - } - - void toggleSidebar() { - hideSidebar.toggle(); - if (Get.width <= 1200 && showSearch.value) { - showSearch.value = false; - } - } + static final loaded = signal(false); - /// Unselect a conversation (when id is set, the current conversation will only be closed if it has that id) - void unselectConversation({LPHAddress? id}) { - if (id != null && currentProvider.value?.conversation.id != id) { - return; + /// Open a conversation. + /// + /// Transitions to a new page on mobile. + /// Changes the tab in the sidebar in case on desktop. + static Future openConversation(Conversation conversation, {String extra = ""}) async { + final provider = ConversationMessageProvider(conversation, extra: extra); + + // Load the current position for messages + final read = conversation.reads.get(extra); + await provider.reloadAt(read + 1); + + if (ConversationController.notificationMap[ConversationService.withExtra(conversation.id.encode(), extra)] == 1) { + unawaited( + ConversationService.overwriteRead( + conversation, + provider.messages.values.first.createdAt.millisecondsSinceEpoch, + extra: extra, + ), + ); } - currentProvider.value?.messages.clear(); - currentProvider.value = null; - } - void openTab(OpenTabType type) { - currentOpenType.value = type; - if (type != OpenTabType.conversation) { - unselectConversation(); - } - } + // Show the messages once they are fully loaded + loaded.value = true; - Future selectConversation(Conversation conversation) async { - currentOpenType.value = OpenTabType.conversation; - loaded.value = false; - currentProvider.value = ConversationMessageProvider(conversation); + // Open page or provider (here to prevent flicker) if (isMobileMode()) { - unawaited(Get.to(MessagesPageMobile(provider: currentProvider.value!))); - } - if (conversation.notificationCount.value != 0) { - // Send new read state to the server - await overwriteRead(conversation); - } - - // Make sure the thing has some messages in it - await currentProvider.value!.loadNewMessagesTop(date: DateTime.now().millisecondsSinceEpoch); + // On mobile transition to the page + unawaited(Get.to(MessagesPageMobile(provider: provider))); + } else { + // Open the sidebar tab + SidebarController.openTab(ConversationSidebarTab(provider)); - loaded.value = true; + // Open the member sidebar in case desired + if (SettingController.settings[AppSettings.showGroupMembers]!.getValue() as bool && conversation.isGroup) { + SidebarController.setRightSidebar(ConversationMembersRightSidebar(conversation)); + } + } } - // Push read state to the server - Future overwriteRead(Conversation conversation) async { - // Send new read state to the server - final json = await postNodeJSON("/conversations/read", { - "token": conversation.token.toMap(), - }); - if (json["success"]) { - conversation.notificationCount.value = 0; - conversation.readAt.value = DateTime.now().millisecondsSinceEpoch; + /// Restore the right sidebar to how it was before another sidebar was opened. + /// + /// Returns the sidebar to the group members overview for example (in case opened). + static void restoreRightSidebar() { + if (SettingController.settings[AppSettings.showGroupMembers]!.getValue() as bool) { + final provider = SidebarController.getCurrentProvider(); + if (provider != null && provider.conversation.isGroup) { + SidebarController.setRightSidebar(ConversationMembersRightSidebar(provider.conversation)); + } else { + SidebarController.setRightSidebar(null); + } + } else { + SidebarController.setRightSidebar(null); } } - /// Store the message in the local database and in the cache (if the conversation is selected). + /// Add a message to the cache. /// - /// Also handles system messages. - Future storeMessage( + /// [simple] can be set to [true] in case you want to only add the message (no extra fancy stuff). + static Future addMessage( Message message, Conversation conversation, { + String extra = "", bool simple = false, (String, String)? part, }) async { - // Ignore certain things in case they are already done or not needed - if (!simple) { - // Update message read time for conversations (nessecary for notification count) - Get.find().updateMessageRead( - conversation.id, - increment: currentProvider.value?.conversation.id != conversation.id, - messageSendTime: message.createdAt.millisecondsSinceEpoch, - ); - - // Play a notification sound when a new message arrives - unawaited(RingingManager.playNotificationSound()); + // Make sure there even is a conversation + var tab = SidebarController.currentOpenTab.peek(); + if (tab is! ConversationSidebarTab) { + return true; // Success, nothing could be done (xd) } // Add message to message history if it's the selected one - if (currentProvider.value?.conversation.id == conversation.id) { - if (message.senderToken != currentProvider.value?.conversation.token.id && !simple) { - await overwriteRead(currentProvider.value!.conversation); + if (tab.provider.conversation.id == conversation.id && tab.provider.extra == extra) { + if (!simple) { + await ConversationService.overwriteRead( + tab.provider.conversation, + message.createdAt.millisecondsSinceEpoch, + extra: extra, + ); } // Check if it is a system message and if it should be rendered or not if (message.type == MessageType.system) { if (SystemMessages.messages[message.content]?.render == true) { - unawaited(currentProvider.value!.addMessageToBottom(message)); + unawaited(tab.provider.addMessageToBottom(message)); } } else { // Store normal type of message - if (currentProvider.value!.messages.isNotEmpty && currentProvider.value!.messages[0].id != message.id) { - unawaited(currentProvider.value!.addMessageToBottom(message)); - } else if (currentProvider.value!.messages.isEmpty) { - unawaited(currentProvider.value!.addMessageToBottom(message)); - } - } - } - - // Handle system messages - if (message.type == MessageType.system) { - if (currentProvider.value?.conversation.id == conversation.id) { - SystemMessages.messages[message.content]?.handle(message, currentProvider.value!); - } else { - SystemMessages.messages[message.content]?.handle(message, ConversationMessageProvider(conversation)); - } - - // Check if message should be stored - if (SystemMessages.messages[message.content]?.store ?? false) { - // Store message in local database - _storeInLocalDatabase(conversation, message, part: part); - } - } else { - // Store message in local database - _storeInLocalDatabase(conversation, message, part: part); - } - - // On call message type, ring using the message TODO: Reintroduce the ringtone in Spaces - /* - if (message.type == MessageType.call && message.senderAddress != StatusController.ownAddress) { - final container = SpaceConnectionContainer.fromJson(jsonDecode(message.content)); - RingingManager.startRinging(conversation, container); - } - */ - - return true; - } - - /// Store a message in the local database. - void _storeInLocalDatabase(Conversation conversation, Message message, {(String, String)? part}) { - db.into(db.message).insertOnConflictUpdate( - MessageData( - id: message.id, - content: part?.$1 ?? encryptSymmetric(message.toContentJson(), databaseKey), - senderToken: message.senderToken.encode(), - senderAddress: part?.$2 ?? encryptSymmetric(message.senderAddress.encode(), databaseKey), - createdAt: BigInt.from(message.createdAt.millisecondsSinceEpoch), - conversation: conversation.id.encode(), - edited: message.edited, - verified: message.verified.value, - ), - ); - } - - /// Store all of the messages in the list in the local database. - /// - /// This method doesn't play a sound because it's only used for synchronization. - Future storeMessages(List messages, Conversation conversation) async { - if (messages.isEmpty) { - return false; - } - - // Sort all the messages to prevent failing system messages - messages.sort( - (a, b) { - return a.createdAt.compareTo(b.createdAt); - }, - ); - - // Encrypt everything for local database storage - final copied = Conversation.copyWithoutKey(conversation); - final parts = await sodiumLib.runIsolated((sodium, keys, pairs) async { - final list = <(String, String)>[]; - for (var message in messages) { - list.add(( - encryptSymmetric(message.toContentJson(), keys[0], sodium), - encryptSymmetric(message.senderAddress.encode(), keys[0], sodium), - )); - } - - return list; - }, secureKeys: [databaseKey]); - - // Store all the messages in the local database - int index = 0; - for (var message in messages) { - await storeMessage( - message, - copied, - simple: true, - part: parts[index], - ); - index++; - } - - // Update message read time (to sort conversations properly) - Get.find().updateMessageRead( - conversation.id, - increment: currentProvider.value?.conversation.id != conversation.id, - messageSendTime: messages.last.createdAt.millisecondsSinceEpoch, - ); - - // Tell the server about the new read state in case the messages have been received properly - if (currentProvider.value != null && currentProvider.value?.conversation.token.id != messages.last.senderToken) { - await overwriteRead(currentProvider.value!.conversation); - } - - return true; - } -} - -/// A message provider that loads messages from a conversation. -class ConversationMessageProvider extends MessageProvider { - final Conversation conversation; - ConversationMessageProvider(this.conversation); - - @override - Future<(List?, bool)> loadMessagesBefore(int time) async { - // Load messages from the local database - final messageQuery = db.select(db.message) - ..where((tbl) => tbl.conversation.equals(conversation.id.encode())) - ..where((tbl) => tbl.createdAt.isSmallerThanValue(BigInt.from(time))) - ..orderBy([(u) => OrderingTerm.desc(u.createdAt)]) - ..limit(10); - final messages = await messageQuery.get(); - - // If there are no messages, check for them on the server - if (messages.isEmpty) { - // Check if the user is even connected to the server (to make sure offline retrieval works) - if (!Get.find().connected.value) { - // Act like the top has been reached - return (null, false); - } - - // Load messages from the server - final json = await postNodeJSON("/conversations/message/list_before", { - "token": conversation.token.toMap(), - "data": time, - }); - - // Check if there was an error - if (!json["success"]) { - conversation.error.value = json["error"]; - return (null, true); - } - - // Check if the top has been reached - if (json["messages"] == null || json["messages"].isEmpty) { - return (null, false); - } - // Unpack the messages in an isolate - final messages = await MessageListener.unpackMessagesInIsolate(conversation, json["messages"]); - - // Prepare messages for - await initAttachmentsForMessages(messages); - return (messages, false); - } - - // Process the messages in a seperate isolate - return (await _processMessages(messages), false); - } - - @override - Future<(List?, bool)> loadMessagesAfter(int time) async { - // Load messages from the local database - final messageQuery = db.select(db.message) - ..where((tbl) => tbl.conversation.equals(conversation.id.encode())) - ..where((tbl) => tbl.createdAt.isBiggerThanValue(BigInt.from(time))) - ..orderBy([(u) => OrderingTerm.asc(u.createdAt)]) - ..limit(10); - final messages = await messageQuery.get(); - - // If there are no messages, check if there are some on the server - if (messages.isEmpty) { - // Check if the user is even connected to the server (to make sure offline retrieval works) - if (!Get.find().connected.value) { - // Act like the bottom has been reached - return (null, false); - } - - // Load the messages from the server - final json = await postNodeJSON("/conversations/message/list_after", { - "token": conversation.token.toMap(), - "data": time, - }); - - // Check if there was an error - if (!json["success"]) { - conversation.error.value = json["error"]; - return (null, true); - } - - // Check if the bottom has been reached - if (json["messages"] == null || json["messages"].isEmpty) { - return (null, false); - } - - // Unpack the messages in an isolate - final messages = await MessageListener.unpackMessagesInIsolate(conversation, json["messages"]); - - // Prepare messages for - await initAttachmentsForMessages(messages); - return (messages, false); - } - - // Process the messages in a seperate isolate - return (await _processMessages(messages), false); - } - - @override - Future loadMessageFromServer(String id, {bool init = true}) async { - // Get the message from the local database - // Load messages from the local database - final messageQuery = db.select(db.message) - ..where((tbl) => tbl.conversation.equals(conversation.id.encode())) - ..where((tbl) => tbl.id.equals(id)) - ..limit(1); - final message = await messageQuery.getSingleOrNull(); - if (message == null) { - // Check if the user is even connected to the server (to make sure offline retrieval works) - if (!Get.find().connected.value) { - // Act like the message doesn't exist - return null; - } - - // Get the message from the server - final json = await postNodeJSON("/conversations/message/get", { - "token": conversation.token.toMap(), - "data": id, - }); - - // Check if there is an error - if (!json["success"]) { - sendLog("error fetching message $id: ${json["error"]}"); - return null; - } - - // Parse message and init attachments (if desired) - final message = await MessageListener.unpackMessageInIsolate(conversation, json["message"]); - if (init) { - await message.initAttachments(this); - } - - return message; - } - - // Process message as a new list and grab it from the list when finished - return (await _processMessages([message], init: init))[0]; - } - - /// Process a message payload from the local database. - /// TODO: Possibly run this in an isolate in the future (needs really advanced code) - /// - /// For the future: TODO: Also process the signatures in the isolate by preloading profiles - Future> _processMessages(List messages, {bool init = true}) async { - if (messages.isEmpty) { - return []; - } - - // Process all messages - final list = []; - for (var data in messages) { - final (message, _) = decryptFromLocalDatabase(data, databaseKey); - - // Don't render system messages that shouldn't be rendered (this is only for safety, should never actually happen) - if (message.type == MessageType.system && SystemMessages.messages[message.content]?.render == false) { - continue; + unawaited(tab.provider.addMessageToBottom(message)); } - - list.add(message); - } - - // Init the attachments to prepare the messages for rendering (if desired) - if (init) { - await initAttachmentsForMessages(list); } - return list; - } - - /// Init the attachments for all passed in messages. - Future initAttachmentsForMessages(List messages) async { - await Future.wait(messages.map((msg) => msg.initAttachments(this))); return true; } - - /// Decrypt a message from the local database. - /// - /// Returns message and conversation found in the local database. - static (Message, String) decryptFromLocalDatabase(MessageData data, SecureKey key, {Sodium? sodium}) { - // Create a new base message - final message = Message( - id: data.id, - type: MessageType.text, - content: decryptSymmetric(data.content, key, sodium), - answer: "", - attachments: [], - senderToken: LPHAddress.from(data.senderToken), - senderAddress: LPHAddress.from(decryptSymmetric(data.senderAddress, key, sodium)), - createdAt: DateTime.fromMillisecondsSinceEpoch(data.createdAt.toInt()), - edited: data.edited, - verified: data.verified, - ); - - // Set the type to system in case it is a system message - if (message.senderToken == MessageController.systemSender) { - message.type = MessageType.system; - message.loadContent(); - return (message, data.conversation); - } - - // Load the type, attachments, answer, .. from the content json - message.loadContent(); - - return (message, data.conversation); - } - - @override - Future deleteMessage(Message message) async { - // Check if the message is sent by the user - final token = Get.find().conversations[conversation.id]!.token; - if (message.senderToken != token.id) { - return "no.permission".tr; - } - - // Send a request to the server - final json = await postNodeJSON("/conversations/message/delete", { - "token": token.toMap(), - "data": message.id, - }); - - if (!json["success"]) { - return json["error"]; - } - - return null; - } - - @override - Future deleteMessageFromClient(String id) async { - messages.removeWhere((element) => element.id == id); - await db.message.deleteWhere((tbl) => tbl.id.equals(id)); - return true; - } - - @override - SecureKey encryptionKey() { - return conversation.key; - } - - @override - Future<(String, int)?> getTimestamp() async { - // Grab a new timestamp from the server - var json = await postNodeJSON("/conversations/timestamp", { - "token": conversation.token.toMap(), - }); - if (!json["success"]) { - return null; - } - - // The stamp is first casted to a num to prevent an error (don't remove) - return (json["token"] as String, (json["stamp"] as num).toInt()); - } - - @override - Future handleMessageSend(String timeToken, String data) async { - // Send message to the server with conversation token as authentication - final json = await postNodeJSON("/conversations/message/send", { - "token": conversation.token.toMap(), - "data": { - "token": timeToken, - "data": data, - } - }); - - if (!json["success"]) { - return json["error"]; - } - return null; - } } diff --git a/lib/controller/conversation/message_provider.dart b/lib/controller/conversation/message_provider.dart index 98a6ebba..d57a6227 100644 --- a/lib/controller/conversation/message_provider.dart +++ b/lib/controller/conversation/message_provider.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:convert'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/account/unknown_controller.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/services/chat/unknown_service.dart'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/pages/settings/data/settings_controller.dart'; @@ -15,97 +15,187 @@ import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/web.dart'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:lorien_chat_list/chat_list_controller.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:sodium_libs/sodium_libs.dart'; // Package this and message sending as one part 'message_sending.dart'; abstract class MessageProvider { - final messages = [].obs; + final messages = mapSignal({}); final waitingMessages = []; // To prevent messages from being sent twice due to a race condition //* Scroll - static const newLoadOffset = 200; + static const newLoadOffset = 50; bool topReached = false; - AutoScrollController? controller; + AutoScrollController? _scrollController; + ChatListController listController = ChatListController(initialItems: []); + + /// Helper method to get the newest message + String? getNewestMessage() { + return getMessageIdAfter(_getOrderedList(), 0, 1); + } + + /// Helper method to get the oldest message + String? getOldestMessage() { + return getMessageIdAfter(_getOrderedList(), listController.itemsCount - 1, -1); + } + + /// Helper function to make sure the message returned actually exists + String? getMessageIdAfter(List list, int index, int direction) { + while (list.length > index && messages[list[index]] == null) { + index += direction; + } + return list.length > index ? list[index] : null; + } + + /// Helper method to get the index + int? getIndexOf(String messageId) { + // Determine the index of the message + int index = 0; + for (var id in _getOrderedList()) { + if (id == messageId) { + break; + } + index++; + } + + return index; + } + + /// Returns the ID of the message immediately before the one at [index], or null if out of bounds. + String? getPreviousMessageId(int index) { + final allIds = _getOrderedList(); + if (index <= 0 || index >= allIds.length) return null; + + return getMessageIdAfter(allIds, index - 1, -1); + } + + /// Returns the ID of the message immediately next to the one at [index], or null if out of bounds. + String? getNextMessageId(int index) { + final allIds = _getOrderedList(); + if (index < 0 || index >= allIds.length - 1) return null; + + return getMessageIdAfter(allIds, index + 1, 1); + } + + /// Helper function to get the complete list + List _getOrderedList() { + return listController.newItems.reversed.toList() + listController.oldItems; + } Future addMessageToBottom(Message message, {bool animation = true}) async { - // Reset the time of the message at the bottom - lastMessage = null; + if (!bottomReached) { + return; + } + + // Update the last message date + lastMessage = message.createdAt.millisecondsSinceEpoch; // Make sure the message is fit for the bottom - if (messages.isNotEmpty && message.createdAt.isBefore(messages[0].createdAt)) { - sendLog("TODO: Reload the message list"); - return; + final newest = getNewestMessage(); + if (newest != null) { + final lastAdded = messages[newest]; + if (messages.isNotEmpty && lastAdded != null) { + if (lastAdded.createdAt.isAfter(message.createdAt)) { + sendLog("WARNING: Message time mismatch detected, but we can't add into a specific index yet :c"); + } + } } // Check if there are any messages with similar ids to prevent adding the same message again - if (waitingMessages.any((msg) => msg == message.id) || messages.any((msg) => msg.id == message.id)) { + if (waitingMessages.any((msg) => msg == message.id) || messages[message.id] != null) { return; } waitingMessages.add(message.id); - // Initialize all message data + // Add the message and initialize await message.initAttachments(this); - waitingMessages.remove(message.id); // Remove after cause then it is added - - // Only load the message, if scrolled near enough to the bottom - if (controller!.position.pixels <= newLoadOffset) { - if (controller!.position.pixels == 0) { - message.playAnimation = true; - messages.insert(0, message); - return; - } + messages[message.id] = message; + listController.addToBottom(message.id); - message.heightCallback = true; - messages.insert(0, message); - return; - } + // Remove after cause then it is added + waitingMessages.remove(message.id); } - void messageHeightCallback(Message message, double height) { - message.canScroll.value = true; - message.currentHeight = height; - controller!.jumpTo(controller!.position.pixels + height); + void newControllers(AutoScrollController newScroll) { + if (_scrollController != null) { + _scrollController!.removeListener(checkCurrentScrollHeight); + } + _scrollController = newScroll; + _scrollController!.addListener(checkCurrentScrollHeight); } - void messageHeightChange(Message message, double extraHeight) { - if (message.heightKey != null) { - controller!.jumpTo(controller!.position.pixels + extraHeight); + /// Reload at a specific spot in time + Future reloadAt(int stamp) async { + bottomReached = false; + messages.clear(); + listController.clearAll(); + newMessagesLoading.value = true; + + // Load all the messages after the stamp + final (loadedAfter, error) = await loadMessagesAfter(stamp); + if (error) { + sendLog("ERROR: Couldn't load messages after"); + return; } - } - void newScrollController(AutoScrollController newController) { - if (controller != null) { - controller!.removeListener(checkCurrentScrollHeight); + // If there are no messages after, load chat normally (no messages after the read) + if (loadedAfter == null || loadedAfter.isEmpty) { + newMessagesLoading.value = false; + sendLog("loading normally"); + bottomReached = true; + await loadNewMessagesTop(date: stamp); + return; } - controller = newController; - controller!.addListener(checkCurrentScrollHeight); - } - /// Runs on every scroll to check if new messages should be loaded - Future checkCurrentScrollHeight() async { - // Get.height is in there because there is a little bit of buffer above - if (controller == null) { + // Load all the messages before + final (loadedBefore, errorBefore) = await loadMessagesBefore(loadedAfter[0].createdAt.millisecondsSinceEpoch); + if (errorBefore) { + sendLog("ERROR: Couldn't load messages before"); return; } - if (controller!.position.pixels > controller!.position.maxScrollExtent - Get.height / 2 - newLoadOffset && !topReached) { - var (topReached, error) = await loadNewMessagesTop(); - if (!error) { - this.topReached = topReached; + + // Add the first message and all the ones before it to the thing + batch(() { + messages[loadedAfter[0].id] = loadedAfter[0]; + listController.addToTop(loadedAfter[0].id); + if (loadedBefore != null && loadedBefore.isNotEmpty) { + addMessagesToTop(loadedBefore); } - } else if (controller!.position.pixels <= newLoadOffset) { + }); + + // Add the messages below after a frame has been rendered + WidgetsBinding.instance.addPostFrameCallback((_) { + addMessagesToBottom(loadedAfter.sublist(1)); + newMessagesLoading.value = false; + }); + } + + /// Runs on every scroll to check if new messages should be loaded + Future checkCurrentScrollHeight() async { + if (_scrollController!.position.pixels <= _scrollController!.position.minScrollExtent + newLoadOffset && + !newMessagesLoading.peek()) { unawaited(loadNewMessagesBottom()); } + + if (_scrollController!.position.pixels <= _scrollController!.position.minScrollExtent + 20 && + !newMessagesLoading.peek()) { + handleBottomReached(); + } } - /// Loading state for new messages (at top or bottom) - final newMessagesLoading = false.obs; + void handleBottomReached() {} + + /// If the bottom has been reached, means new messages will be added to the bottom when there + bool bottomReached = true; - /// Whether or not the messages are loading at the top (for showing a loading indicator) - bool messagesLoadingTop = false; + /// Loading state for new messages (at top or bottom) + final newMessagesLoading = signal(false); /// The timestamp of the last message at the bottom (for preventing too many requests) int? lastMessage; @@ -118,9 +208,8 @@ abstract class MessageProvider { if (newMessagesLoading.value || (messages.isEmpty && date == null)) { return (false, true); } - messagesLoadingTop = true; newMessagesLoading.value = true; - date ??= messages.last.createdAt.millisecondsSinceEpoch; + date ??= messages[getOldestMessage()]!.createdAt.millisecondsSinceEpoch; // Load new messages final (loadedMessages, error) = await loadMessagesBefore(date); @@ -128,28 +217,34 @@ abstract class MessageProvider { newMessagesLoading.value = false; return (false, true); } - if (loadedMessages == null) { - newMessagesLoading.value = false; + newMessagesLoading.value = false; + if (loadedMessages == null || loadedMessages.isEmpty) { return (true, false); } - messages.addAll(loadedMessages); + addMessagesToTop(loadedMessages); - newMessagesLoading.value = false; return (false, false); } + void addMessagesToTop(List loaded) { + batch(() { + for (var msg in loaded) { + messages[msg.id] = msg; + } + listController.addRangeToTop(loaded.map((m) => m.id).toList()); + }); + } + /// Load new messages at the bottom of the scroll feed from the server. /// /// Returns whether or not it was successful. /// Will open an error dialog in case something goes wrong on the server. - Future loadNewMessagesBottom() async { - if (newMessagesLoading.value || messages.isEmpty) { + Future loadNewMessagesBottom({int? time}) async { + if (newMessagesLoading.value || (messages.isEmpty && time == null)) { return false; } - messagesLoadingTop = false; - newMessagesLoading.value = true; // We'll use the same loading as above to make sure this doesn't break anything - final firstMessage = messages.first; - final time = firstMessage.createdAt.millisecondsSinceEpoch; + newMessagesLoading.value = true; // Same loading state as above to not break anything + time ??= messages[getNewestMessage()]!.createdAt.millisecondsSinceEpoch; // Make sure we're not requesting the same messages again if (lastMessage == time) { @@ -160,66 +255,81 @@ abstract class MessageProvider { // Process the messages final (loadedMessages, error) = await loadMessagesAfter(time); - if (error || loadedMessages == null) { + if (error) { newMessagesLoading.value = false; return true; } - for (var message in loadedMessages) { - message.heightCallback = true; + if (loadedMessages == null || loadedMessages.isEmpty) { + newMessagesLoading.value = false; + bottomReached = true; + return true; } - loadedMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Sort to prevent weird order - messages.insertAll(0, loadedMessages); + addMessagesToBottom(loadedMessages); newMessagesLoading.value = false; return true; } + void addMessagesToBottom(List loaded) { + loaded.sort((a, b) => a.createdAt.compareTo(b.createdAt)); // Sort to prevent weird order + batch(() { + for (var msg in loaded) { + messages[msg.id] = msg; + } + listController.addRangeToBottom(loaded.map((m) => m.id).toList(), scrollToBottom: false); + }); + } + /// Scroll to a message (only animated when message is loaded in cache). /// /// Loads the message from the server if it is not in the cache and refreshes /// the complete feed in that case. Future scrollToMessage(String id) async { // Check if message is already on screen - var message = messages.firstWhereOrNull((msg) => msg.id == id); + var message = messages[id]; if (message != null) { - unawaited(controller!.scrollToIndex(messages.indexOf(message) + 1)); + unawaited(_scrollController!.scrollToIndex(getIndexOf(message.id)!)); if (message.highlightAnimation == null) { // If the message is not yet rendered do it through a callback - message.highlightCallback = () { - Timer(Duration(milliseconds: 500), () { - message!.highlightAnimation!.value = 0; - message.highlightAnimation!.animateTo(1); - }); - }; + message.queueHighlightAnimation(); } else { // If it is rendered, don't do it through a callback - message.highlightAnimation!.value = 0; - unawaited(message.highlightAnimation!.animateTo(1)); + message.playHighlightAnimation(); } return true; } + newMessagesLoading.value = true; // If message is not on screen, load it dynamically from the database message = await loadMessageFromServer(id); if (message == null) { + newMessagesLoading.value = false; + return false; + } + + // Get the messages before the message + final (loaded, error) = await loadMessagesBefore(message.createdAt.millisecondsSinceEpoch); + if (error) { + newMessagesLoading.value = false; return false; } + final toAdd = [message] + (loaded ?? []); // Add the message to the feed and remove all the others messages.clear(); - messages.add(message); + listController.clearAll(); + bottomReached = false; + addMessagesToTop(toAdd); // Highlight the message - message.highlightCallback = () { - Timer(Duration(milliseconds: 500), () { - message!.highlightAnimation!.value = 0; - message.highlightAnimation!.animateTo(1); - }); - }; + message.queueHighlightAnimation(); - // Load the messages below - await loadNewMessagesBottom(); + // Add the messages below after a frame has been rendered + WidgetsBinding.instance.addPostFrameCallback((_) { + loadNewMessagesBottom(); + }); + newMessagesLoading.value = false; return true; } @@ -228,7 +338,7 @@ abstract class MessageProvider { /// /// Returns an error or null if successful. Future sendTextMessageWithFiles( - RxBool loading, + Signal loading, String message, List files, String answer, @@ -241,7 +351,7 @@ abstract class MessageProvider { // Upload files final attachments = []; for (var file in files) { - final res = await Get.find().uploadFile(file, StorageType.temporary, Constants.fileAttachmentTag); + final res = await AttachmentController.uploadFile(file, StorageType.temporary, Constants.fileAttachmentTag); if (res.container == null) { return res.message; } @@ -257,14 +367,15 @@ abstract class MessageProvider { /// /// Returns an error or null if successful. Future sendMessage( - RxBool loading, + Signal loading, MessageType type, List attachments, String message, - String answer, - ) async { + String answer, { + (String, int)? timestampInfo, + }) async { // Check if there is a connection before doing this - if (!Get.find().connected.value) { + if (!ConnectionController.connected.value) { return "error.no_connection".tr; } @@ -304,25 +415,20 @@ abstract class MessageProvider { } // Grab a new timestamp from the server - var obj = await getTimestamp(); - if (obj == null) { + timestampInfo = await getTimestamp(); + if (timestampInfo == null) { return "error.message.timestamp".tr; } // Use the timestamp from the json (to prevent desynchronization and stuff) - final (timeToken, stamp) = obj; - final content = Message.buildContentJson( - content: message, - type: type, - attachments: attachments, - answerId: answer, - ); + final (timeToken, stamp) = timestampInfo; + final content = Message.buildContentJson(content: message, type: type, attachments: attachments, answerId: answer); // Encrypt message with signature final info = SymmetricSequencedInfo.builder(content, stamp).finish(encryptionKey()); // Send message - final error = await handleMessageSend(timeToken, info); + final error = await handleMessageSend(timeToken, info, stamp); if (error != null) { loading.value = false; return error; @@ -368,7 +474,7 @@ abstract class MessageProvider { /// This method should send this data to the server as the message. /// /// This method should return an error or null if it was successful. - Future handleMessageSend(String timeToken, String data); + Future handleMessageSend(String timeToken, String data, int stamp); } class Message { @@ -376,7 +482,7 @@ class Message { MessageType type; String content; List attachments; - final verified = true.obs; + final verified = signal(true); String answer; final LPHAddress senderToken; final LPHAddress senderAddress; @@ -385,18 +491,29 @@ class Message { Function()? highlightCallback; AnimationController? highlightAnimation; - final canScroll = false.obs; - double? currentHeight; - GlobalKey? heightKey; - bool heightReported = false; - bool heightCallback = false; + final canScroll = signal(false); bool renderingAttachments = false; final attachmentsRenderer = []; Message? answerMessage; + /// Queue playing the highlight animation on the message + void queueHighlightAnimation() { + highlightCallback = () { + Timer(500.ms, () { + playHighlightAnimation(); + }); + }; + } + + /// Play the highlight animation for the message + void playHighlightAnimation() { + highlightAnimation!.value = 0; + highlightAnimation!.loop(count: 2, reverse: true, period: 250.ms); + } + /// Extracts and decrypts the attachments Future initAttachments(MessageProvider? provider) async { - //* Load answer + // Load answer if (answer != "" && provider != null) { final message = await provider.loadMessageFromServer(answer, init: false); answerMessage = message; @@ -404,38 +521,40 @@ class Message { answerMessage = null; } - //* Load attachments + // Load attachments if (attachmentsRenderer.isNotEmpty || renderingAttachments) { return true; } renderingAttachments = true; if (attachments.isNotEmpty && type != MessageType.system) { for (var attachment in attachments) { - if (attachment.isURL) { - final container = AttachmentContainer.remoteImage(attachment); + // Parse the attachment to the container + final container = await AttachmentController.fromString(attachment); + + // Make sure to properly handle remote containers (both links and remote images) + if (container.attachmentType != AttachmentContainerType.file) { await container.init(); attachmentsRenderer.add(container); continue; } - final json = jsonDecode(attachment); - final type = await AttachmentController.checkLocations(json["i"], StorageType.temporary); - final container = Get.find().fromJson(type, json); + + // Check if the container should be downloaded automatically if (!await container.existsLocally()) { final extension = container.id.split(".").last; if (FileSettings.imageTypes.contains(extension)) { - final download = Get.find().settings[FileSettings.autoDownloadImages]!.getValue(); + final download = SettingController.settings[FileSettings.autoDownloadImages]!.getValue(); if (download) { - await Get.find().downloadAttachment(container, ignoreLimit: false); + await AttachmentController.downloadAttachment(container, ignoreLimit: false); } } else if (FileSettings.videoTypes.contains(extension)) { - final download = Get.find().settings[FileSettings.autoDownloadVideos]!.getValue(); + final download = SettingController.settings[FileSettings.autoDownloadVideos]!.getValue(); if (download) { - await Get.find().downloadAttachment(container, ignoreLimit: false); + await AttachmentController.downloadAttachment(container, ignoreLimit: false); } } else if (FileSettings.audioTypes.contains(extension)) { - final download = Get.find().settings[FileSettings.autoDownloadAudio]!.getValue(); + final download = SettingController.settings[FileSettings.autoDownloadAudio]!.getValue(); if (download) { - await Get.find().downloadAttachment(container, ignoreLimit: false); + await AttachmentController.downloadAttachment(container, ignoreLimit: false); } } } @@ -515,7 +634,7 @@ class Message { /// Verifies the signature of the message Future verifySignature(SymmetricSequencedInfo info, [Sodium? sodium]) async { - final sender = await Get.find().loadUnknownProfile(senderAddress); + final sender = await UnknownService.loadUnknownProfile(senderAddress); if (sender == null) { sendLog("NO SENDER FOUND"); verified.value = false; @@ -543,9 +662,4 @@ class Message { } } -enum MessageType { - text, - system, - call, - liveshare; -} +enum MessageType { text, system, call, liveshare } diff --git a/lib/controller/conversation/message_search_controller.dart b/lib/controller/conversation/message_search_controller.dart deleted file mode 100644 index bcb80bd4..00000000 --- a/lib/controller/conversation/message_search_controller.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'dart:async'; - -import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/conversation/message_provider.dart'; -import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/pages/status/setup/instance_setup.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:drift/drift.dart'; - -class MessageSearchController extends GetxController { - final filters = [].obs; - final results = [].obs; - FocusNode? currentFocus; - - // Data for the message search algorithm - bool _finished = false; - bool _restart = false; - int _lastTime = 0; - Timer? _searchTimer; - int neededMessages = 10; - - void search({bool increment = false}) { - _searchTimer?.cancel(); - if (increment) { - if (_finished) { - return; - } - neededMessages += 10; - _restart = false; - } else { - neededMessages = 10; - _restart = true; - } - bool working = false; - _searchTimer = Timer.periodic( - Duration(milliseconds: 50), - (timer) async { - final wasRestart = _restart; - if (_restart) { - _restart = false; - _lastTime = DateTime.now().millisecondsSinceEpoch; - } - if (working) { - return; - } - working = true; - - // Check if there is a conversation filter - final convFilter = filters.firstWhereOrNull((f) => f is ConversationFilter) as ConversationFilter?; - - // Grab all the messages from the list using the offset - // Make sure to only search in the current conversation in case there is a conversation filter - final SimpleSelectStatement<$MessageTable, MessageData> messageQuery; - if (convFilter != null) { - messageQuery = db.select(db.message) - ..where((tbl) => tbl.createdAt.isSmallerThanValue(BigInt.from(_lastTime))) - ..where((tbl) => tbl.conversation.equals(convFilter.conversationId)) - ..orderBy([(u) => OrderingTerm.desc(u.createdAt)]) - ..limit(100); - } else { - messageQuery = db.select(db.message) - ..where((tbl) => tbl.createdAt.isSmallerThanValue(BigInt.from(_lastTime))) - ..orderBy([(u) => OrderingTerm.desc(u.createdAt)]) - ..limit(100); - } - - // Make sure to only search in the current conversation in case there is a conversation filter - if (convFilter != null) {} - final messages = await messageQuery.get(); - - // If there are no messages, cancel the timer - if (messages.isEmpty) { - timer.cancel(); - return; - } - - // Set the last message time to make sure messages aren't loaded twice - _lastTime = messages.last.createdAt.toInt(); - - // Check all the filters for the current messages (maybe put in an isolate in the future?) - final found = []; - for (var message in messages) { - final (processed, conversation) = ConversationMessageProvider.decryptFromLocalDatabase(message, databaseKey); - - // Maybe remove this limitation in the future? - if (processed.type != MessageType.text) { - continue; - } - - bool fail = false; - for (var filter in filters) { - if (!filter.matches(processed, conversation: conversation)) { - fail = true; - break; - } - } - - if (!fail) { - found.add(processed); - } - } - - // Initialize all the attachments on the messages - for (var msg in found) { - await msg.initAttachments(null); - } - - // Add all found results to the list - if (wasRestart) { - results.value = found; - } else { - results.addAll(found); - - // Check if the algorithm should be stopped for now - if (results.length >= neededMessages) { - timer.cancel(); - } - } - - // Check if fetching can be stopped - if (messages.length < 100) { - _finished = true; - timer.cancel(); - } - - working = false; - }, - ); - } -} - -/// Abstract class for all filters related to messages -abstract class MessageFilter { - /// This function is called for every message in the database. - /// - /// If it returns true, the message will be loaded as part of the search results. - bool matches(Message message, {String? conversation}); -} - -/// Filter for all messages in a conversation -class ConversationFilter extends MessageFilter { - final String conversationId; - - ConversationFilter(this.conversationId); - - @override - bool matches(Message message, {String? conversation}) { - if (conversation == null) { - return false; - } - - return conversationId == conversation; - } -} - -/// Filter that checks if a certain piece of content is in a message -class ContentFilter extends MessageFilter { - final String content; - - ContentFilter(this.content); - - @override - bool matches(Message message, {String? conversation}) { - // Split the search query into words - final searchWords = content.toLowerCase().split(RegExp(r'\s+')); - - // Check if all words in the search query are found in the content - final contentWords = message.content.toLowerCase().split(RegExp(r'\s+')); - if (searchWords.every((word) => contentWords.any((contentWord) => contentWord.contains(word)))) { - return true; - } - - // Check if all words in the search query are found in any attachment - for (var attachment in message.attachments) { - final attachmentWords = attachment.toLowerCase().split(RegExp(r'\s+')); - if (searchWords.every((word) => attachmentWords.any((attachmentWord) => attachmentWord.contains(word)))) { - return true; - } - } - - // No matches found - return false; - } -} diff --git a/lib/controller/conversation/message_sending.dart b/lib/controller/conversation/message_sending.dart index 7d099227..7a08faef 100644 --- a/lib/controller/conversation/message_sending.dart +++ b/lib/controller/conversation/message_sending.dart @@ -1,12 +1,17 @@ part of 'message_provider.dart'; class MessageSendHelper { - static final currentDraft = Rx(null); + static final currentDraft = signal(null); static final drafts = {}; // TargetID -> Message draft /// Add a reply to the current message draft static void addReplyToCurrentDraft(Message message) { - currentDraft.value?.answer.value = AnswerData(message.id, message.senderAddress, message.content, message.attachments); + currentDraft.value?.answer.value = AnswerData( + message.id, + message.senderAddress, + message.content, + message.attachments, + ); } /// Add a file to the current message draft @@ -26,9 +31,7 @@ class MessageSendHelper { if (size > specialConstants[Constants.specialConstantMaxFileSize]! * 1000 * 1000) { showErrorPopup( "error", - "file.too_large".trParams({ - "1": specialConstants[Constants.specialConstantMaxFileSize].toString(), - }), + "file.too_large".trParams({"1": specialConstants[Constants.specialConstantMaxFileSize].toString()}), ); return false; } @@ -49,7 +52,12 @@ class AnswerData { AnswerData(this.id, this.senderAddress, this.content, this.attachments); /// Convert a message to the answer content for the reply container - static String answerContent(MessageType type, String content, List attachments, {FriendController? controller}) { + static String answerContent( + MessageType type, + String content, + List attachments, { + FriendController? controller, + }) { // Return different information based on every type switch (type) { case MessageType.text: @@ -61,7 +69,7 @@ class AnswerData { if (attachments.first.isURL) { content = attachments.first; } else { - content = Get.find().fromJson(StorageType.cache, jsonDecode(attachments.first)).name; + content = AttachmentController.fromJson(StorageType.cache, jsonDecode(attachments.first)).name; } } return content; @@ -77,9 +85,9 @@ class AnswerData { class MessageDraft { final String target; - final answer = Rx(null); + final answer = signal(null); String message; - final files = [].obs; + final files = listSignal([]); final attachments = []; MessageDraft(this.target, this.message); @@ -87,7 +95,7 @@ class MessageDraft { class UploadData { final XFile file; - final progress = 0.0.obs; + final progress = signal(0.0); UploadData(this.file); } diff --git a/lib/controller/conversation/sidebar_controller.dart b/lib/controller/conversation/sidebar_controller.dart new file mode 100644 index 00000000..24bc8b92 --- /dev/null +++ b/lib/controller/conversation/sidebar_controller.dart @@ -0,0 +1,99 @@ +import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class SidebarController { + static final rightSidebar = mapSignal({}); + static final hideSidebar = signal(false); + static final loaded = signal(false); + static final currentOpenTab = signal(DefaultSidebarTab()); + + /// Set the right sidebar for the current sidebar tab. + static void setRightSidebar(RightSidebar? tab) { + rightSidebar[currentOpenTab.peek().key] = tab; + if (Get.width <= 1200) { + if (tab != null) { + hideSidebar.value = true; + } else { + hideSidebar.value = false; + } + } + } + + /// Toggle the open state of the main sidebar. + static void toggleSidebar() { + hideSidebar.value = !hideSidebar.peek(); + if (Get.width <= 1200 && rightSidebar[currentOpenTab.peek().key] != null) { + rightSidebar[currentOpenTab.peek().key] = null; + } + } + + /// Set a new tab for the sidebar. + static void openTab(SidebarTab tab) { + try { + if (!(rightSidebar[currentOpenTab.peek().key]?.cache ?? true)) { + rightSidebar.remove(currentOpenTab.peek().key); + } + currentOpenTab.value = tab; + } catch (e) { + sendLog("WANRING: The weird exception happened again."); + } + } + + /// Get the current message provider (in case the current tab is a [ConversationSidebarTab]). + /// + /// No subscriptions are made during this call, for that use [getCurrentProviderReactive]. + static ConversationMessageProvider? getCurrentProvider() { + final tab = currentOpenTab.peek(); + if (tab is ConversationSidebarTab) { + return tab.provider; + } + return null; + } + + /// Get the current message provider (in case the current tab is a [ConversationSidebarTab]). + static ConversationMessageProvider? getCurrentProviderReactive() { + final tab = currentOpenTab.value; + if (tab is ConversationSidebarTab) { + return tab.provider; + } + return null; + } + + /// Helper function to unselect a specific conversation with [id]. + static void unselectConversation(LPHAddress id) { + final provider = getCurrentProvider(); + if (provider != null && provider.conversation.id == id) { + openTab(DefaultSidebarTab()); + } + } + + /// Helper function to get the current key (for the current open sidebar tab) + static String getCurrentKey() { + return currentOpenTab.value.key; + } +} + +enum SidebarTabType { none, conversation, space } + +abstract class SidebarTab { + final String key; + final SidebarTabType type; + + SidebarTab(this.type, this.key); + + Widget build(BuildContext context); +} + +abstract class RightSidebar { + final String key; + final bool cache; + + RightSidebar(this.key, {this.cache = false}); + + Widget build(BuildContext context); +} diff --git a/lib/controller/conversation/square.dart b/lib/controller/conversation/square.dart new file mode 100644 index 00000000..6b5364f2 --- /dev/null +++ b/lib/controller/conversation/square.dart @@ -0,0 +1,70 @@ +import 'dart:convert'; + +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/database/database_entities.dart' as model; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:signals/signals_flutter.dart'; + +class Square extends Conversation { + final topicsShown = signal(false); + + Square( + LPHAddress id, + String vaultId, + ConversationToken token, + SquareContainer container, + String packedKey, + int lastVersion, + int updatedAt, + ConversationReads reads, + ) : super(id, vaultId, model.ConversationType.square, token, container, packedKey, lastVersion, updatedAt, reads); + + @override + Square.fromJson(Map json, String vaultId) + : this( + LPHAddress.from(json["id"]), + vaultId, + ConversationToken.fromJson(json["token"]), + SquareContainer.fromJson(json["data"]), + json["key"], + 0, // Just ignore it for now + json["update"] ?? DateTime.now().millisecondsSinceEpoch, + ConversationReads.fromContainer(""), + ); + + @override + Square.fromData(ConversationData data) + : this( + LPHAddress.from(data.id), + fromDbEncrypted(data.vaultId), + ConversationToken.fromJson(jsonDecode(fromDbEncrypted(data.token))), + SquareContainer.fromJson(jsonDecode(fromDbEncrypted(data.data))), + fromDbEncrypted(data.key), + data.lastVersion.toInt(), + data.updatedAt.toInt(), + ConversationReads.fromLocalContainer(data.reads), + ); + + @override + factory Square.copyWithoutKey(Square square) { + final copy = Square( + square.id, + square.vaultId, + square.token, + square.container as SquareContainer, + "", + square.lastVersion, + square.updatedAt, + square.reads, + ); + + // Copy all the members + copy.members.addAll(square.members); + + return copy; + } +} diff --git a/lib/controller/conversation/system_messages.dart b/lib/controller/conversation/system_messages.dart index ed38094c..330b37db 100644 --- a/lib/controller/conversation/system_messages.dart +++ b/lib/controller/conversation/system_messages.dart @@ -1,7 +1,8 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -14,10 +15,9 @@ class SystemMessages { Icons.shield, translation: (msg, provider) { if (provider is ConversationMessageProvider) { - final friendController = Get.find(); return "chat.rank_change.${msg.attachments[0]}->${msg.attachments[1]}".trParams({ - "name": friendController.getFriend(LPHAddress.from(msg.attachments[2])).displayName.value, - "sender": friendController.getFriend(LPHAddress.from(msg.attachments[3])).displayName.value, // NZJNP232RS5g + "name": FriendController.getFriend(LPHAddress.from(msg.attachments[2])).displayName.value, + "sender": FriendController.getFriend(LPHAddress.from(msg.attachments[3])).displayName.value, // NZJNP232RS5g }); } @@ -25,7 +25,7 @@ class SystemMessages { }, handler: (msg, provider) { if (provider is ConversationMessageProvider) { - provider.conversation.fetchData(); + ConversationService.fetchNewestVersion(provider.conversation); } }, ), @@ -37,7 +37,7 @@ class SystemMessages { translation: (msg, provider) { if (provider is ConversationMessageProvider) { return "chat.token_change".trParams({ - "name": Get.find().getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, + "name": FriendController.getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, }); } @@ -45,7 +45,7 @@ class SystemMessages { }, handler: (msg, provider) { if (provider is ConversationMessageProvider) { - provider.conversation.fetchData(); + ConversationService.fetchNewestVersion(provider.conversation); } }, ), @@ -57,7 +57,7 @@ class SystemMessages { translation: (msg, provider) { if (provider is ConversationMessageProvider) { return "chat.member_join".trParams({ - "name": Get.find().getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, + "name": FriendController.getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, }); } @@ -65,7 +65,7 @@ class SystemMessages { }, handler: (msg, provider) { if (provider is ConversationMessageProvider) { - provider.conversation.fetchData(); + ConversationService.fetchNewestVersion(provider.conversation); } }, ), @@ -77,8 +77,8 @@ class SystemMessages { translation: (msg, provider) { if (provider is ConversationMessageProvider) { return "chat.member_invite".trParams({ - "invitor": Get.find().getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, - "name": Get.find().getFriend(LPHAddress.from(msg.attachments[1])).displayName.value, + "invitor": FriendController.getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, + "name": FriendController.getFriend(LPHAddress.from(msg.attachments[1])).displayName.value, }); } @@ -86,7 +86,7 @@ class SystemMessages { }, handler: (msg, provider) { if (provider is ConversationMessageProvider) { - provider.conversation.fetchData(); + ConversationService.fetchNewestVersion(provider.conversation); } }, ), @@ -98,14 +98,14 @@ class SystemMessages { translation: (msg, provider) { if (provider is ConversationMessageProvider) { return "chat.member_leave".trParams({ - "name": Get.find().getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, + "name": FriendController.getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, }); } return "not.supported".tr; }, handler: (msg, provider) { if (provider is ConversationMessageProvider) { - provider.conversation.fetchData(); + ConversationService.fetchNewestVersion(provider.conversation); } }, ), @@ -117,15 +117,15 @@ class SystemMessages { translation: (msg, provider) { if (provider is ConversationMessageProvider) { return "chat.kick".trParams({ - "issuer": Get.find().getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, - "name": Get.find().getFriend(LPHAddress.from(msg.attachments[1])).displayName.value, + "issuer": FriendController.getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, + "name": FriendController.getFriend(LPHAddress.from(msg.attachments[1])).displayName.value, }); } return "not.supported".tr; }, handler: (msg, provider) { if (provider is ConversationMessageProvider) { - provider.conversation.fetchData(); + ConversationService.fetchNewestVersion(provider.conversation); } }, ), @@ -137,7 +137,7 @@ class SystemMessages { translation: (msg, provider) { if (provider is ConversationMessageProvider) { return "chat.new_admin".trParams({ - "name": Get.find().getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, + "name": FriendController.getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, }); } @@ -145,7 +145,7 @@ class SystemMessages { }, handler: (msg, provider) { if (provider is ConversationMessageProvider) { - provider.conversation.fetchData(); + ConversationService.fetchNewestVersion(provider.conversation); } }, ), @@ -153,12 +153,11 @@ class SystemMessages { // Called when someone changes something about the conversation // Format: [accountId] "conv.edited": SystemMessage( - Icons.update, - store: false, + Icons.edit, translation: (msg, provider) { if (provider is ConversationMessageProvider) { - return "chat.edit_data".trParams({ - "name": Get.find().getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, + return "conv.edit_data".trParams({ + "name": FriendController.getFriend(LPHAddress.from(msg.attachments[0])).displayName.value, }); } @@ -166,7 +165,7 @@ class SystemMessages { }, handler: (msg, provider) { if (provider is ConversationMessageProvider) { - provider.conversation.fetchData(); + ConversationService.fetchNewestVersion(provider.conversation); } }, ), @@ -194,7 +193,11 @@ class SystemMessages { handler: (msg, provider) { if (provider is ConversationMessageProvider) { if (LPHAddress.from(msg.attachments[0]) == StatusController.ownAddress) { - provider.conversation.delete(popup: false, request: false); + ConversationService.delete( + provider.conversation.id, + vaultId: provider.conversation.vaultId, + token: provider.conversation.token, + ); } } }, diff --git a/lib/controller/conversation/zap_share_controller.dart b/lib/controller/conversation/zap_share_controller.dart index 8567321c..4d3ee890 100644 --- a/lib/controller/conversation/zap_share_controller.dart +++ b/lib/controller/conversation/zap_share_controller.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; -import 'package:chat_interface/connection/connection.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/messaging.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart' as msg; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/main.dart'; @@ -22,33 +22,34 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'package:liphium_bridge/liphium_bridge.dart'; import 'package:open_file/open_file.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:sodium_libs/sodium_libs.dart'; import 'package:path/path.dart' as path; -class ZapShareController extends GetxController { +class ZapShareController { // Current transaction - final currentReceiver = Rx(null); - final currentConversation = Rx(null); - final waiting = false.obs; - final step = "loading".tr.obs; - final progress = 0.0.obs; - bool uploading = false; - final currentPart = 0.obs; - int endPart = 0; - String? transactionId; - String? transactionToken; - String? uploadToken; - String? filePath; - SecureKey? key; - StreamSubscription? partSubscription; + static final currentReceiver = signal(null); + static final currentConversation = signal(null); + static final waiting = signal(false); + static final step = signal("loading".tr); + static final progress = signal(0.0); + static bool uploading = false; + static final currentPart = signal(0); + static int endPart = 0; + static String? transactionId; + static String? transactionToken; + static String? uploadToken; + static String? filePath; + static SecureKey? key; + static StreamSubscription? partSubscription; static const chunkSize = 1024 * 1024; - bool isRunning() { + static bool isRunning() { return currentReceiver.value != null || currentConversation.value != null || waiting.value; } - void resetControllerState() { + static void resetControllerState() { step.value = "loading".tr; currentReceiver.value = null; currentConversation.value = null; @@ -67,14 +68,12 @@ class ZapShareController extends GetxController { partSubscription = null; } - void cancel() { + static void cancel() { if (!isRunning()) { return; } if (uploading) { - connector.sendAction( - ServerAction("cancel_transaction", {}), - ); + connector.sendAction(ServerAction("cancel_transaction", {})); } else { partSubscription?.cancel(); } @@ -82,7 +81,7 @@ class ZapShareController extends GetxController { } /// Open the window for zap share for a conversation - Future openWindow(Conversation conversation, ContextMenuData data) async { + static Future openWindow(Conversation conversation, ContextMenuData data) async { if (GetPlatform.isMobile) { showErrorPopup("error", "zap.no_mobile".tr); return; @@ -106,7 +105,7 @@ class ZapShareController extends GetxController { //* Everything about sending starts here - Future newTransaction(LPHAddress friend, LPHAddress conversationId, List files) async { + static Future newTransaction(LPHAddress friend, LPHAddress conversationId, List files) async { if (files.length > 1) { sendLog("zapping multiple files is currently not supported"); return; @@ -135,10 +134,7 @@ class ZapShareController extends GetxController { endPart = (fileSize.toDouble() / chunkSize.toDouble()).ceil(); step.value = "chat.zapshare.waiting".tr; connector.sendAction( - ServerAction("create_transaction", { - "name": fileName, - "size": fileSize, - }), + ServerAction("create_transaction", {"name": fileName, "size": fileSize}), handler: (event) { if (!event.data["success"]) { sendLog("creating transaction failed"); @@ -153,19 +149,31 @@ class ZapShareController extends GetxController { uploadToken = event.data["upload_token"]; // Send live share message - final container = LiveshareInviteContainer(event.data["url"], transactionId!, transactionToken!, fileName, key!); - Get.find().currentProvider.value!.sendMessage(false.obs, msg.MessageType.liveshare, [], container.toJson(), ""); + final container = LiveshareInviteContainer( + event.data["url"], + transactionId!, + transactionToken!, + fileName, + key!, + ); + SidebarController.getCurrentProvider()!.sendMessage( + signal(false), + msg.MessageType.liveshare, + [], + container.toJson(), + "", + ); }, ); } // For the zapper to know what to download - int currentlySending = 0; - int currentEndPart = 0; - bool zapperStarted = false; + static int currentlySending = 0; + static int currentEndPart = 0; + static bool zapperStarted = false; /// Zap deamon - Future _startZapper(int start, int end) async { + static Future _startZapper(int start, int end) async { if (zapperStarted) { return; } @@ -220,7 +228,7 @@ class ZapShareController extends GetxController { } /// Upload the actual file part to the server - Future _sendActualFilePart(int chunk) async { + static Future _sendActualFilePart(int chunk) async { if (!isRunning()) { sendLog("why would the server ask for parts when zap isn't even running :smug:"); return false; @@ -233,7 +241,8 @@ class ZapShareController extends GetxController { // Calculate the size of the next chunk to prefill the list (optimization) final fileSize = await file.length(); - final Uint8List toEncrypt = chunk * chunkSize >= fileSize ? Uint8List(fileSize - (chunk - 1) * chunkSize) : Uint8List(chunkSize); + final Uint8List toEncrypt = + chunk * chunkSize >= fileSize ? Uint8List(fileSize - (chunk - 1) * chunkSize) : Uint8List(chunkSize); // Send the chunk once done final completer = Completer(); @@ -259,12 +268,7 @@ class ZapShareController extends GetxController { final res = await dio.post( nodePath("/auth/liveshare/upload"), data: formData, - options: d.Options( - validateStatus: (status) => true, - headers: { - authorizationHeader: authorizationValue(), - }, - ), + options: d.Options(validateStatus: (status) => true, headers: {authorizationHeader: authorizationValue()}), ); // Could've been stopped at this point @@ -293,10 +297,10 @@ class ZapShareController extends GetxController { return completer.future; } - int sending = 0; + static int sending = 0; /// Called for every file part sending request by the server - Future onFilePartRequest(Event event) async { + static Future onFilePartRequest(Event event) async { if (!isRunning()) { sendLog("why would the server ask for parts when zap share isn't even running :smug:"); return; @@ -314,7 +318,7 @@ class ZapShareController extends GetxController { } /// Called when a transaction is ended (only when sending) - Future onTransactionEnd() async { + static Future onTransactionEnd() async { waiting.value = false; uploading = false; progress.value = 0.0; @@ -333,7 +337,11 @@ class ZapShareController extends GetxController { //* Everything about receiving starts here /// Join a transaction with a given ID and token + start listening for parts - Future joinTransaction(LPHAddress conversation, LPHAddress friendAddress, LiveshareInviteContainer container) async { + static Future joinTransaction( + LPHAddress conversation, + LPHAddress friendAddress, + LiveshareInviteContainer container, + ) async { if (isRunning()) { sendLog("Already in a transaction"); return; @@ -347,10 +355,10 @@ class ZapShareController extends GetxController { step.value = "preparing".tr; // Get info about the file - final json = await postAny( - "${nodeProtocol()}${container.url}/liveshare/info", - {"id": container.id, "token": container.token}, - ); + final json = await postAny("${nodeProtocol()}${container.url}/liveshare/info", { + "id": container.id, + "token": container.token, + }); if (!json["success"]) { showErrorPopup("error", "chat.zapshare.not_found".tr); return; @@ -377,19 +385,14 @@ class ZapShareController extends GetxController { currentReceiver.value = friendAddress; // Subscribe to byte stream - final formData = d.FormData.fromMap({ - "id": container.id, - "token": container.token, - }); + final formData = d.FormData.fromMap({"id": container.id, "token": container.token}); final res = await dio.post( "${nodeProtocol()}${container.url}/liveshare/subscribe", data: formData, options: d.Options( validateStatus: (status) => status != 404, responseType: d.ResponseType.stream, - headers: { - authorizationHeader: authorizationValue(), - }, + headers: {authorizationHeader: authorizationValue()}, ), ); final body = res.data as d.ResponseBody; @@ -445,18 +448,11 @@ class ZapShareController extends GetxController { } // Download stuff - final formData = d.FormData.fromMap({ - "id": container.id, - "token": container.token, - "chunk": currentChunk, - }); + final formData = d.FormData.fromMap({"id": container.id, "token": container.token, "chunk": currentChunk}); final res = await dio.get( "${nodeProtocol()}${container.url}/liveshare/download", data: formData, - options: d.Options( - responseType: d.ResponseType.bytes, - validateStatus: (status) => true, - ), + options: d.Options(responseType: d.ResponseType.bytes, validateStatus: (status) => true), ); if (res.statusCode != 200) { @@ -481,21 +477,23 @@ class ZapShareController extends GetxController { } // Tell the server the part was successfully received - unawaited(_tellReceived( - container.url, - container.id, - container.token, - receiverId, - callback: (complete) async { - completed = complete; - if (completed) { - sendLog("download with zap completed, opening final folder.."); - unawaited(OpenFile.open(path.dirname(receiveFile.path))); - await partSubscription?.cancel(); - } - }, - onError: () => sendLog("error"), - )); + unawaited( + _tellReceived( + container.url, + container.id, + container.token, + receiverId, + callback: (complete) async { + completed = complete; + if (completed) { + sendLog("download with zap completed, opening final folder.."); + unawaited(OpenFile.open(path.dirname(receiveFile.path))); + await partSubscription?.cancel(); + } + }, + onError: () => sendLog("error"), + ), + ); } } }, @@ -509,18 +507,19 @@ class ZapShareController extends GetxController { ); } - Future _tellReceived(String url, String id, String token, String receiverId, {Function(bool)? callback, Function()? onError}) async { + static Future _tellReceived( + String url, + String id, + String token, + String receiverId, { + Function(bool)? callback, + Function()? onError, + }) async { // Send receive confirmation final res = await dio.post( "${nodeProtocol()}$url/liveshare/received", - data: jsonEncode({ - "id": id, - "token": token, - "receiver": receiverId, - }), - options: d.Options( - validateStatus: (status) => true, - ), + data: jsonEncode({"id": id, "token": token, "receiver": receiverId}), + options: d.Options(validateStatus: (status) => true), ); if (res.statusCode != 200) { @@ -558,12 +557,6 @@ class LiveshareInviteContainer { } String toJson() { - return jsonEncode({ - "url": url, - "id": id, - "token": token, - "name": fileName, - "key": packageSymmetricKey(key), - }); + return jsonEncode({"url": url, "id": id, "token": token, "name": fileName, "key": packageSymmetricKey(key)}); } } diff --git a/lib/controller/current/connection_controller.dart b/lib/controller/current/connection_controller.dart index cfabfe7e..3eec7967 100644 --- a/lib/controller/current/connection_controller.dart +++ b/lib/controller/current/connection_controller.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:chat_interface/connection/connection.dart'; +import 'package:chat_interface/services/connection/connection.dart'; import 'package:chat_interface/controller/current/steps/account_step.dart'; import 'package:chat_interface/controller/current/steps/connection_step.dart'; import 'package:chat_interface/controller/current/tasks/friend_sync_task.dart'; @@ -13,28 +13,27 @@ import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class ConnectionController extends GetxController { - final loading = false.obs; - final connected = false.obs; - final error = RxString(""); - Timer? _retryTimer; +class ConnectionController { + static final loading = signal(false); + static final connected = signal(false); + static final error = signal(""); + static Timer? _retryTimer; // Static tasks so their loading state can be accessed from anywhere static final friendSyncTask = FriendsSyncTask(); static final vaultSyncTask = VaultSyncTask(); /// Tasks that run after the setup - final _tasks = [ - friendSyncTask, - vaultSyncTask, - ]; - bool tasksRan = false; + static Timer? _taskRunner; + static final _tasks = [friendSyncTask, vaultSyncTask]; + static bool tasksRan = false; /// Steps that run to get the client connected - final _steps = []; + static final _steps = []; - ConnectionController() { + static void init() { // Refresh the token and make sure it works _steps.add(RefreshTokenStep()); @@ -51,13 +50,15 @@ class ConnectionController extends GetxController { _steps.add(StoredActionsSetup()); } - Future tryConnection() async { + static Future tryConnection() async { // Initialize all the stuff for (var task in _tasks) { + sendLog("initializing task ${task.name}.."); final result = await task.init(); if (result != null) { error.value = ""; error.value = result; + sendLog("task ${task.name} failed during initialization: $result"); _retry(); } } @@ -98,22 +99,33 @@ class ConnectionController extends GetxController { unawaited(_startTasks()); } - Future _startTasks() async { + static Future _startTasks() async { if (tasksRan) return; tasksRan = true; - // Start all the tasks - for (var task in _tasks) { - unawaited(task.start()); + // Create an inline function for running the tasks + Future runTasks() async { + for (var task in _tasks) { + sendLog("running sync task ${task.name}.."); + final error = await task.run(); + if (error != null) { + sendLog("task ${task.name} finished with error: $error"); + } + } } + + // Start the task runner + _taskRunner = Timer.periodic(Duration(seconds: 30), (timer) => runTasks()); + await runTasks(); } - void restart() { + static void restart() { tasksRan = false; // Reset all data from the tasks before the restart + _taskRunner?.cancel(); for (var task in _tasks) { - task.stop(); + task.onRestart(); } // Reset all previous state @@ -126,14 +138,14 @@ class ConnectionController extends GetxController { } /// Retries to connect again after a certain amount of time - void _retry() { + static void _retry() { _retryTimer?.cancel(); _retryTimer = Timer(const Duration(seconds: 10), () { tryConnection(); }); } - void connectionStopped() { + static void connectionStopped() { connected.value = false; loading.value = true; error.value = "error.network".tr; @@ -147,11 +159,7 @@ class SetupResponse { final bool retryConnection; final String? error; - SetupResponse({ - this.restart = false, - this.retryConnection = false, - this.error, - }); + SetupResponse({this.restart = false, this.retryConnection = false, this.error}); } abstract class ConnectionStep { @@ -164,29 +172,22 @@ abstract class ConnectionStep { abstract class SynchronizationTask { final String name; - final Duration frequency; - SynchronizationTask(this.name, this.frequency); + SynchronizationTask(this.name); - Timer? _timer; - final loading = false.obs; + final loading = signal(false); - /// Starts the task. - Future start() async { - _timer = Timer.periodic( - frequency, - (timer) async { - if (loading.value) { - return; - } - loading.value = true; - final result = await refresh(); - if (result != null) { - sendLog("task $name finished with error: $result"); - } - loading.value = false; - }, - ); + /// Run an interation of the task. + /// + /// Returns an error if there was one. + Future run() async { + if (loading.value) { + return "loading".tr; + } + loading.value = true; + final result = await refresh(); + loading.value = false; + return result; } /// This method should initialize everything needed for the task. @@ -194,15 +195,7 @@ abstract class SynchronizationTask { /// Returns an error if there is one. Future init(); - /// Stops the task. - void stop() { - _timer?.cancel(); - onRestart(); - } - /// This method will be called every time in the loop. - /// You can specify the duration of it using the [frequency] parameter in - /// the constructor. /// /// Returns an error if there is one. Future refresh(); diff --git a/lib/controller/current/status_controller.dart b/lib/controller/current/status_controller.dart index 5ed25629..7b359dd8 100644 --- a/lib/controller/current/status_controller.dart +++ b/lib/controller/current/status_controller.dart @@ -1,152 +1,106 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; - -import 'package:chat_interface/connection/connection.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/chat/setup_listener.dart'; -import 'package:chat_interface/connection/messaging.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/conversation/attachment_controller.dart'; + +import 'package:chat_interface/services/chat/status_service.dart'; +import 'package:chat_interface/theme/ui/profile/status_renderer.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/current/steps/account_step.dart'; -import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/pages/settings/account/data_settings.dart'; -import 'package:chat_interface/pages/settings/data/settings_controller.dart'; -import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; -import 'package:drift/drift.dart'; -import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class StatusController extends GetxController { +class StatusController { static String ownAccountId = ""; static List permissions = []; static List ranks = []; static LPHAddress get ownAddress => LPHAddress(basePath, ownAccountId); - Timer? _timer; - StatusController() { - if (_timer != null) _timer!.cancel(); - - // Update status every minute - _timer = Timer.periodic(const Duration(minutes: 1), (timer) { - if (connector.isConnected()) { - setStatus(); - } - }); - } - - final displayName = "not-set".obs; - final name = 'not-set'.obs; + static final displayName = signal("not-set"); + static final name = signal('not-set'); // Status message - final statusLoading = true.obs; - final status = ''.obs; - final type = 1.obs; + static final statusLoading = signal(true); + static final status = signal(""); + static final type = signal(1); // Shared content by friends - final sharedContent = RxMap(); + static final sharedContent = mapSignal({}); // Current shared content (by this account) - final ownContainer = Rx(null); + static final ownContainer = signal(null); void setName(String value) => name.value = value; - String statusJson() => jsonEncode({ - "s": base64Encode(utf8.encode(status.value)), - "t": type.value, - }); + /// Get the current status json for the current client. + static String statusJson() => + jsonEncode({"s": base64Encode(utf8.encode(status.peek())), "t": type.peek()}); - String newStatusJson(String status, int type) => jsonEncode({ - "s": base64Encode(utf8.encode(status)), - "t": type, - }); + /// Create a new status json. + static String newStatusJson(String status, int type) => + jsonEncode({"s": base64Encode(utf8.encode(status)), "t": type}); - void fromStatusJson(String json) { - sendLog("received $json"); - final data = jsonDecode(json); - try { - status.value = utf8.decode(base64Decode(data["s"])); - } catch (e) { + /// Load the default status because nothing has been saved yet. + static void loadDefaultStatus() { + batch(() { status.value = ""; - } - type.value = data["t"] ?? 1; + type.value = statusOnline; + statusLoading.value = false; + }); + } + + /// Update the status from a status json. + static void fromStatusJson(String json) { + // Decode the status + final data = jsonDecode(json); + + // Start a batch to set the new status + batch(() { + try { + status.value = utf8.decode(base64Decode(data["s"])); + } catch (e) { + status.value = ""; + } + type.value = data["t"] ?? 1; + statusLoading.value = false; + }); } - String statusPacket([String? newStatusJson]) { + /// Get the encrypted version of the status json. + static String statusPacket([String? newStatusJson]) { return encryptSymmetric(newStatusJson ?? statusJson(), profileKey); } - String sharedContentPacket() { + /// Get the encrypted version of the currently shared content. + static String sharedContentPacket() { if (ownContainer.value == null) { return ""; } return encryptSymmetric(ownContainer.value!.toJson(), profileKey); } - Future share(ShareContainer container) async { + /// Share a new [ShareContainer]. + static Future share(ShareContainer container) async { if (ownContainer.value != null) return false; // TODO: Potentially remove ownContainer.value = container; - await setStatus(); + await StatusService.sendStatus(); return true; } - void stopSharing() { + /// Stop sharing the current [ShareContainer]. + static void stopSharing() { if (ownContainer.value == null) { return; } ownContainer.value = null; - setStatus(); + StatusService.sendStatus(); } - Future setStatus({String? message, int? type, Function()? success}) async { - if (statusLoading.value) return false; - statusLoading.value = true; - - // Secret: Enable new social features experiment - if (message == "liphium.social") { - message = "activated"; - await Get.find().settings[DataSettings.socialFeatures]!.setValue(true); - } - - // Validate the status to make sure everything is fine - connector.sendAction( - ServerAction("st_validate", { - "status": statusPacket(newStatusJson(message ?? status.value, type ?? this.type.value)), - "data": sharedContentPacket(), - }), handler: (event) { - statusLoading.value = false; - success?.call(); - if (event.data["success"] == true) { - if (message != null) status.value = message; - if (type != null) this.type.value = type; - - // Send the new status - subscribeToConversations(controller: this); - } + /// Update the current status. + static void updateStatus({String? message, int? type}) { + batch(() { + if (message != null) status.value = message; + if (type != null) StatusController.type.value = type; }); - - return true; - } - - // Log out of this account - Future logOut({deleteEverything = false, deleteFiles = false}) async { - // Delete the session information - await db.setting.deleteWhere((tbl) => tbl.key.equals("profile")); - - // Delete all data - if (deleteEverything) { - for (var table in db.allTables) { - await table.deleteAll(); - } - } - - // Delete all files - if (deleteFiles) { - await Get.find().deleteAllFiles(); - } - - // Exit the app - exit(0); } } @@ -174,27 +128,15 @@ class RankData { String name; int level; - RankData({ - required this.id, - required this.name, - required this.level, - }); + RankData({required this.id, required this.name, required this.level}); // Factory constructor to create Rank object from JSON factory RankData.fromJson(Map json) { - return RankData( - id: json['id'] as int, - name: json['name'] as String, - level: json['level'] as int, - ); + return RankData(id: json['id'] as int, name: json['name'] as String, level: json['level'] as int); } // Method to convert Rank object to JSON Map toJson() { - return { - 'id': id, - 'name': name, - 'level': level, - }; + return {'id': id, 'name': name, 'level': level}; } } diff --git a/lib/controller/current/steps/account_step.dart b/lib/controller/current/steps/account_step.dart index 40e274d8..a024b5d8 100644 --- a/lib/controller/current/steps/account_step.dart +++ b/lib/controller/current/steps/account_step.dart @@ -1,5 +1,7 @@ -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'dart:async'; + +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/controller/current/steps/key_step.dart'; @@ -8,7 +10,6 @@ import 'package:chat_interface/pages/status/setup/instance_setup.dart'; import 'package:chat_interface/standards/server_stored_information.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; -import 'package:get/get.dart'; import 'package:sodium_libs/sodium_libs.dart'; late SecureKey vaultKey; @@ -17,6 +18,12 @@ late SecureKey profileKey; class AccountStep extends ConnectionStep { AccountStep() : super('loading.account'); + /// A completer to wait until the keys are set. + /// + /// Instantiated in [FriendSyncTask]. + /// Completed when this setup is over. + static Completer? keyCompleter; + @override Future load() async { // Get account from database @@ -28,21 +35,21 @@ class AccountStep extends ConnectionStep { } // Set all account data - StatusController controller = Get.find(); - final uNameChanged = controller.name.value != account["username"]; - final dNameChanged = controller.displayName.value != account["display_name"]; + final uNameChanged = StatusController.name.value != account["username"]; + final dNameChanged = StatusController.displayName.value != account["display_name"]; // Set the account id if there isn't one - if (StatusController.ownAccountId == "" || uNameChanged || dNameChanged || StatusController.ownAccountId != account["id"]) { + if (StatusController.ownAccountId == "" || + uNameChanged || + dNameChanged || + StatusController.ownAccountId != account["id"]) { sendLog("setting account id"); await setEncryptedValue("cache_account_id", account["id"]); await setEncryptedValue("cache_account_uname", account["username"]); await setEncryptedValue("cache_account_dname", account["display_name"]); // Restart to migrate to the new account id - return SetupResponse( - restart: true, - ); + return SetupResponse(restart: true); } // Set all permissions @@ -62,8 +69,12 @@ class AccountStep extends ConnectionStep { storedActionKey = body["actions"]; // Set own key pair as cached (in the friend that represents this account) - Get.find().friends[StatusController.ownAddress]!.keyStorage = - KeyStorage(asymmetricKeyPair.publicKey, signatureKeyPair.publicKey, profileKey, ""); + FriendController.friends[StatusController.ownAddress]!.setKeyStorage( + KeyStorage(asymmetricKeyPair.publicKey, signatureKeyPair.publicKey, profileKey, ""), + ); + + // Tell the completer that the keys of the own friend have been set + keyCompleter?.complete(); return SetupResponse(); } diff --git a/lib/controller/current/steps/connection_step.dart b/lib/controller/current/steps/connection_step.dart index db17b4f8..87954d54 100644 --- a/lib/controller/current/steps/connection_step.dart +++ b/lib/controller/current/steps/connection_step.dart @@ -1,4 +1,4 @@ -import 'package:chat_interface/connection/connection.dart'; +import 'package:chat_interface/services/connection/connection.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/main.dart'; diff --git a/lib/controller/current/steps/key_step.dart b/lib/controller/current/steps/key_step.dart index 26a2a7be..2dac5d4f 100644 --- a/lib/controller/current/steps/key_step.dart +++ b/lib/controller/current/steps/key_step.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'dart:convert'; -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/connection/encryption/hash.dart'; -import 'package:chat_interface/connection/encryption/signatures.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/hash.dart'; +import 'package:chat_interface/util/encryption/signatures.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/pages/status/setup/instance_setup.dart'; import 'package:chat_interface/pages/status/setup/setup_page.dart'; @@ -51,9 +51,7 @@ class KeySetup extends ConnectionStep { final genVaultKey = randomSymmetricKey(); // Set public key on the server - res = await postAuthorizedJSON("/account/keys/public/set", { - "key": packagedPub, - }); + res = await postAuthorizedJSON("/account/keys/public/set", {"key": packagedPub}); if (!res["success"]) { return SetupResponse(error: "key.error"); } @@ -69,9 +67,7 @@ class KeySetup extends ConnectionStep { if (!res["success"]) { return SetupResponse(error: "key.error"); } - res = await postAuthorizedJSON("/account/keys/signature/set", { - "key": packagedSignaturePub, - }); + res = await postAuthorizedJSON("/account/keys/signature/set", {"key": packagedSignaturePub}); if (!res["success"]) { return SetupResponse(error: "key.error"); } @@ -89,19 +85,13 @@ class KeySetup extends ConnectionStep { // Open key synchronization if there are no local keys if (publicKey == null || privateKey == null) { final res = await openKeySynchronization(); - return SetupResponse( - restart: true, - error: res, - ); + return SetupResponse(restart: true, error: res); } // Check if the key is the same as on the server if (res["key"] != publicKey) { final res = await openKeySynchronization(); - return SetupResponse( - restart: true, - error: res, - ); + return SetupResponse(restart: true, error: res); } // Set local key pair @@ -155,9 +145,7 @@ class KeySetup extends ConnectionStep { } // Ask the server whether the request already exists - final json = await postJSON("/account/keys/requests/exists", { - "token": refreshToken, - }); + final json = await postJSON("/account/keys/requests/exists", {"token": refreshToken}); // Check if there was an error if (!json["success"]) { @@ -165,15 +153,17 @@ class KeySetup extends ConnectionStep { } // Go to the key setup page - unawaited(Get.dialog( - KeySetupPage( - signature: signature, - signatureKeyPair: signatureKeyPair, - encryptionKeyPair: encryptionKeyPair, - exists: json["exists"], + unawaited( + Get.dialog( + KeySetupPage( + signature: signature, + signatureKeyPair: signatureKeyPair, + encryptionKeyPair: encryptionKeyPair, + exists: json["exists"], + ), + barrierDismissible: false, ), - barrierDismissible: false, - )); + ); return completer.future; } } @@ -227,6 +217,12 @@ class _KeySetupPageState extends State { super.initState(); } + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return SmoothDialogWindow(controller: controller); @@ -259,25 +255,20 @@ class _KeySynchronizationPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text( - 'key.sync.title'.tr, - style: Get.textTheme.headlineMedium, - textAlign: TextAlign.center, - ), + Text('key.sync.title'.tr, style: Get.textTheme.headlineMedium, textAlign: TextAlign.center), verticalSpacing(sectionSpacing), - Text( - "key.sync.desc".tr, - style: Get.textTheme.bodyMedium, - ), + Text("key.sync.desc".tr, style: Get.textTheme.bodyMedium), verticalSpacing(sectionSpacing), FJElevatedLoadingButton( - loading: false.obs, onTap: () async { final json = await postJSON("/account/keys/requests/check", { "token": refreshToken, - "signature": - signMessage(widget.signatureKeyPair.secretKey, hashSha(widget.signature + packagePublicKey(widget.encryptionKeyPair.publicKey))), - "key": "${packagePublicKey(widget.signatureKeyPair.publicKey)}:${packagePublicKey(widget.encryptionKeyPair.publicKey)}", + "signature": signMessage( + widget.signatureKeyPair.secretKey, + hashSha(widget.signature + packagePublicKey(widget.encryptionKeyPair.publicKey)), + ), + "key": + "${packagePublicKey(widget.signatureKeyPair.publicKey)}:${packagePublicKey(widget.encryptionKeyPair.publicKey)}", }); if (!json["success"]) { @@ -285,11 +276,15 @@ class _KeySynchronizationPageState extends State { return; } - unawaited(widget.controller.transitionTo(KeyCodePage( - encryptionKeyPair: widget.encryptionKeyPair, - signatureKeyPair: widget.signatureKeyPair, - signature: widget.signature, - ))); + unawaited( + widget.controller.transitionTo( + KeyCodePage( + encryptionKeyPair: widget.encryptionKeyPair, + signatureKeyPair: widget.signatureKeyPair, + signature: widget.signature, + ), + ), + ); }, label: "key.sync.ask_device".tr, ), @@ -322,8 +317,12 @@ class _KeyCodePageState extends State { _timer = Timer.periodic(5000.ms, (timer) async { final json = await postJSON("/account/keys/requests/check", { "token": refreshToken, - "signature": signMessage(widget.signatureKeyPair.secretKey, hashSha(widget.signature + packagePublicKey(widget.encryptionKeyPair.publicKey))), - "key": "${packagePublicKey(widget.signatureKeyPair.publicKey)}:${packagePublicKey(widget.encryptionKeyPair.publicKey)}", + "signature": signMessage( + widget.signatureKeyPair.secretKey, + hashSha(widget.signature + packagePublicKey(widget.encryptionKeyPair.publicKey)), + ), + "key": + "${packagePublicKey(widget.signatureKeyPair.publicKey)}:${packagePublicKey(widget.encryptionKeyPair.publicKey)}", }); if (!json["success"]) { @@ -333,7 +332,11 @@ class _KeyCodePageState extends State { // Add all the keys to the database if there is a payload if (json["payload"] != null && json["payload"] != "") { - final payload = decryptAsymmetricAnonymous(widget.encryptionKeyPair.publicKey, widget.encryptionKeyPair.secretKey, json["payload"]); + final payload = decryptAsymmetricAnonymous( + widget.encryptionKeyPair.publicKey, + widget.encryptionKeyPair.secretKey, + json["payload"], + ); if (payload == "") { sendLog("couldn't decrypt message ${json["payload"]}"); return; @@ -368,10 +371,7 @@ class _KeyCodePageState extends State { textAlign: TextAlign.center, ), verticalSpacing(sectionSpacing), - Text( - 'key.code.desc'.tr, - style: Get.textTheme.bodyMedium, - ), + Text('key.code.desc'.tr, style: Get.textTheme.bodyMedium), ], ); } diff --git a/lib/controller/current/steps/profile_step.dart b/lib/controller/current/steps/profile_step.dart index d31c619f..d5c8ba92 100644 --- a/lib/controller/current/steps/profile_step.dart +++ b/lib/controller/current/steps/profile_step.dart @@ -25,10 +25,7 @@ class RefreshTokenStep extends ConnectionStep { String session = getSessionFromJWT(sessionToken); // Refresh token - final body = await postJSON("/account/auth/refresh", { - "session": session, - "token": refreshToken, - }); + final body = await postJSON("/account/auth/refresh", {"session": session, "token": refreshToken}); // Set new token (if refreshed) if (body["success"]) { @@ -43,10 +40,7 @@ class RefreshTokenStep extends ConnectionStep { // Check if the session is not verified if (body["verified"] != null && !body["verified"]) { final res = await KeySetup.openKeySynchronization(); - return SetupResponse( - retryConnection: true, - error: res, - ); + return SetupResponse(retryConnection: true, error: res); } // Check if the account data is invalid (make sure to delete everything) diff --git a/lib/controller/current/steps/stored_actions_step.dart b/lib/controller/current/steps/stored_actions_step.dart index 7e2220f7..00026e0d 100644 --- a/lib/controller/current/steps/stored_actions_step.dart +++ b/lib/controller/current/steps/stored_actions_step.dart @@ -1,4 +1,4 @@ -import 'package:chat_interface/connection/chat/stored_actions_listener.dart'; +import 'package:chat_interface/services/connection/chat/stored_actions_listener.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/util/web.dart'; diff --git a/lib/controller/current/tasks/friend_sync_task.dart b/lib/controller/current/tasks/friend_sync_task.dart index e015f182..80e58aab 100644 --- a/lib/controller/current/tasks/friend_sync_task.dart +++ b/lib/controller/current/tasks/friend_sync_task.dart @@ -1,151 +1,33 @@ -import 'dart:convert'; +import 'dart:async'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/account/friends/requests_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/account/requests_controller.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; -import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/main.dart'; import 'package:chat_interface/controller/current/steps/account_step.dart'; -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:chat_interface/util/web.dart'; -import 'package:drift/drift.dart'; -import 'package:get/get.dart'; -import 'package:sodium_libs/sodium_libs.dart'; -import 'package:sodium_libs/sodium_libs_sumo.dart'; class FriendsSyncTask extends SynchronizationTask { - FriendsSyncTask() : super("loading.friends", const Duration(seconds: 30)); + FriendsSyncTask() : super("loading.friends"); @override Future init() async { // Load requests and friends from database - await Get.find().loadRequests(); - await Get.find().loadFriends(); + await RequestController.loadRequests(); + await FriendController.loadFriends(); + + // Make sure to set the completer to null again + AccountStep.keyCompleter = Completer(); // Add self as friend (without keys) - Get.find().addSelf(); + FriendController.addSelf(); return null; } @override Future refresh() { - return refreshFriendsVault(); + return FriendsVault.refreshFriendsVault(); } @override void onRestart() {} } - -class _FriendsListResponse { - final List requests; - final List requestsSent; - final List friends; - - // Strings lists for later processing - final List friendIds = []; - final List allRequestIds = []; - final List requestIds = []; - final List requestSentIds = []; - - _FriendsListResponse(this.requests, this.requestsSent, this.friends) { - for (var friend in friends) { - friendIds.add(friend.id); - } - for (var request in requests) { - allRequestIds.add(request.id); - requestIds.add(request.id); - } - for (var sentRequest in requestsSent) { - allRequestIds.add(sentRequest.id); - requestSentIds.add(sentRequest.id); - } - } -} - -/// A global boolean that tells you whether the friends vault is currently refreshing or not -final friendsVaultRefreshing = false.obs; - -/// Refresh all friends and load them from the vault (also removes what's not on the server) -Future refreshFriendsVault() async { - if (friendsVaultRefreshing.value) { - sendLog("COLLISION: Friends vault is already refreshing, maybe this should be something worth looking into"); - return null; - } - - friendsVaultRefreshing.value = true; - // Load friends from vault - final json = await postAuthorizedJSON("/account/friends/list", { - "after": 0, - }); - if (!json["success"]) { - friendsVaultRefreshing.value = false; - return "friends.error".tr; - } - - // Parse the JSON (in different isolate) - final res = await sodiumLib.runIsolated( - (sodium, keys, pairs) => _parseFriends(json, sodium, keys[0]), - secureKeys: [vaultKey], - ); - - // Push requests - final controller = Get.find(); - controller.requests.removeWhere((item, rq) => !res.requestIds.contains(item)); - for (var request in res.requests) { - if (controller.requests[request.id] == null) { - controller.addRequest(request); - } - } - controller.requestsSent.removeWhere((item, rq) => !res.requestSentIds.contains(item)); - for (var request in res.requestsSent) { - if (controller.requestsSent[request.id] == null) { - controller.addSentRequest(request); - } - } - await db.request.deleteWhere((t) => t.id.isNotIn(res.allRequestIds.map((e) => e.encode()))); // Remove the other ones that aren't there - - // Push friends - final friendController = Get.find(); - friendController.friends.removeWhere((id, fr) => !res.friendIds.contains(id) && id != StatusController.ownAddress); - for (var friend in res.friends) { - if (friendController.friends[friend.id] == null) { - friendController.add(friend); - } - } - await db.friend.deleteWhere((t) => t.id.isNotIn(res.friendIds.map((e) => e.encode()))); // Remove the other ones that aren't there - - friendsVaultRefreshing.value = false; - return null; -} - -Future<_FriendsListResponse> _parseFriends(Map json, Sodium sodium, SecureKey key) async { - final friends = []; - final requests = []; - final requestsSent = []; - for (var friend in json["friends"]) { - final decrypted = decryptSymmetric(friend["friend"], key, sodium); - final data = jsonDecode(decrypted); - - // Check if request or friend - if (data["rq"]) { - if (data["self"]) { - final rq = Request.fromStoredPayload(data, friend["updated_at"]); - rq.vaultId = friend["id"]; - requestsSent.add(rq); - } else { - final rq = Request.fromStoredPayload(data, friend["updated_at"]); - rq.vaultId = friend["id"]; - requests.add(rq); - } - } else { - final fr = Friend.fromStoredPayload(data, friend["updated_at"]); - fr.vaultId = friend["id"]; - friends.add(fr); - } - } - - return _FriendsListResponse(requests, requestsSent, friends); -} diff --git a/lib/controller/current/tasks/vault_actions.dart b/lib/controller/current/tasks/vault_actions.dart index 04ca6d6d..e49e44d8 100644 --- a/lib/controller/current/tasks/vault_actions.dart +++ b/lib/controller/current/tasks/vault_actions.dart @@ -2,20 +2,22 @@ part of 'vault_sync_task.dart'; /// Remove an entry from the vault (returns null if successful (error otherwise)) Future removeFromVault(String id) async { - final json = await postAuthorizedJSON("/account/vault/remove", { - "id": id, - }); + final json = await postAuthorizedJSON("/account/vault/remove", {"id": id}); if (!json["success"]) { return json["error"]; } + // Notify the vault sync task about the deletion of the entry + ConnectionController.vaultSyncTask.onDeletion(json["tag"], id, json["version"]); + return null; } /// Add a new entry to the vault (payload is encrypted with the public key of the account in the function). /// -/// Returns the vault id in case the request was successful. -Future addToVault(String tag, String payload) async { +/// The first element is an error in case there was one. +/// The second element is the vault id if successfull. +Future<(String?, String?)> addToVault(String tag, String payload) async { final encryptedPayload = encryptSymmetric(payload, vaultKey); final json = await postAuthorizedJSON("/account/vault/add", { @@ -23,14 +25,21 @@ Future addToVault(String tag, String payload) async { "payload": encryptedPayload, }); if (!json["success"]) { - return null; + return (json["error"] as String, null); } - return json["id"]; + // Notify the vault sync task about the new entry + ConnectionController.vaultSyncTask.onUpdateOrInsert( + tag, + VaultEntry(json["id"], tag, json["version"], StatusController.ownAccountId, payload, 0), + json["version"], + ); + + return (null, json["id"] as String); } /// Update an entry in the vault (payload is encrypted with the public key of the account in the function) -Future updateVault(String id, String payload) async { +Future updateVault(String tag, String id, String payload) async { final encryptedPayload = encryptSymmetric(payload, vaultKey); final json = await postAuthorizedJSON("/account/vault/update", { @@ -41,5 +50,12 @@ Future updateVault(String id, String payload) async { return false; } + // Notify the vault sync task about the new entry + ConnectionController.vaultSyncTask.onUpdateOrInsert( + tag, + VaultEntry(id, tag, json["version"], StatusController.ownAccountId, payload, 0), + json["version"], + ); + return true; } diff --git a/lib/controller/current/tasks/vault_sync_task.dart b/lib/controller/current/tasks/vault_sync_task.dart index 1e0d921a..a598cb8f 100644 --- a/lib/controller/current/tasks/vault_sync_task.dart +++ b/lib/controller/current/tasks/vault_sync_task.dart @@ -1,125 +1,169 @@ -import 'dart:convert'; - -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; +import 'dart:async'; + +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/controller/square/shared_space_controller.dart'; +import 'package:chat_interface/services/chat/library_manager.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/chat/vault_versioning_service.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/controller/current/steps/account_step.dart'; -import 'package:chat_interface/database/database.dart'; import 'package:chat_interface/main.dart'; -import 'package:chat_interface/pages/chat/components/library/library_manager.dart'; -import 'package:chat_interface/util/constants.dart'; -import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; -import 'package:drift/drift.dart'; import 'package:get/get.dart'; import 'package:sodium_libs/sodium_libs.dart'; part 'vault_actions.dart'; class VaultSyncTask extends SynchronizationTask { - VaultSyncTask() : super("loading.vault", const Duration(seconds: 30)); + VaultSyncTask() : super("loading.vault"); + + // All vault targets (everything that needs sync from the server vault) + final List targets = [ConversationService(), LibraryManager()]; @override Future init() async { - // Load conversations from the database - final conversationController = Get.find(); - final conversations = await (db.select(db.conversation)..orderBy([(u) => OrderingTerm.asc(u.updatedAt)])).get(); - for (var conversation in conversations) { - await conversationController.add(Conversation.fromData(conversation)); + // Initialize all vault targets + for (var target in targets) { + await target.init(); } return null; } @override Future refresh() async { - // Refresh the regular vault (conversations and stuff) - var error = await refreshVault(); - if (error != null) { - return error; + // Get the latest versions of all the targets + Map versionMap = {}; + for (var target in targets) { + versionMap[target.tag] = await VaultVersioningService.retrieveVersion( + VaultVersioningService.vaultTypeGeneral, + target.tag, + ); + } + + // Synchronize using the endpoint from the server + final json = await postAuthorizedJSON("/account/vault/sync", {"tags": versionMap}); + if (!json["success"]) { + return json["error"]; + } + + // Parse all of the entries + final (deleted, newEntries, newVersions) = await sodiumLib.runIsolated((sodium, keys, pairs) { + // Sort the entries into deleted ones and new ones per tag + var deleted = >{}; + var newEntries = >{}; + for (var unparsedEntry in json["entries"]) { + final entry = VaultEntry.fromJson(unparsedEntry); + + // Increment the version of the tag in case increased + if (entry.version > versionMap[entry.tag]!) { + versionMap[entry.tag] = entry.version; + } + + if (unparsedEntry["deleted"] == true) { + // Create a new deleted list or add if list already there + if (deleted[entry.tag] == null) { + deleted[entry.tag] = [entry.id]; + } else { + deleted[entry.tag]!.add(entry.id); + } + } else { + // Decrypt payload and add to list of new entries + entry.payload = decryptSymmetric(entry.payload, keys[0], sodium); + if (newEntries[entry.tag] == null) { + newEntries[entry.tag] = [entry]; + } else { + newEntries[entry.tag]!.add(entry); + } + } + } + + // Return both lists to the outside + return (deleted, newEntries, versionMap); + }, secureKeys: [vaultKey]); + + // Save all the new versions + for (var target in targets) { + unawaited( + VaultVersioningService.storeOrUpdateVersion( + VaultVersioningService.vaultTypeGeneral, + target.tag, + newVersions[target.tag]!, + ), + ); + } + + // Notify the vault targets about the changes + for (var target in targets) { + target.processEntries(deleted[target.tag] ?? [], newEntries[target.tag] ?? []); + } + + return null; + } + + /// Called by vault_actions when a new entry is added or updated + void onUpdateOrInsert(String tag, VaultEntry entry, int version) { + final target = targets.firstWhereOrNull((target) => target.tag == tag); + if (target == null) { + return; } - // Refresh the library (gifs, saved images, etc.) - error = await LibraryManager.refreshEntries(); - return error; + // Let the target process the new entry + target.processEntries([], [entry]); + unawaited( + VaultVersioningService.storeOrUpdateVersion(VaultVersioningService.vaultTypeGeneral, target.tag, version), + ); + } + + /// Called by vault_actions when an entry is deleted + void onDeletion(String tag, String id, int version) { + final target = targets.firstWhereOrNull((target) => target.tag == tag); + if (target == null) { + return; + } + + // Let the target process the deletion + target.processEntries([id], []); + unawaited( + VaultVersioningService.storeOrUpdateVersion(VaultVersioningService.vaultTypeGeneral, target.tag, version), + ); } @override - void onRestart() {} + void onRestart() { + SharedSpaceController.clearAll(); + } +} + +abstract class VaultTarget { + final String tag; + + VaultTarget(this.tag); + + /// Called on intialization by the vault sync task + Future init() async {} + + /// Called when the vault is refreshed with the new entries and the deleted ones + void processEntries(List deleted, List newEntries); } class VaultEntry { final String id; final String tag; final String account; - final String payload; + final int version; + String payload; final int updatedAt; bool error = false; - VaultEntry(this.id, this.tag, this.account, this.payload, this.updatedAt); + VaultEntry(this.id, this.tag, this.version, this.account, this.payload, this.updatedAt); VaultEntry.fromJson(Map json) - : id = json["id"], - tag = json["tag"], - account = json["account"], - payload = json["payload"], - updatedAt = json["updated_at"]; + : id = json["id"], + tag = json["tag"], + version = json["version"], + account = json["account"], + payload = json["payload"], + updatedAt = json["updated_at"]; String decryptedPayload([SecureKey? key, Sodium? sodium]) => decryptSymmetric(payload, key ?? vaultKey, sodium); } - -// Returns an error string (null if successful) -Future refreshVault() async { - // Load conversations - final json = await postAuthorizedJSON("/account/vault/list", { - "after": 0, // Unix - "tag": Constants.vaultConversationTag, - }); - if (!json["success"]) { - return json["error"]; - } - - sendLog("loading.."); - sendLog(json["entries"].length); - - // Run decryption and decoding in a separate isolate - final (conversations, ids) = await sodiumLib.runIsolated((sodium, keys, pairs) { - var list = []; - var ids = []; - for (var unparsedEntry in json["entries"]) { - final entry = VaultEntry.fromJson(unparsedEntry); - final decrypted = decryptSymmetric(entry.payload, keys[0], sodium); - final decoded = jsonDecode(decrypted); - final conv = Conversation.fromJson(decoded, entry.id); - list.add(conv); - ids.add(conv.id); - } - - return (list, ids); - }, secureKeys: [vaultKey]); - - // Delete all old conversations in the cache - final messageController = Get.find(); - final controller = Get.find(); - controller.conversations.removeWhere((id, conv) { - final remove = !ids.contains(id); - if (remove) { - controller.order.remove(id); - messageController.unselectConversation(id: id); - } - return remove; - }); - - // Add all new conversations - for (var conversation in conversations) { - if (controller.conversations[conversation.id] == null) { - await controller.addFromVault(conversation); - } - } - - // Delete all old conversations from the database - final stringIds = ids.map((id) => id.encode()); - await db.conversation.deleteWhere((tbl) => tbl.id.isNotIn(stringIds)); - await db.member.deleteWhere((tbl) => tbl.conversationId.isNotIn(stringIds)); - - return null; -} diff --git a/lib/controller/spaces/ringing_manager.dart b/lib/controller/spaces/ringing_manager.dart index f4911560..ba9106c9 100644 --- a/lib/controller/spaces/ringing_manager.dart +++ b/lib/controller/spaces/ringing_manager.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/spaces/space_container.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/chat/components/conversations/conversation_ringing_window.dart'; import 'package:chat_interface/pages/settings/app/general_settings.dart'; @@ -33,10 +33,7 @@ class RingingManager { }); // Wait for the dialog to be closed and potentially stop the ringtone after that - await Get.dialog(ConversationRingingWindow( - conversation: conversation, - container: container, - )); + await Get.dialog(ConversationRingingWindow(conversation: conversation, container: container)); await stopRingtone(); } @@ -64,20 +61,21 @@ class RingingManager { /// Checks whether the client can currently be ringed static Future _canRing() async { // Don't ring when the setting is turned off - if (!Get.find().settings[GeneralSettings.ringOnInvite]!.getValue()) { + if (!SettingController.settings[GeneralSettings.ringOnInvite]!.getValue()) { return false; } // Only ring when the status is online or away - final doNotDisturb = Get.find().type.value == statusDoNotDisturb || Get.find().type.value == statusOffline; - if (doNotDisturb && !Get.find().settings[GeneralSettings.soundsDoNotDisturb]!.getValue()) { + final doNotDisturb = + StatusController.type.value == statusDoNotDisturb || StatusController.type.value == statusOffline; + if (doNotDisturb && !SettingController.settings[GeneralSettings.soundsDoNotDisturb]!.getValue()) { return false; } // Check if ring should only be played when Liphium is minimized final inTray = await windowManager.isVisible(); - final ignoreTray = Get.find().settings[GeneralSettings.ringIgnoreTray]!.getValue(); - final playOnlyInTray = Get.find().settings[GeneralSettings.soundsOnlyWhenTray]!.getValue(); + final ignoreTray = SettingController.settings[GeneralSettings.ringIgnoreTray]!.getValue(); + final playOnlyInTray = SettingController.settings[GeneralSettings.soundsOnlyWhenTray]!.getValue(); if (inTray && playOnlyInTray && !ignoreTray) { return false; } @@ -88,19 +86,20 @@ class RingingManager { /// Checks whether a notification sound can currently be played static Future _canPlayNotificationSound() async { // Don't play a sound when the setting is turned off - if (!Get.find().settings[GeneralSettings.soundsEnabled]!.getValue()) { + if (!SettingController.settings[GeneralSettings.soundsEnabled]!.getValue()) { return false; } // Check if it should play a sound when the status is do not disturb - final doNotDisturb = Get.find().type.value == statusDoNotDisturb || Get.find().type.value == statusOffline; - if (doNotDisturb && !Get.find().settings[GeneralSettings.soundsDoNotDisturb]!.getValue()) { + final doNotDisturb = + StatusController.type.value == statusDoNotDisturb || StatusController.type.value == statusOffline; + if (doNotDisturb && !SettingController.settings[GeneralSettings.soundsDoNotDisturb]!.getValue()) { return false; } // Check if notification sound should only be played when in tray final inTray = await windowManager.isVisible(); - final playOnlyInTray = Get.find().settings[GeneralSettings.soundsOnlyWhenTray]!.getValue(); + final playOnlyInTray = SettingController.settings[GeneralSettings.soundsOnlyWhenTray]!.getValue(); if (inTray && playOnlyInTray) { return false; } diff --git a/lib/controller/spaces/space_controller.dart b/lib/controller/spaces/space_controller.dart new file mode 100644 index 00000000..a372542f --- /dev/null +++ b/lib/controller/spaces/space_controller.dart @@ -0,0 +1,236 @@ +import 'dart:async'; + +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/conversation/system_messages.dart'; +import 'package:chat_interface/controller/spaces/ringing_manager.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; +import 'package:chat_interface/controller/conversation/message_provider.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; +import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/main.dart'; +import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; +import 'package:chat_interface/services/spaces/space_message_provider.dart'; +import 'package:chat_interface/services/spaces/space_service.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; +import 'package:sodium_libs/sodium_libs.dart'; +import 'package:window_manager/window_manager.dart'; + +bool areSpacesSupported = !isWeb && !GetPlatform.isMobile; + +class SpaceController { + //* Call status + static final spaceLoading = signal(false); + static final connected = signal(false); + static final start = signal(DateTime.now()); + static final currentTab = signal(SpaceTabType.space.index); + static int _prevTab = SpaceTabType.space.index; + static bool shouldSwitchToPage = true; + + //* Space information + static String? domain; + static final id = signal(null); + static SecureKey? key; + + //* Call layout + static final chatOpen = signal(true); + static final fullScreen = signal(false); + static final sidebarTabType = signal(SpaceSidebarTabType.chat.index); + + // Spaces messaging + static SpacesMessageProvider provider = SpacesMessageProvider(); + + /// Update the start time for the Space + static void updateStartDate(DateTime newStart) { + start.value = newStart; + } + + /// Toggle full screen in the Space + static void toggleFullScreen() { + fullScreen.value = !fullScreen.value; + if (fullScreen.value) { + windowManager.setFullScreen(true); + } else { + windowManager.setFullScreen(false); + } + } + + /// Switch to a tab programatically + static void switchToTabAndChange(SpaceTabType type) { + currentTab.value = type.index; + switchToTab(type); + } + + /// Event that is called after the tab switch was done through the selector + static void switchToTab(SpaceTabType type) { + if (type.index == _prevTab) { + return; + } + + // If the previous tab was the table, disconnect from the event stream + if (_prevTab == SpaceTabType.table.index) { + TabletopController.closeTableTab(); + } + + // If the current tab is a table tab, connect to the event stream + if (type == SpaceTabType.table) { + TabletopController.openTableTab(); + } + _prevTab = currentTab.value; + } + + /// Create a space (publish says whether or not it should be published through status). + /// + /// Returns an error if there was one. + static Future createSpace(bool publish, {bool dialog = true, bool openPage = true}) async { + shouldSwitchToPage = openPage; + spaceLoading.value = true; + final (container, error) = await SpaceService.createSpace(); + spaceLoading.value = false; + if (error != null) { + if (dialog) { + showErrorPopup("error", error); + } + return error; + } + + if (publish) { + unawaited(StatusController.share(container!)); + } + return null; + } + + /// Create a space and connect to it (with sending an invite to the message provider) + static Future createAndConnect(MessageProvider provider, {bool openPage = true}) async { + shouldSwitchToPage = openPage; + if (!areSpacesSupported) { + showNotSupported(); + return; + } + + spaceLoading.value = true; + final (container, error) = await SpaceService.createSpace(); + spaceLoading.value = false; + + if (error != null) { + showErrorPopup("error", error); + return; + } + + unawaited(provider.sendMessage(signal(false), MessageType.call, [], container!.toInviteJson(), "")); + } + + static Future join(SpaceConnectionContainer container) async { + spaceLoading.value = true; + + // Connect to a Space + final error = await SpaceService.connectToSpace(container.node, container.roomId, container.key); + spaceLoading.value = false; + if (error != null) { + showErrorPopup("error", error); + } + } + + /// Function called by the space service to tell this controller about the connection + static void onConnect(String server, String spaceId, SecureKey spaceKey) { + // Load information from space container + domain = server; + key = spaceKey; + + // Load the first messages of the Space chat + provider.loadNewMessagesTop(date: DateTime.now().millisecondsSinceEpoch); + + // Update all the state in one go + batch(() { + // Set all the state in the controller required for the UI + connected.value = true; + chatOpen.value = true; + id.value = spaceId; + + // Switch to the proper tab for the space + switchToTab(SpaceTabType.space); + sidebarTabType.value = SpaceSidebarTabType.chat.index; + }); + } + + static void showNotSupported() { + showErrorPopup("spaces.not_supported", "spaces.not_supported.desc".tr); + } + + static void inviteToCall(MessageProvider provider) { + provider.sendMessage(signal(false), MessageType.call, [], getContainer().toInviteJson(), ""); + } + + /// Get a [SpaceConnectionContainer] for the current Space. + static SpaceConnectionContainer getContainer() { + return SpaceConnectionContainer(domain!, id.value!, key!, null); + } + + /// Leave the space. + static Future leaveSpace({error = false}) async { + // Disconnect from the space + SpaceConnection.disconnect(); + + // Update the state to reflect the change + batch(() { + connected.value = false; + id.value = null; + }); + key = null; + domain = null; + + // Reset the tab + currentTab.value = SpaceTabType.space.index; + _prevTab = SpaceTabType.space.index; + + // Reset the message provider + provider = SpacesMessageProvider(); + + // Show an error if there was one + if (!error && SidebarController.currentOpenTab.peek().type == SidebarTabType.space) { + SidebarController.openTab(DefaultSidebarTab()); + } + } + + /// Add a message to the Spaces chat. + /// + /// Also plays a notification sound if desired by the user. + static void addMessage(Message message) { + // Play a notification sound when a new message arrives + RingingManager.playNotificationSound(); + + // Check if it is a system message and if it should be rendered or not + if (message.type == MessageType.system) { + if (SystemMessages.messages[message.content]?.render == true) { + provider.addMessageToBottom(message); + } + } else { + // Store normal type of message + provider.addMessageToBottom(message); + } + + // Handle system messages + if (message.type == MessageType.system) { + SystemMessages.messages[message.content]?.handle(message, provider); + } + } +} + +enum SpaceTabType { + space("spaces.tab.space"), + table("spaces.tab.table"); + + final String name; + + const SpaceTabType(this.name); +} + +enum SpaceSidebarTabType { + chat("spaces.sidebar.chat"), + people("spaces.sidebar.people"); + + final String name; + const SpaceSidebarTabType(this.name); +} diff --git a/lib/controller/spaces/spaces_controller.dart b/lib/controller/spaces/spaces_controller.dart deleted file mode 100644 index 691adc31..00000000 --- a/lib/controller/spaces/spaces_controller.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'dart:async'; - -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/spaces/space_connection.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/conversation/message_provider.dart'; -import 'package:chat_interface/controller/spaces/space_container.dart'; -import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_message_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; -import 'package:chat_interface/controller/spaces/warp_controller.dart'; -import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/main.dart'; -import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; -import 'package:chat_interface/services/spaces/space_service.dart'; -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:chat_interface/util/popups.dart'; -import 'package:chat_interface/util/web.dart'; -import 'package:get/get.dart'; -import 'package:sodium_libs/sodium_libs.dart'; -import 'package:window_manager/window_manager.dart'; - -bool areSpacesSupported = !isWeb && !GetPlatform.isMobile; - -class SpacesController extends GetxController { - //* Call status - final inSpace = false.obs; - final spaceLoading = false.obs; - final connected = false.obs; - final start = DateTime.now().obs; - final currentTab = SpaceTabType.space.index.obs; - int _prevTab = SpaceTabType.space.index; - - //* Space information - static String? currentDomain; - final id = "".obs; - static SecureKey? key; - - //* Call layout - final chatOpen = true.obs; - final hideSidebar = false.obs; - final fullScreen = false.obs; - final sidebarTabType = SpaceSidebarTabType.chat.index.obs; - - void toggleFullScreen() { - fullScreen.toggle(); - if (fullScreen.value) { - windowManager.setFullScreen(true); - } else { - windowManager.setFullScreen(false); - } - } - - /// Switch to a tab programatically - void switchToTabAndChange(SpaceTabType type) { - currentTab.value = type.index; - switchToTab(type); - } - - /// Event that is called after the tab switch was done through the selector - void switchToTab(SpaceTabType type) { - if (type.index == _prevTab) { - return; - } - - // If the previous tab was the table, disconnect from the event stream - if (_prevTab == SpaceTabType.table.index) { - Get.find().closeTableTab(); - } - - // If the current tab is a table tab, connect to the event stream - if (type == SpaceTabType.table) { - Get.find().openTableTab(); - } - _prevTab = currentTab.value; - } - - /// Create a space (publish says whether or not it should be published through status) - Future createSpace(bool publish) async { - spaceLoading.value = true; - final (container, error) = await SpaceService.createSpace(); - spaceLoading.value = false; - if (error != null) { - showErrorPopup("error", error); - return; - } - - if (publish) { - unawaited(Get.find().share(container!)); - } - } - - Future createAndConnect(MessageProvider provider) async { - if (!areSpacesSupported) { - showNotSupported(); - return; - } - - spaceLoading.value = true; - final (container, error) = await SpaceService.createSpace(); - spaceLoading.value = false; - - if (error != null) { - showErrorPopup("error", error); - return; - } - - unawaited(provider.sendMessage(spaceLoading, MessageType.call, [], container!.toInviteJson(), "")); - } - - Future join(SpaceConnectionContainer container) async { - spaceLoading.value = true; - - // Connect to a Space - final error = await SpaceService.connectToSpace(container.node, container.roomId, container.key); - spaceLoading.value = false; - if (error != null) { - showErrorPopup("error", error); - } - } - - /// Function called by the space service to tell this controller about the connection - void onConnect(String spaceId, SecureKey spaceKey) { - sendLog("connected"); - - // Load information from space container - id.value = spaceId; - key = spaceKey; - switchToTab(SpaceTabType.space); - sidebarTabType.value = SpaceSidebarTabType.chat.index; - - // Open the screen - Get.find().unselectConversation(); - Get.find().openTab(OpenTabType.space); - Get.find().open(); - - // Reset everything on the table - Get.find().resetControllerState(); - - // Initialize the member controller - Get.find().onConnect(spaceKey); - - connected.value = true; - inSpace.value = true; - chatOpen.value = true; - } - - void showNotSupported() { - showErrorPopup("spaces.not_supported", "spaces.not_supported.desc".tr); - } - - void inviteToCall(MessageProvider provider) { - provider.sendMessage(spaceLoading, MessageType.call, [], getContainer().toInviteJson(), ""); - } - - SpaceConnectionContainer getContainer() { - return SpaceConnectionContainer(currentDomain!, id.value, key!, null); - } - - Future leaveCall({error = false}) async { - inSpace.value = false; - connected.value = false; - id.value = ""; - spaceConnector.disconnect(); - - // Tell other controllers about it - Get.find().stopSharing(); - Get.find().onDisconnect(); - Get.find().resetControllerState(); - Get.find().resetControllerState(); - Get.find().clearProvider(); - - if (!error) { - unawaited(Get.offAll(getChatPage(), transition: Transition.fadeIn)); - Get.find().openTab(OpenTabType.conversation); - } - } -} - -enum SpaceTabType { - space("spaces.tab.space"), - table("spaces.tab.table"); - - final String name; - - const SpaceTabType(this.name); -} - -enum SpaceSidebarTabType { - chat("spaces.sidebar.chat"), - people("spaces.sidebar.people"); - - final String name; - const SpaceSidebarTabType(this.name); -} - -class SpaceInfo { - late bool exists; - bool error = false; - late DateTime start; - final List friends = []; - late final List members; - - SpaceInfo(this.start, this.members) { - error = false; - exists = true; - final controller = Get.find(); - for (var member in members) { - final friend = controller.friends[member]; - if (friend != null) friends.add(friend); - } - } - - SpaceInfo.fromJson(SpaceConnectionContainer container, Map json) { - start = DateTime.fromMillisecondsSinceEpoch(json["start"]); - members = List.from(json["members"].map((e) => LPHAddress.from(decryptSymmetric(e, container.key)))); - exists = true; - - final controller = Get.find(); - for (var member in members) { - final friend = controller.friends[member]; - if (friend != null) friends.add(friend); - } - } - - SpaceInfo.notLoaded({bool wasError = false}) { - exists = false; - error = wasError; - members = []; - } -} diff --git a/lib/controller/spaces/spaces_member_controller.dart b/lib/controller/spaces/spaces_member_controller.dart index 7f1fbe5c..b9819799 100644 --- a/lib/controller/spaces/spaces_member_controller.dart +++ b/lib/controller/spaces/spaces_member_controller.dart @@ -1,67 +1,87 @@ -import 'package:chat_interface/connection/encryption/signatures.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/account/unknown_controller.dart'; +import 'package:chat_interface/util/encryption/signatures.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/services/chat/unknown_service.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/services/spaces/space_service.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; -import 'package:get/get.dart'; -import 'package:sodium_libs/sodium_libs.dart'; +import 'package:signals/signals_flutter.dart'; + +class SpaceMemberController { + static final membersLoading = signal(false); -class SpaceMemberController extends GetxController { - SecureKey? key; - final membersLoading = false.obs; // Client ID -> SpaceMember - final members = {}.obs; + static final members = mapSignal({}); + // This is for caching only the account ids for message decryption - final memberIds = {}; // Client id -> Account id - static String ownId = ""; + static final memberIds = {}; // Client id -> Account id + static String _ownId = ""; - void onMembersChanged(List members) { - final statusController = Get.find(); + /// Parse a member list and add it to the members map. + static void onMembersChanged(List newMembers) { final membersFound = []; - for (var member in members) { - final clientId = member["id"]; - final decrypted = decryptSymmetric(member["data"], key!); - final address = LPHAddress.from(decrypted); - if (address == StatusController.ownAddress) { - SpaceMemberController.ownId = clientId; - } - membersFound.add(clientId); - - // Add the member to the list if they're not in it yet - if (this.members[clientId] == null) { - this.members[clientId] = SpaceMember( - Get.find().friends[address] ?? - (address == StatusController.ownAddress ? Friend.me(statusController) : Friend.unknown(address)), - clientId, - ); - this.members[clientId]!.verifySignature(member["sign"]); + // Start a batch to make sure members only updates after all the changes have been made + batch(() { + for (var member in newMembers) { + final clientId = member["id"]; + final decrypted = decryptSymmetric(member["data"], SpaceController.key!); + final address = LPHAddress.from(decrypted); + if (address == StatusController.ownAddress) { + _ownId = clientId; + } + membersFound.add(clientId); + + // Add the member to the list if they're not in it yet + if (members[clientId] == null) { + members[clientId] = SpaceMember( + FriendController.friends[address] ?? + (address == StatusController.ownAddress ? Friend.me() : Friend.unknown(address)), + clientId, + ); + members[clientId]!.verifySignature(member["sign"]); + } + + // Update their state + members[clientId]!.connectedToStudio.value = member["st"]; + members[clientId]!.isMuted.value = member["mute"]; + members[clientId]!.isDeafened.value = member["deaf"]; + if (member["mute"] || member["deaf"] || !member["st"]) { + members[clientId]!.talking.value = false; + } + + // Cache the account id + memberIds[clientId] = address; } - // Cache the account id - memberIds[clientId] = address; - } + // Remove everyone who left the space + members.removeWhere((key, value) => !membersFound.contains(key)); - // Remove everyone who left the space - this.members.removeWhere((key, value) => !membersFound.contains(key)); - membersLoading.value = false; + // Update the signals + membersLoading.value = false; + }); } - Future onConnect(SecureKey key) async { - this.key = key; + /// Handle a change in talking state for a member + static void handleTalkingState(String id, bool talking) { + members[id]?.talking.value = talking; } - void onDisconnect() { - membersLoading.value = true; - members.clear(); + /// Get the id of the current client + static String getOwnId() { + return _ownId; + } + + /// Get the Space member for a client id + static SpaceMember? getMember(String clientId) { + return members[clientId]; } - bool isLocalDeafened() { - return members[ownId]!.isDeafened.value; + static void onDisconnect() { + membersLoading.value = true; + members.clear(); } } @@ -69,17 +89,19 @@ class SpaceMember { final String id; final Friend friend; - // We'll just keep this here for when Lightwire is finished - final isSpeaking = false.obs; - final isMuted = false.obs; - final isDeafened = false.obs; - final verified = true.obs; + // Lightwire and Studio state + final talking = signal(false); + DateTime? lastPacket; + final connectedToStudio = signal(false); + final isMuted = signal(false); + final isDeafened = signal(false); + final verified = signal(true); SpaceMember(this.friend, this.id); Future verifySignature(String signature) async { // Load the guy's profile - final profile = await Get.find().loadUnknownProfile(friend.id); + final profile = await UnknownService.loadUnknownProfile(friend.id); if (profile == null) { verified.value = false; sendLog("couldn't find profile: identity of space member is uncertain"); @@ -88,7 +110,7 @@ class SpaceMember { // Verify the signature try { - final message = SpaceService.craftSignature(Get.find().id.value, id, friend.id.encode()); + final message = SpaceService.craftSignature(SpaceController.id.value!, id, friend.id.encode()); verified.value = checkSignature(signature, profile.signatureKey, message); sendLog("space member verified: ${verified.value}"); } catch (e) { diff --git a/lib/controller/spaces/studio/studio_controller.dart b/lib/controller/spaces/studio/studio_controller.dart new file mode 100644 index 00000000..440370f2 --- /dev/null +++ b/lib/controller/spaces/studio/studio_controller.dart @@ -0,0 +1,102 @@ +import 'package:chat_interface/services/spaces/studio/studio_connection.dart'; +import 'package:chat_interface/services/spaces/studio/studio_service.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:signals/signals_flutter.dart'; + +class StudioController { + static StudioConnection? _connection; + + // State for the UI + static final connecting = signal(false); + static final connectionError = signal(""); + static final connected = signal(false); + + // Media controls + static final videoEnabled = signal(false); + + // Lightwire / audio state + static final audioStateLoading = signal(false); + static final audioMuted = signal(true); + static final audioDeafened = signal(false); + + /// Connect to Studio. + /// + /// Returns an error if there was one. + static Future connectToStudio() async { + connecting.value = true; + + // Connect to Studio using the service + final (connection, error) = await StudioService.connectToStudio(); + if (error != null) { + batch(() { + connectionError.value = error; + connecting.value = false; + }); + return; + } + + // Set all the state + _connection = connection; + batch(() { + connecting.value = false; + connected.value = true; + }); + } + + static void resetControllerState() { + _connection?.close(); + _connection = null; + batch(() { + connected.value = false; + connecting.value = false; + videoEnabled.value = false; + audioMuted.value = true; + audioStateLoading.value = false; + audioDeafened.value = false; + }); + } + + /// Called by the service when Studio gets disconnected. + static void handleDisconnect() { + resetControllerState(); + } + + /// Toggle the audio mute state + static Future toggleMute() async { + await _updateAudioState(muted: !audioMuted.peek()); + } + + /// Toggle the audio deafened state + static Future toggleDeafened() async { + await _updateAudioState(deafened: !audioDeafened.peek()); + } + + /// Update the current audio state on the server. + static Future _updateAudioState({bool? muted, bool? deafened}) async { + if (audioStateLoading.peek()) { + return; + } + audioStateLoading.value = true; + + // Send the action + final error = await StudioService.updateAudioState(muted: muted, deafened: deafened); + if (error != null) { + showErrorPopup("error", error); + return; + } + + // Update the states locally + batch(() { + audioMuted.value = muted ?? audioMuted.peek(); + audioDeafened.value = deafened ?? audioDeafened.peek(); + audioStateLoading.value = false; + }); + } + + /// Get the connection to Studio. + /// + /// Should only be accessed by services. + static StudioConnection? getConnection() { + return _connection; + } +} diff --git a/lib/controller/spaces/studio/studio_device_manager.dart b/lib/controller/spaces/studio/studio_device_manager.dart new file mode 100644 index 00000000..be814b5d --- /dev/null +++ b/lib/controller/spaces/studio/studio_device_manager.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:chat_interface/pages/settings/app/audio_settings.dart'; +import 'package:chat_interface/pages/settings/components/list_selection.dart'; +import 'package:chat_interface/src/rust/api/audio_devices.dart' as libdevices; +import 'package:flutter/material.dart'; +import 'package:get/get_utils/src/extensions/export.dart'; +import 'package:signals/signals_flutter.dart'; + +class StudioDeviceManager { + // State for the microphone selector + static final microphones = listSignal([]); + static final selectedMicrophone = computed(() { + // Try to find the currently selected device + final selected = AudioSettings.microphone.getValue(); + int i = 0; + for (var mic in microphones.value) { + // If it's currently selected, return the index + if (mic.label == selected) { + return i; + } + i++; + } + + // Return index 0 (default microphone) + return 0; + }); + + // State for the output device selector + static final outputDevices = listSignal([]); + static final selectedOutputDevice = computed(() { + // Try to find the currently selected output device + final selected = AudioSettings.outputDevice.getValue(); + int i = 0; + for (var device in outputDevices.value) { + // If it's currently selected, return the index + if (device.label == selected) { + return i; + } + i++; + } + + // Return index 0 (default output device) + return 0; + }); + + static Timer? _timer; + static void init() { + _timer?.cancel(); + _timer = Timer.periodic(10.seconds, (timer) { + _updateMicrophones(); + _updateOutputDevices(); + }); + } + + /// Convert all the output devices from libspaceship to selectable items for the selector + static Future _updateOutputDevices() async { + // Add the default output device as a first option + final defaultDevice = await libdevices.getDefaultOutputDevice(); + if (outputDevices.disposed) { + return; + } + final newList = [SelectableItem("Default (${defaultDevice.name})", Icons.speaker)]; + + // Add all the other devices + for (var device in await libdevices.getOutputDevices()) { + newList.add(SelectableItem(device.name, Icons.speaker)); + } + + // Only update in case necessary + if (outputDevices.value != newList) { + outputDevices.value = newList; + } + } + + /// Convert all the microphones from libspaceship to selectable items for the selector + static Future _updateMicrophones() async { + // Add the default microphone as a first option + final defaultMicrophone = await libdevices.getDefaultInputDevice(); + if (microphones.disposed) { + return; + } + final newList = [SelectableItem("Default (${defaultMicrophone.name})", Icons.mic)]; + + // Add all the other microphones + for (var mic in await libdevices.getInputDevices()) { + newList.add(SelectableItem(mic.name, Icons.mic)); + } + + // Only update in case necessary + if (microphones.value != newList) { + microphones.value = newList; + } + } +} diff --git a/lib/controller/spaces/studio/studio_track_controller.dart b/lib/controller/spaces/studio/studio_track_controller.dart new file mode 100644 index 00000000..a75e5c2d --- /dev/null +++ b/lib/controller/spaces/studio/studio_track_controller.dart @@ -0,0 +1,70 @@ +import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:signals/signals_flutter.dart'; + +class StudioTrackController { + /// All the tracks published by the server. + /// + /// Updated in real time through the event stream. + static final tracks = mapSignal({}); + + /// Update a track or register it if it's not there yet. + static void updateOrRegisterTrack(StudioTrack track) { + // If the track is already registed, update it. + if (tracks[track.id] != null) { + tracks[track.id]!.takeUpdate( + paused: track.paused.peek(), + channels: track.channels.peek(), + subscribers: track.channels.peek(), + ); + return; + } + + sendLog("new track: ${track.id}"); + + // Register as a new track + tracks[track.id] = track; + } + + /// Delete a track from the controller. + static void deleteTrack(String id) { + tracks.remove(id); + } + + /// Reset all of the controller state (when the user disconnects from the Space) + static void handleDisconnect() { + tracks.clear(); + } +} + +/// A track published by Studio. +class StudioTrack { + final String id; + final SpaceMember publisher; + final paused = signal(false); + final channels = listSignal([]); + final subscribers = listSignal([]); + + StudioTrack({ + required this.id, + required this.publisher, + required bool paused, + required List channels, + required List subscribers, + }) { + this.paused.value = paused; + } + + /// Update the track but only set what's needed. + void takeUpdate({bool? paused, List? channels, List? subscribers}) { + if (paused != null && this.paused.peek() != paused) { + this.paused.value = paused; + } + if (channels != null && this.channels.peek() != channels) { + this.channels.value = channels; + } + if (subscribers != null && this.subscribers.peek() != subscribers) { + this.subscribers.value = subscribers; + } + } +} diff --git a/lib/controller/spaces/tabletop/tabletop_controller.dart b/lib/controller/spaces/tabletop/tabletop_controller.dart index d1e0e18e..3133b77d 100644 --- a/lib/controller/spaces/tabletop/tabletop_controller.dart +++ b/lib/controller/spaces/tabletop/tabletop_controller.dart @@ -1,56 +1,54 @@ import 'dart:async'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/messaging.dart'; -import 'package:chat_interface/connection/spaces/space_connection.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/tabletop/tabletop_cursor.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_card.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_cursor.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_deck.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_inventory.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_text.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_card.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_inventory.dart'; import 'package:chat_interface/pages/settings/town/tabletop_settings.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class TabletopController extends GetxController { - final loading = false.obs; +class TabletopController { + static final loading = signal(false); /// Currently held object - TableObject? heldObject; - bool movingAllowed = false; - Offset? originalHeldObjectPosition; - bool cancelledHolding = false; - - List hoveringObjects = []; - InventoryObject? inventory; - final orderSorted = []; - final objectOrder = {}; - final objects = {}; - final cursors = {}.obs; // Other users cursors + static TableObject? heldObject; + static bool movingAllowed = false; + static Offset? originalHeldObjectPosition; + static bool cancelledHolding = false; + + static List hoveringObjects = []; + static InventoryObject? inventory; + static final orderSorted = []; + static final objectOrder = {}; + static final objects = {}; + static final cursors = mapSignal({}); // Other users cursors /// The rate at which the table is updated (to the server) static const tickRate = 20; - Timer? _ticker; - Offset? _lastMousePos; - Offset mousePos = const Offset(0, 0); - Offset mousePosUnmodified = const Offset(0, 0); - Offset globalCanvasPosition = const Offset(0, 0); + static Timer? _ticker; + static Offset? _lastMousePos; + static Offset mousePos = const Offset(0, 0); + static Offset mousePosUnmodified = const Offset(0, 0); + static Offset globalCanvasPosition = const Offset(0, 0); // Developer options - final disableCursorSending = false.obs; + static final disableCursorSending = signal(false); // Movement of the canvas - Offset canvasOffset = const Offset(0, 0); - double canvasZoom = 0.5; - final canvasRotation = 0.0.obs; + static Offset canvasOffset = const Offset(0, 0); + static double canvasZoom = 0.5; + static final canvasRotation = signal(0.0); /// Reset the entire state of the controller (on every call start) - void resetControllerState() { + static void resetControllerState() { loading.value = false; heldObject = null; @@ -76,9 +74,9 @@ class TabletopController extends GetxController { } /// Called when the tabletop tab is opened (to receive events again) - void openTableTab() { + static void openTableTab() { loading.value = true; - spaceConnector.sendAction( + SpaceConnection.spaceConnector!.sendAction( ServerAction("table_enable", {}), handler: (event) { loading.value = false; @@ -97,14 +95,14 @@ class TabletopController extends GetxController { } /// Called when the tabletop tab is closed (to disable events) - void closeTableTab() { + static void closeTableTab() { objects.clear(); objectOrder.clear(); _ticker?.cancel(); hoveringObjects.clear(); cursors.clear(); loading.value = true; - spaceConnector.sendAction( + SpaceConnection.spaceConnector!.sendAction( ServerAction("table_disable", {}), handler: (event) { loading.value = false; @@ -118,11 +116,11 @@ class TabletopController extends GetxController { } /// Called every tick - void _handleTableTick() { + static void _handleTableTick() { // Send the location of the held object if (heldObject != null) { if (movingAllowed) { - spaceConnector.sendAction( + SpaceConnection.spaceConnector!.sendAction( ServerAction("tobj_move", { "id": heldObject!.id, "x": heldObject!.location.dx, @@ -140,52 +138,33 @@ class TabletopController extends GetxController { // Send mouse position if available if (_lastMousePos != mousePos && !disableCursorSending.value) { - spaceConnector.sendAction(ServerAction("tc_move", { - "x": mousePos.dx, - "y": mousePos.dy, - "c": TabletopSettings.getHue(), - })); + SpaceConnection.spaceConnector!.sendAction( + ServerAction("tc_move", {"x": mousePos.dx, "y": mousePos.dy, "c": TabletopSettings.getHue()}), + ); } _lastMousePos = mousePos; } /// Update the cursor position of other people - void updateCursor(String id, Offset position, double hue) { - if (id == SpaceMemberController.ownId) { + static void updateCursor(String id, Offset position, double hue) { + if (id == SpaceMemberController.getOwnId()) { return; } - - if (cursors[id] == null) { - cursors[id] = TabletopCursor(id, position, hue); - } else { - if (cursors[id]!.hue.value != hue) { - cursors[id]!.hue.value = hue; + batch(() { + if (cursors[id] == null) { + cursors[id] = TabletopCursor(id, position, hue); + } else { + if (cursors[id]!.hue.value != hue) { + cursors[id]!.hue.value = hue; + } + cursors[id]!.move(position); } - cursors[id]!.move(position); - } - } - - /// Create a new object - TableObject newObject(TableObjectType type, String id, int order, Offset location, Size size, double rotation, String data) { - TableObject object; - switch (type) { - case TableObjectType.text: - object = TextObject(id, order, location, size); - case TableObjectType.deck: - object = DeckObject(id, order, location, size); - case TableObjectType.card: - object = CardObject(id, order, location, size); - case TableObjectType.inventory: - object = InventoryObject(id, order, location, size); - } - object.rotate(rotation); - object.decryptData(data); - return object; + }); } /// Add an object to the list - void addObject(TableObject object) { + static void addObject(TableObject object) { if (object.id == "" || object.order == 0) { return; } @@ -202,7 +181,7 @@ class TabletopController extends GetxController { } /// Add a new order to the sorted order list. - void addNewOrder(int newOrder) { + static void addNewOrder(int newOrder) { int index = 0; for (var order in orderSorted) { if (newOrder < order) { @@ -214,15 +193,20 @@ class TabletopController extends GetxController { } /// Remove an object from the list - void removeObject({TableObject? object, String? id}) { + static void removeObject({TableObject? object, String? id}) { objects.remove(id ?? object?.id); if (objectOrder[object?.order ?? -1] != null) { objectOrder.remove(object?.order); } + + // Make sure to remove the inventory when it's not there anymore + if ((object == inventory && inventory != null) || id == inventory?.id) { + inventory = null; + } } /// Set the order of an object - void setOrder(String object, int newOrder, {bool removeOld = false}) { + static void setOrder(String object, int newOrder, {bool removeOld = false}) { // Remove the object id from the old layer if desired by the server if (newOrder == -1) { final obj = objects[object]!; @@ -245,8 +229,8 @@ class TabletopController extends GetxController { } /// Get the object at a location - List raycast(Offset location) { - final objects = []; + static List raycast(Offset location) { + final objectsFound = []; final typesFound = []; final ordersToRemove = []; for (var order in orderSorted.reversed) { @@ -257,14 +241,14 @@ class TabletopController extends GetxController { } // Check if the object is hovered - final object = this.objects[objectId]; + final object = objects[objectId]; if (object == null) { ordersToRemove.add(order); continue; } final rect = Rect.fromLTWH(object.location.dx, object.location.dy, object.size.width, object.size.height); if (rect.contains(location) && !typesFound.contains(object.type)) { - objects.add(object); + objectsFound.add(object); typesFound.add(object.type); } } @@ -275,11 +259,11 @@ class TabletopController extends GetxController { orderSorted.remove(order); } - return objects; + return objectsFound; } /// Start holding an object in tabletop (also drops objects in case they don't exist) - Future startHoldingObject(TableObject object) async { + static Future startHoldingObject(TableObject object) async { // Check if it is a card from the inventory that should be dropped var currentlyExists = false; if (object is CardObject && object.inventory) { @@ -331,7 +315,7 @@ class TabletopController extends GetxController { } /// Cancels the holding of an object and makes sure it's cancelled - void stopHoldingObject({required bool error}) { + static void stopHoldingObject({required bool error}) { if (heldObject == null) return; // Notify the server of the unselection when there was no error @@ -349,7 +333,7 @@ class TabletopController extends GetxController { } /// Gets the inventory or creates it on the table (in case needed). - Future getOrCreateInventory() async { + static Future getOrCreateInventory() async { if (inventory == null) { final object = InventoryObject("", -1, mousePos, Size(200, 200)); if (await object.sendAdd()) { @@ -363,303 +347,6 @@ class TabletopController extends GetxController { } } -enum TableObjectType { - text(Icons.text_fields, "Text"), - deck(Icons.filter_none, "Deck"), - card(Icons.image, "Card", creatable: false), - inventory(Icons.business_center, "Inventory", creatable: false); - - final IconData icon; - final String label; - final bool creatable; - - const TableObjectType(this.icon, this.label, {this.creatable = true}); -} - -abstract class TableObject { - TableObject(this.id, this.order, this.location, this.size, this.type); - - Function()? dataCallback; - String id; - int order; - TableObjectType type; - - /// The size of the object - Size size; - - /// The top left location of the object on the table - String? dataBeforeQueue; - DateTime? _lastMove; - Offset? _lastLocation; - Offset location; - bool deleted = false; - bool added = false; - - // Modifiers - bool positionOverwrite = false; - final positionX = AnimatedDouble(0.0); - final positionY = AnimatedDouble(0.0); - final rotation = AnimatedDouble(0.0); - final scale = AnimatedDouble(1.0, from: 0.0); - - Offset interpolatedLocation(DateTime now) { - if (positionOverwrite) { - return Offset(positionX.value(now), positionY.value(now)); - } - if (_lastMove == null || _lastLocation == null) { - return location; - } - final time = now.difference(_lastMove!).inMilliseconds; - final delta = time / (1000 ~/ TabletopController.tickRate); - return Offset.lerp(_lastLocation!, location, delta.clamp(0, 1))!; - } - - void move(Offset location) { - _lastMove = DateTime.now(); - _lastLocation = this.location; - this.location = location; - } - - double lastRotation = 0; - void rotate(double rot) { - sendLog(lastRotation); - if (lastRotation == -1) { - rotation.setValue(rot); - } else { - lastRotation = rot; - } - } - - void newRotation(double rot) { - queue(() async { - final event = await spaceConnector.sendActionAndWait(ServerAction("tobj_rotate", { - "id": id, - "r": rot, - })); - currentlyModifying = false; - - // Check if there was an error with the rotation - if (event == null) { - sendLog("error with object rotation: no response"); - return; - } - if (!event.data["success"]) { - sendLog("error with object rotation: ${event.data["message"]}"); - } - }); - } - - /// Called every frame when the object is hovered - void hoverRotation(double rot) { - if (lastRotation == -1) { - lastRotation = rotation.realValue; - } - rotation.setValue(rot); - } - - /// Called every frame when the object is no longer hovered - void unhoverRotation() { - if (lastRotation != -1) { - rotation.setValue(lastRotation); - lastRotation = -1; - } - } - - /// DONT OVERWRITE THIS METHOD - void decryptData(String data) { - handleData(decryptSymmetric(data, SpacesController.key!)); - } - - /// NEVER CALL THIS METHOD WITH ENCRYPTED DATA - void handleData(String data) {} - - /// Implemented optionally when needed - String getData() { - return ""; - } - - String encryptedData() { - return encryptSymmetric(getData(), SpacesController.key!); - } - - /// Render with rotation and scale applied (used for movable objects) - void render(Canvas canvas, Offset location, TabletopController controller) {} - - /// Called when the object is clicked - void runAction(TabletopController controller) {} - - /// Called when the object is right clicked - List getContextMenuAdditions() { - return []; - } - - /// Add a new object - Future sendAdd() { - deleted = false; - if (added) { - sendLog("WHAT DA HELL"); - } - added = true; - final completer = Completer(); - - // Send to the server - spaceConnector.sendAction( - ServerAction("tobj_create", { - "x": location.dx, - "y": location.dy, - "w": size.width, - "h": size.height, - "r": lastRotation, - "type": type.index, - "data": encryptedData(), - }), - handler: (event) { - if (!event.data["success"]) { - sendLog("SOMETHING WENT WRONG"); - completer.complete(false); - return; - } - id = event.data["id"]; - order = event.data["o"]; - sendLog("ADDING $id to table with order $order"); - Get.find().addObject(this); - completer.complete(true); - }, - ); - - return completer.future; - } - - /// Remove an object - void sendRemove() { - deleted = true; - added = false; - spaceConnector.sendAction(ServerAction("tobj_delete", id)); - } - - /// Start a modification process (data) - Future select() { - final completer = Completer(); - - spaceConnector.sendAction( - ServerAction("tobj_select", id), - handler: (event) { - if (!event.data["success"]) { - showErrorPopup("error", event.data["message"]); - sendLog("can't select rn"); - completer.complete(false); - return; - } - completer.complete(true); - }, - ); - - return completer.future; - } - - /// Start a modification process (data) - Future unselect() { - final completer = Completer(); - if (deleted) { - completer.complete(false); - } else { - spaceConnector.sendAction( - ServerAction("tobj_unselect", id), - handler: (event) { - if (!event.data["success"]) { - sendLog("can't unselect rn"); - completer.complete(false); - return; - } - completer.complete(true); - }, - ); - } - - return completer.future; - } - - // Boolean to make sure the object is not modified - bool currentlyModifying = false; - - /// Wait until the data can be modified - void queue(Function() callback) { - if (currentlyModifying) { - return; - } - currentlyModifying = true; - dataBeforeQueue = getData(); - spaceConnector.sendAction( - ServerAction("tobj_mqueue", id), - handler: (event) { - if (!event.data["success"]) { - showErrorPopup("error", event.data["message"]); - return; - } - - if (event.data["direct"]) { - callback(); - } else { - dataCallback = callback; - } - }, - ); - } - - /// Update the data of the object - Future modifyData() { - final completer = Completer(); - spaceConnector.sendAction( - ServerAction("tobj_modify", { - "id": id, - "data": encryptedData(), - "width": size.width, - "height": size.height, - }), - handler: (event) { - currentlyModifying = false; - // Reset data in case the modification wasn't successful - if (!event.data["success"]) { - if (dataBeforeQueue == null) { - sendLog("NO ROLLBACK STATE FOR OBJECT"); - return; - } - - sendLog("modification of $id wasn't possible: ${event.data["message"]}"); - handleData(dataBeforeQueue!); - completer.complete(false); - } else { - completer.complete(true); - } - - // Reset it - dataBeforeQueue = null; - }, - ); - return completer.future; - } -} - -class ContextMenuAction { - final IconData icon; - final bool category; - final String label; - final Color? color; - final Color? iconColor; - final bool goBack; - final Function(TabletopController) onTap; - - const ContextMenuAction({ - required this.icon, - required this.label, - required this.onTap, - this.category = false, - this.goBack = true, - this.color, - this.iconColor, - }); -} - class AnimatedDouble { static const animationDuration = 250; static const curve = Curves.ease; diff --git a/lib/controller/spaces/tabletop/objects/tabletop_cursor.dart b/lib/controller/spaces/tabletop/tabletop_cursor.dart similarity index 83% rename from lib/controller/spaces/tabletop/objects/tabletop_cursor.dart rename to lib/controller/spaces/tabletop/tabletop_cursor.dart index 7f1bc02f..3568e3a9 100644 --- a/lib/controller/spaces/tabletop/objects/tabletop_cursor.dart +++ b/lib/controller/spaces/tabletop/tabletop_cursor.dart @@ -1,7 +1,7 @@ import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; import 'package:chat_interface/pages/settings/town/tabletop_settings.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class TabletopCursor { String clientId; @@ -9,7 +9,7 @@ class TabletopCursor { DateTime? _lastMove; Offset? _lastLocation; Offset location; - final hue = 0.0.obs; + final hue = signal(0.0); TabletopCursor(this.clientId, this.location, double hue) { this.hue.value = hue; @@ -36,9 +36,10 @@ class TabletopCursor { return; } - final paint = Paint() - ..color = TabletopSettings.getCursorColor(hue: hue.value) - ..style = PaintingStyle.fill; + final paint = + Paint() + ..color = TabletopSettings.getCursorColor(hue: hue.value) + ..style = PaintingStyle.fill; canvas.drawCircle(interpolatedLocation(DateTime.now()), 10, paint); } } diff --git a/lib/controller/spaces/tabletop/tabletop_decks.dart b/lib/controller/spaces/tabletop/tabletop_decks.dart index ad7fea18..3e9db66d 100644 --- a/lib/controller/spaces/tabletop/tabletop_decks.dart +++ b/lib/controller/spaces/tabletop/tabletop_decks.dart @@ -4,14 +4,11 @@ import 'package:chat_interface/util/constants.dart'; import 'package:chat_interface/util/web.dart'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; -import 'package:get/get.dart'; +import 'package:signals/signals.dart'; class TabletopDecks { static Future?> listDecks() async { - final json = await postAuthorizedJSON("/account/vault/list", { - "after": 0, - "tag": Constants.vaultDeckTag, - }); + final json = await postAuthorizedJSON("/account/vault/list", {"after": 0, "tag": Constants.vaultDeckTag}); if (!json["success"]) { return null; @@ -29,18 +26,15 @@ class TabletopDeck { String? vaultId; String name; List encodedCards = []; - final cards = [].obs; - final amounts = {}.obs; // Map of card id to amount + final cards = signal([]); + final amounts = signal({}); // Map of card id to amount TabletopDeck(this.name, {this.vaultId}); factory TabletopDeck.decrypt(StorageType usecase, Map json) { final entry = VaultEntry.fromJson(json); final decrypted = jsonDecode(entry.decryptedPayload()); - final deck = TabletopDeck( - decrypted['name'], - vaultId: entry.id, - ); + final deck = TabletopDeck(decrypted['name'], vaultId: entry.id); if (decrypted['cards'] == null) { return deck; } @@ -52,19 +46,16 @@ class TabletopDeck { /// Add the deck to the vault Future save() async { final encodedCards = >[]; - for (var card in cards) { + for (var card in cards.peek()) { final json = card.toJson(); - json["amount"] = amounts[card.id] ?? 1; + json["amount"] = amounts.peek()[card.id] ?? 1; encodedCards.add(json); } - final payload = jsonEncode({ - "name": name, - "cards": encodedCards, - }); + final payload = jsonEncode({"name": name, "cards": encodedCards}); if (vaultId != null) { - return updateVault(vaultId!, payload); + return updateVault(Constants.vaultDeckTag, vaultId!, payload); } - final id = await addToVault(Constants.vaultDeckTag, payload); + final (_, id) = await addToVault(Constants.vaultDeckTag, payload); if (id == null) { return false; } @@ -74,18 +65,21 @@ class TabletopDeck { /// usecase is the type of storage to use for the cards (for downloaded ones it should be "cache" for example) Future loadCards(StorageType usecase) async { - final controller = Get.find(); bool removed = false; for (var card in encodedCards) { - final type = await AttachmentController.checkLocations(card['i'], usecase, types: [StorageType.permanent, StorageType.cache]); - final container = controller.fromJson(type, card); - amounts[container.id] = card['a'] ?? 1; - final result = await controller.downloadAttachment(container); + final type = await AttachmentController.checkLocations( + card['i'], + usecase, + types: [StorageType.permanent, StorageType.cache], + ); + final container = AttachmentController.fromJson(type, card); + amounts.value[container.id] = card['a'] ?? 1; + final result = await AttachmentController.downloadAttachment(container); if (!result || container.error.value) { removed = true; continue; } - cards.add(container); + cards.value.add(container); } // Save the deck in case cards have been removed because they don't exist anymore @@ -97,9 +91,8 @@ class TabletopDeck { Future delete() async { // Delete all cards - final controller = Get.find(); - for (var card in cards) { - await controller.deleteFile(card); + for (var card in cards.peek()) { + await AttachmentController.deleteFile(card); } if (vaultId == null) { diff --git a/lib/controller/spaces/warp_controller.dart b/lib/controller/spaces/warp_controller.dart index f060e3cf..0ad080e0 100644 --- a/lib/controller/spaces/warp_controller.dart +++ b/lib/controller/spaces/warp_controller.dart @@ -1,75 +1,65 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/messaging.dart'; -import 'package:chat_interface/connection/spaces/space_connection.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/services/spaces/warp/warp_connection.dart'; +import 'package:chat_interface/services/spaces/warp/warp_service.dart'; +import 'package:chat_interface/services/spaces/warp/warp_shared.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/pages/spaces/warp/warp_manager_window.dart'; -import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class WarpController extends GetxController { - bool loading = false; +class WarpController { + static bool loading = false; /// Ports the current user is sharing as Warps - final sharedWarps = {}.obs; + static final sharedWarps = mapSignal({}); /// Warps that the user is connected to. - final activeWarps = {}.obs; + static final activeWarps = mapSignal({}); /// Warps that are currently available (not connected) according to the server. - final warps = [].obs; + static final warps = listSignal([]); - void resetControllerState() { + static void resetControllerState() { // Stop all the Warps - for (var warp in sharedWarps.values) { + for (var warp in sharedWarps.peek().values) { warp.stop(action: false); } - for (var warp in activeWarps.values) { + for (var warp in activeWarps.peek().values) { warp.disconnectFromWarp(action: false); } // Delete all the state - warps.clear(); - sharedWarps.clear(); - activeWarps.clear(); + batch(() { + warps.clear(); + sharedWarps.clear(); + activeWarps.clear(); + }); loading = false; } /// Create a Warp using the port it should share. - /// - /// This will tell the server about a port that this client wants to share with others in - /// the Space. The connections to the local server will only start being opened once the - /// first packet from the server arrives (they will be made on demand). - /// - /// This function doesn't do any validation since that's already happening in the WarpCreateWindow - /// that calls this function. - /// - /// Returns an error if there was one. - Future createWarp(int port) async { - final event = await spaceConnector.sendActionAndWait(ServerAction("wp_create", port)); - if (event == null) { - return "server.error".tr; + static Future createWarp(int port) async { + // Check if the port is already being shared + if (sharedWarps.peek().values.any((sw) => sw.port == port)) { + return "warp.error.port_already_shared".tr; } - // Make sure the request was valid - if (!event.data["success"]) { - return event.data["message"]; + // Create a Warp using the service + final (error, warp) = await WarpService.createWarp(port); + if (error != null) { + return error; } // Add the Warp to the list of shared warps - sharedWarps[event.data["id"]] = SharedWarp(event.data["id"], port); + assert(warp != null); + sharedWarps[warp!.id] = warp; return null; } /// Stop a Warp and remove it from the list of shared Warps. - void stopWarp(SharedWarp warp) { + static void stopWarp(SharedWarp warp) { warp.stop(); sharedWarps.remove(warp.id); } @@ -77,37 +67,15 @@ class WarpController extends GetxController { /// Connect to a Warp using its container. /// /// This will start an isolate that then tries to connect to every port on the local system. - Future connectToWarp(WarpShareContainer container) async { + static Future connectToWarp(WarpShareContainer container) async { if (loading) { return; } container.loading.value = true; loading = true; - // Scan for a port that is free on the current system - final random = Random(); - int currentPort = container.port; // Start with the port that the sharer desired - bool found = false; - while (!found) { - // Try connecting to the port - try { - await Socket.connect("localhost", currentPort); - - // Generate a new random port - currentPort = random.nextInt(65535 - 1024) + 1024; - - // This is just here in case this turns into an infinite loop and to prevent over-spinning - await Future.delayed(Duration(milliseconds: 100)); - } catch (e) { - found = true; - } - } - - // Use the port that's been scanned above to start a socket for the Warp - final warp = ConnectedWarp(container.id, container.port, currentPort, container.account); - await warp.startServer(); - - // Add the Warp to the list of active ones + // Connect to the Warp using the service + final warp = await WarpService.connectToWarp(container); activeWarps[warp.id] = warp; container.loading.value = false; @@ -115,366 +83,49 @@ class WarpController extends GetxController { } /// Completely disconnect a Warp - void disconnectWarp(ConnectedWarp warp) { + static void disconnectWarp(ConnectedWarp warp) { warp.disconnectFromWarp(); activeWarps.remove(warp.id); } /// This method gets called when a Warp ends according to the server. - void onWarpEnd(String warp) { - // Disconnect if an active warp is closed - if (activeWarps.containsKey(warp)) { - activeWarps[warp]!.disconnectFromWarp(action: false); - activeWarps.remove(warp); - } + static void onWarpEnd(String warp) { + // Start a batch to make sure there is only one update + batch(() { + // Disconnect if an active warp is closed + if (activeWarps.containsKey(warp)) { + activeWarps[warp]!.disconnectFromWarp(action: false); + activeWarps.remove(warp); + } - // Remove it from the list of Warps on the server - warps.removeWhere((w) => w.id == warp); + // Remove it from the list of Warps on the server + warps.removeWhere((w) => w.id == warp); + }); } /// Open Warp based on what it's currently doing. - void open() { + static void open() { showModal(WarpManagerWindow()); } -} - -/// A Warp that has been shared by the user. -class SharedWarp { - /// The id of the Warp (to identify it) - final String id; - - /// The port being shared through Warp - final int port; - - SharedWarp(this.id, this.port); - - /// All the current connections coming from clients - final _sockets = >{}; - - /// Completers to make sure packets aren't sent before the socket is connected - final _completers = >>{}; - - // All the subscriptions made for the sockets (they need to be cancelled on disconnect) - final _subs = >{}; - - // All sequence related stuff for jitter buffering (can happen cause server) - final _sequenceNumbers = >{}; - final _packetQueue = >>{}; - - /// Send a packet from a client to the Warp. - /// - /// This will also open a new connection to the server in case there isn't one yet (for this client). - Future receivePacketFromClient(String id, int connId, Uint8List bytes, int seq) async { - if (_sockets[id] == null) { - sendLog("reset"); - - // Initialize the socket storage and completers - _sockets[id] = {}; - _completers[id] = >{}; - - // Initialize jitter buffering with correct values - _packetQueue[id] = >{}; - _sequenceNumbers[id] = {}; - } - - // Check if there is a connection already - if (_sockets[id]![connId] == null) { - // Make sure other packets wait - _sequenceNumbers[id]![connId] = 0; - final completer = Completer(); - _completers[id]![connId] = completer; - // Create a new connection to the local server - try { - final socket = await Socket.connect("localhost", port); - registerListener(id, connId, socket); - _sockets[id]![connId] = socket; - completer.complete(true); - } catch (e) { - sendLog("couldn't connect to local server: $e"); - completer.complete(false); - return false; - } - } - - // Check if there is a completer to wait for - if (_completers[id]![connId] != null) { - final result = await _completers[id]![connId]!.future; - if (!result) { - return false; - } - } - - sendLog("received $connId $seq ${_sequenceNumbers[id]![connId]!}"); - - // Check what the last sequence number was - if (seq != _sequenceNumbers[id]![connId]! + 1) { - if (_packetQueue[id]![connId] == null) { - _packetQueue[id]![connId] = {}; - } - sendLog("packet queue $connId (share)"); - - // Add the packet to the packet queue for now - _packetQueue[id]![connId]![seq] = bytes; - return true; - } else { - _sequenceNumbers[id]![connId] = seq; - } - - // Send the packet to the socket - final socket = _sockets[id]![connId]!; - socket.add(bytes); - sendLog("sending $connId $seq"); - - // Check if there is a packet after it in the sequence queue - while (_packetQueue[id]![connId]?[seq + 1] != null) { - seq++; - - // Send the packet to the socket and remove it from the queue - socket.add(_packetQueue[id]![connId]![seq]!); - _packetQueue[id]![connId]!.remove(seq); - - sendLog("sending pq $connId $seq"); - - // Update the sequence number accordingly - _sequenceNumbers[id]![connId] = seq; - } - - return true; + /// Add a shared warp from the server to the list on the client. + static void addWarp(WarpShareContainer container) { + warps.add(container); } - /// Register the listener that listens to the packets sent to the socket. - /// - /// This method will take all those packets and send them back to the other client - /// through the server. - void registerListener(String id, int connId, Socket socket) { - if (_subs[id] == null) { - _subs[id] = {}; - } - - int seq = 1; - _subs[id]![connId] = socket.listen( - (packet) { - sendPacketToClient(id, connId, packet, seq); - seq++; - }, - onError: (e) { - removeClientFromWarp(id); - }, - cancelOnError: true, - ); + /// Get a currently active Warp + static ConnectedWarp? getActiveWarp(String id) { + return activeWarps[id]; } - /// Send a packet to a client through the server. - Future sendPacketToClient(String id, int connId, Uint8List bytes, int seq) async { - final event = await spaceConnector.sendActionAndWait(ServerAction("wp_send_back", { - "w": this.id, // The parameter called "id" almost got me here xd - "t": id, - "c": connId, - "s": seq, - "p": base64Encode(encryptSymmetricBytes(bytes, SpacesController.key!)), - })); - - // Remove the client from the warp in case the response from the server is invalid (or there was an error) - if (event == null || !event.data["success"]) { - removeClientFromWarp(id); - return; - } + /// Get a currently shared Warp + static SharedWarp? getSharedWarp(String id) { + return sharedWarps[id]; } - /// Disconnect a client from the Warp. - /// - /// This kicks them and blocks packets on the server side. - void removeClientFromWarp(String id) { - // Tell the server to kick the client - spaceConnector.sendAction(ServerAction("wp_kick", { - "w": this.id, - "t": id, - })); - - // Disconnect them from the local server - handleDisconnect(id); - } - - /// Handle a client disconnect. - /// - /// This stops the client's connection to the local server. - void handleDisconnect(String id) { - if (_subs[id] != null) { - for (var sub in _subs[id]!.values) { - sub.cancel(); - } - } - if (_sockets[id] != null) { - for (var socket in _sockets[id]!.values) { - socket.close(); - } - } - _sequenceNumbers.remove(id); - _packetQueue.remove(id); - _subs.remove(id); - _sockets.remove(id); - - // Remove from the controller - Get.find().sharedWarps.remove(id); - } - - /// Stop the Warp completely. - /// - /// Disconnects all clients + tells the server about the closure. - void stop({bool action = true}) { - if (action) { - spaceConnector.sendAction(ServerAction("wp_end", id)); - } - - // Disconnect all clients - for (var map in _sockets.values) { - for (var socket in map.values) { - socket.close(); - } - } - } -} - -/// A Warp that is being shared with the user and has been bound to a port. -class ConnectedWarp { - /// The id of the Warp (to identify it) - final String id; - - /// The port on the hoster's computer (for clarity when rendering) - final int originPort; - - /// The port the server is being bound to on the local system - final int goalPort; - - /// The friend that's hosting the Warp - final Friend hoster; - - ConnectedWarp(this.id, this.originPort, this.goalPort, this.hoster); - - /// The server that proxies the connections. - ServerSocket? server; - - /// The counter keeping track of the current connection number (for forwarding more efficiently) - int connectionCount = 1; - - /// All the sockets that are currently connected to the local server - final _sockets = {}; - - // All sequence related stuff for jitter buffering (can happen cause server) - final _sequenceNumbers = {}; - final _packetQueue = >{}; - - /// Start the server for proxying the connection. - Future startServer() async { - server = await ServerSocket.bind(InternetAddress.loopbackIPv4, goalPort, shared: false); - - sendLog("bound server on ${server!.address.toString()} ${server!.port}"); - - server!.listen( - (socket) { - // Increment the connection count and take current count as new identifier for this connection - final currentId = connectionCount; - _sockets[currentId] = socket; - connectionCount++; - - // Listen to all packets from the socket - int seq = 0; - StreamSubscription? sub; - sub = socket.listen( - (packet) async { - // Forward the bytes to the server - seq++; - final result = await forwardBytesToHost(currentId, packet, seq); - if (!result) { - disconnectFromWarp(); - } - }, - onDone: () { - sub?.cancel(); - _sockets.remove(currentId); - }, - onError: (e) { - sendLog("disconnected cause $e"); - }, - cancelOnError: true, - ); - }, - onDone: () => disconnectFromWarp(), - onError: (e) { - sendLog("warp ended cause $e"); - }, - cancelOnError: true, - ); - } - - /// Send bytes to the host server. - Future forwardBytesToHost(int connId, Uint8List bytes, int seq) async { - final event = await spaceConnector.sendActionAndWait(ServerAction("wp_send_to", { - "w": id, - "s": seq, - "c": connId, - "p": base64Encode(encryptSymmetricBytes(bytes, SpacesController.key!)), - })); - if (event == null || !event.data["success"]) { - return false; - } - return true; - } - - /// Forward a packet to a socket connected to the local server by their connection id - Future forwardPacketToSocket(int connId, Uint8List bytes, int seq) async { - if (_sockets[connId] == null) { - return false; - } - if (_packetQueue[connId] == null) { - _packetQueue[connId] = {}; - _sequenceNumbers[connId] = 0; - } - - // Make sure it's the right sequence number - if (seq != _sequenceNumbers[connId]! + 1) { - _packetQueue[connId]![seq] = bytes; - sendLog("packet queue (conn)"); - return true; - } else { - _sequenceNumbers[connId] = seq; - } - - // Forward the packet - _sockets[connId]!.add(bytes); - - // Check the queue for more packets after the current one - while (_packetQueue[connId]![seq + 1] != null) { - seq++; - - // Send the packet in the queue - _sockets[connId]!.add(_packetQueue[connId]![seq]!); - _packetQueue[connId]!.remove(seq); - - // Update the sequence number accordingly - _sequenceNumbers[connId] = seq; - } - return true; - } - - /// Disconnect from the Warp and close the local server. - void disconnectFromWarp({bool action = true}) { - if (action) { - spaceConnector.sendAction(ServerAction("wp_disconnect", id)); - } - onDisconnect(); - } - - /// Called when the user gets disconnected. - void onDisconnect() { - for (var socket in _sockets.values) { - socket.close(); - } - server?.close(); - - // Remove from the controller - Get.find().activeWarps.remove(id); + /// Remove currently active warp + static void removeActiveWarp(ConnectedWarp warp) { + activeWarps.remove(warp.id); } } @@ -489,11 +140,7 @@ class WarpShareContainer { /// The person sharing the Warp. final Friend account; - final loading = false.obs; + final loading = signal(false); - WarpShareContainer({ - required this.id, - required this.account, - required this.port, - }); + WarpShareContainer({required this.id, required this.account, required this.port}); } diff --git a/lib/controller/square/shared_space_controller.dart b/lib/controller/square/shared_space_controller.dart new file mode 100644 index 00000000..e6668f7d --- /dev/null +++ b/lib/controller/square/shared_space_controller.dart @@ -0,0 +1,30 @@ +import 'package:chat_interface/services/squares/square_shared_space.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:signals/signals_flutter.dart'; + +class SharedSpaceController { + static final sharedSpaceMap = mapSignal>({}); + + /// Add or update a shared space to a conversation. + static void addSharedSpace(LPHAddress convId, SharedSpace space) { + final map = sharedSpaceMap.peek()[convId] ?? {}; + map[space.id] = space; + sharedSpaceMap[convId] = map; + } + + /// Delete a shared space from a conversation. + static void deleteSharedSpace(LPHAddress convId, String spaceId) { + final map = sharedSpaceMap.peek()[convId]; + if (map == null) { + return; + } + + // Delete and update the map + map.remove(spaceId); + sharedSpaceMap[convId] = map; + } + + static void clearAll() { + sharedSpaceMap.clear(); + } +} diff --git a/lib/database/database.dart b/lib/database/database.dart index 565d81da..93d5ba07 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -8,31 +8,47 @@ part 'database.g.dart'; bool databaseInitialized = false; late Database db; -@DriftDatabase(tables: [ - Conversation, - Message, - Member, - Setting, - Friend, - Request, - UnknownProfile, - Profile, - TrustedLink, - LibraryEntry, -]) +@DriftDatabase( + tables: [Conversation, Message, Member, Setting, Friend, Request, UnknownProfile, Profile, TrustedLink, LibraryEntry], +) class Database extends _$Database { Database(super.e); @override - int get schemaVersion => 2; + int get schemaVersion => 5; @override MigrationStrategy get migration { return MigrationStrategy( onUpgrade: stepByStep( from1To2: (m, schema) async { + // Add new table for local message storage await m.createTable(schema.message); }, + from2To3: (m, schema) async { + // Add indexes to some tables for improved performance + await m.createIndex(schema.idxConversationUpdated); + await m.createIndex(schema.idxFriendsUpdated); + await m.createIndex(schema.idxLibraryEntryCreated); + await m.createIndex(schema.idxMessageCreated); + }, + from3To4: (m, schema) async { + // Create a new column for a hashed identifier of the library entry (so the file container can be encrypted) + await m.addColumn(schema.libraryEntry, schema.libraryEntry.identifierHash); + + // Create a new column for a unknown profile's last cache (it's now no longer cached in memory) + await m.addColumn(schema.unknownProfile, schema.unknownProfile.lastFetched); + + // Add indexes to improve performance (forgot some during initial additions) + await m.createIndex(schema.idxUnknownProfilesLastFetched); + await m.createIndex(schema.idxLibraryEntryIdhash); + await m.createIndex(schema.idxRequestsUpdated); + }, + from4To5: (m, schema) async { + // Replace the read column with the new encrypted version (that also handles extras) + await m.dropColumn(schema.conversation, "read_at"); + await m.addColumn(schema.conversation, schema.conversation.reads); + }, ), ); } diff --git a/lib/database/database.g.dart b/lib/database/database.g.dart index 69ec3833..4405a470 100644 --- a/lib/database/database.g.dart +++ b/lib/database/database.g.dart @@ -12,63 +12,113 @@ class $ConversationTable extends Conversation static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _vaultIdMeta = - const VerificationMeta('vaultId'); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _vaultIdMeta = const VerificationMeta( + 'vaultId', + ); @override late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _typeMeta = const VerificationMeta('type'); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override late final GeneratedColumnWithTypeConverter type = - GeneratedColumn('type', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true) - .withConverter($ConversationTable.$convertertype); + GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ).withConverter($ConversationTable.$convertertype); static const VerificationMeta _dataMeta = const VerificationMeta('data'); @override late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _tokenMeta = const VerificationMeta('token'); @override late final GeneratedColumn token = GeneratedColumn( - 'token', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _keyMeta = const VerificationMeta('key'); @override late final GeneratedColumn key = GeneratedColumn( - 'key', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _lastVersionMeta = - const VerificationMeta('lastVersion'); + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _lastVersionMeta = const VerificationMeta( + 'lastVersion', + ); @override late final GeneratedColumn lastVersion = GeneratedColumn( - 'last_version', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); - static const VerificationMeta _updatedAtMeta = - const VerificationMeta('updatedAt'); + 'last_version', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); @override late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); - static const VerificationMeta _readAtMeta = const VerificationMeta('readAt'); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + static const VerificationMeta _readsMeta = const VerificationMeta('reads'); + @override + late final GeneratedColumn reads = GeneratedColumn( + 'reads', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(""), + ); @override - late final GeneratedColumn readAt = GeneratedColumn( - 'read_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); - @override - List get $columns => - [id, vaultId, type, data, token, key, lastVersion, updatedAt, readAt]; + List get $columns => [ + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + reads, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'conversation'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -77,49 +127,61 @@ class $ConversationTable extends Conversation context.missing(_idMeta); } if (data.containsKey('vault_id')) { - context.handle(_vaultIdMeta, - vaultId.isAcceptableOrUnknown(data['vault_id']!, _vaultIdMeta)); + context.handle( + _vaultIdMeta, + vaultId.isAcceptableOrUnknown(data['vault_id']!, _vaultIdMeta), + ); } else if (isInserting) { context.missing(_vaultIdMeta); } - context.handle(_typeMeta, const VerificationResult.success()); if (data.containsKey('data')) { context.handle( - _dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta)); + _dataMeta, + this.data.isAcceptableOrUnknown(data['data']!, _dataMeta), + ); } else if (isInserting) { context.missing(_dataMeta); } if (data.containsKey('token')) { context.handle( - _tokenMeta, token.isAcceptableOrUnknown(data['token']!, _tokenMeta)); + _tokenMeta, + token.isAcceptableOrUnknown(data['token']!, _tokenMeta), + ); } else if (isInserting) { context.missing(_tokenMeta); } if (data.containsKey('key')) { context.handle( - _keyMeta, key.isAcceptableOrUnknown(data['key']!, _keyMeta)); + _keyMeta, + key.isAcceptableOrUnknown(data['key']!, _keyMeta), + ); } else if (isInserting) { context.missing(_keyMeta); } if (data.containsKey('last_version')) { context.handle( + _lastVersionMeta, + lastVersion.isAcceptableOrUnknown( + data['last_version']!, _lastVersionMeta, - lastVersion.isAcceptableOrUnknown( - data['last_version']!, _lastVersionMeta)); + ), + ); } else if (isInserting) { context.missing(_lastVersionMeta); } if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); } else if (isInserting) { context.missing(_updatedAtMeta); } - if (data.containsKey('read_at')) { - context.handle(_readAtMeta, - readAt.isAcceptableOrUnknown(data['read_at']!, _readAtMeta)); - } else if (isInserting) { - context.missing(_readAtMeta); + if (data.containsKey('reads')) { + context.handle( + _readsMeta, + reads.isAcceptableOrUnknown(data['reads']!, _readsMeta), + ); } return context; } @@ -130,25 +192,52 @@ class $ConversationTable extends Conversation ConversationData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ConversationData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - type: $ConversationTable.$convertertype.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}type'])!), - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, - token: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}token'])!, - key: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}key'])!, - lastVersion: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}last_version'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, - readAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}read_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + type: $ConversationTable.$convertertype.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + ), + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + token: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}token'], + )!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + lastVersion: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}last_version'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + reads: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reads'], + )!, ); } @@ -171,32 +260,34 @@ class ConversationData extends DataClass final String key; final BigInt lastVersion; final BigInt updatedAt; - final BigInt readAt; - const ConversationData( - {required this.id, - required this.vaultId, - required this.type, - required this.data, - required this.token, - required this.key, - required this.lastVersion, - required this.updatedAt, - required this.readAt}); + final String reads; + const ConversationData({ + required this.id, + required this.vaultId, + required this.type, + required this.data, + required this.token, + required this.key, + required this.lastVersion, + required this.updatedAt, + required this.reads, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); map['vault_id'] = Variable(vaultId); { - map['type'] = - Variable($ConversationTable.$convertertype.toSql(type)); + map['type'] = Variable( + $ConversationTable.$convertertype.toSql(type), + ); } map['data'] = Variable(data); map['token'] = Variable(token); map['key'] = Variable(key); map['last_version'] = Variable(lastVersion); map['updated_at'] = Variable(updatedAt); - map['read_at'] = Variable(readAt); + map['reads'] = Variable(reads); return map; } @@ -210,24 +301,27 @@ class ConversationData extends DataClass key: Value(key), lastVersion: Value(lastVersion), updatedAt: Value(updatedAt), - readAt: Value(readAt), + reads: Value(reads), ); } - factory ConversationData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ConversationData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return ConversationData( id: serializer.fromJson(json['id']), vaultId: serializer.fromJson(json['vaultId']), - type: $ConversationTable.$convertertype - .fromJson(serializer.fromJson(json['type'])), + type: $ConversationTable.$convertertype.fromJson( + serializer.fromJson(json['type']), + ), data: serializer.fromJson(json['data']), token: serializer.fromJson(json['token']), key: serializer.fromJson(json['key']), lastVersion: serializer.fromJson(json['lastVersion']), updatedAt: serializer.fromJson(json['updatedAt']), - readAt: serializer.fromJson(json['readAt']), + reads: serializer.fromJson(json['reads']), ); } @override @@ -236,38 +330,39 @@ class ConversationData extends DataClass return { 'id': serializer.toJson(id), 'vaultId': serializer.toJson(vaultId), - 'type': serializer - .toJson($ConversationTable.$convertertype.toJson(type)), + 'type': serializer.toJson( + $ConversationTable.$convertertype.toJson(type), + ), 'data': serializer.toJson(data), 'token': serializer.toJson(token), 'key': serializer.toJson(key), 'lastVersion': serializer.toJson(lastVersion), 'updatedAt': serializer.toJson(updatedAt), - 'readAt': serializer.toJson(readAt), + 'reads': serializer.toJson(reads), }; } - ConversationData copyWith( - {String? id, - String? vaultId, - ConversationType? type, - String? data, - String? token, - String? key, - BigInt? lastVersion, - BigInt? updatedAt, - BigInt? readAt}) => - ConversationData( - id: id ?? this.id, - vaultId: vaultId ?? this.vaultId, - type: type ?? this.type, - data: data ?? this.data, - token: token ?? this.token, - key: key ?? this.key, - lastVersion: lastVersion ?? this.lastVersion, - updatedAt: updatedAt ?? this.updatedAt, - readAt: readAt ?? this.readAt, - ); + ConversationData copyWith({ + String? id, + String? vaultId, + ConversationType? type, + String? data, + String? token, + String? key, + BigInt? lastVersion, + BigInt? updatedAt, + String? reads, + }) => ConversationData( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + reads: reads ?? this.reads, + ); ConversationData copyWithCompanion(ConversationCompanion data) { return ConversationData( id: data.id.present ? data.id.value : this.id, @@ -279,7 +374,7 @@ class ConversationData extends DataClass lastVersion: data.lastVersion.present ? data.lastVersion.value : this.lastVersion, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, - readAt: data.readAt.present ? data.readAt.value : this.readAt, + reads: data.reads.present ? data.reads.value : this.reads, ); } @@ -294,14 +389,23 @@ class ConversationData extends DataClass ..write('key: $key, ') ..write('lastVersion: $lastVersion, ') ..write('updatedAt: $updatedAt, ') - ..write('readAt: $readAt') + ..write('reads: $reads') ..write(')')) .toString(); } @override int get hashCode => Object.hash( - id, vaultId, type, data, token, key, lastVersion, updatedAt, readAt); + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + reads, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -314,7 +418,7 @@ class ConversationData extends DataClass other.key == this.key && other.lastVersion == this.lastVersion && other.updatedAt == this.updatedAt && - other.readAt == this.readAt); + other.reads == this.reads); } class ConversationCompanion extends UpdateCompanion { @@ -326,7 +430,7 @@ class ConversationCompanion extends UpdateCompanion { final Value key; final Value lastVersion; final Value updatedAt; - final Value readAt; + final Value reads; final Value rowid; const ConversationCompanion({ this.id = const Value.absent(), @@ -337,7 +441,7 @@ class ConversationCompanion extends UpdateCompanion { this.key = const Value.absent(), this.lastVersion = const Value.absent(), this.updatedAt = const Value.absent(), - this.readAt = const Value.absent(), + this.reads = const Value.absent(), this.rowid = const Value.absent(), }); ConversationCompanion.insert({ @@ -349,17 +453,16 @@ class ConversationCompanion extends UpdateCompanion { required String key, required BigInt lastVersion, required BigInt updatedAt, - required BigInt readAt, + this.reads = const Value.absent(), this.rowid = const Value.absent(), - }) : id = Value(id), - vaultId = Value(vaultId), - type = Value(type), - data = Value(data), - token = Value(token), - key = Value(key), - lastVersion = Value(lastVersion), - updatedAt = Value(updatedAt), - readAt = Value(readAt); + }) : id = Value(id), + vaultId = Value(vaultId), + type = Value(type), + data = Value(data), + token = Value(token), + key = Value(key), + lastVersion = Value(lastVersion), + updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, Expression? vaultId, @@ -369,7 +472,7 @@ class ConversationCompanion extends UpdateCompanion { Expression? key, Expression? lastVersion, Expression? updatedAt, - Expression? readAt, + Expression? reads, Expression? rowid, }) { return RawValuesInsertable({ @@ -381,22 +484,23 @@ class ConversationCompanion extends UpdateCompanion { if (key != null) 'key': key, if (lastVersion != null) 'last_version': lastVersion, if (updatedAt != null) 'updated_at': updatedAt, - if (readAt != null) 'read_at': readAt, + if (reads != null) 'reads': reads, if (rowid != null) 'rowid': rowid, }); } - ConversationCompanion copyWith( - {Value? id, - Value? vaultId, - Value? type, - Value? data, - Value? token, - Value? key, - Value? lastVersion, - Value? updatedAt, - Value? readAt, - Value? rowid}) { + ConversationCompanion copyWith({ + Value? id, + Value? vaultId, + Value? type, + Value? data, + Value? token, + Value? key, + Value? lastVersion, + Value? updatedAt, + Value? reads, + Value? rowid, + }) { return ConversationCompanion( id: id ?? this.id, vaultId: vaultId ?? this.vaultId, @@ -406,7 +510,7 @@ class ConversationCompanion extends UpdateCompanion { key: key ?? this.key, lastVersion: lastVersion ?? this.lastVersion, updatedAt: updatedAt ?? this.updatedAt, - readAt: readAt ?? this.readAt, + reads: reads ?? this.reads, rowid: rowid ?? this.rowid, ); } @@ -421,8 +525,9 @@ class ConversationCompanion extends UpdateCompanion { map['vault_id'] = Variable(vaultId.value); } if (type.present) { - map['type'] = - Variable($ConversationTable.$convertertype.toSql(type.value)); + map['type'] = Variable( + $ConversationTable.$convertertype.toSql(type.value), + ); } if (data.present) { map['data'] = Variable(data.value); @@ -439,8 +544,8 @@ class ConversationCompanion extends UpdateCompanion { if (updatedAt.present) { map['updated_at'] = Variable(updatedAt.value); } - if (readAt.present) { - map['read_at'] = Variable(readAt.value); + if (reads.present) { + map['reads'] = Variable(reads.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -459,7 +564,7 @@ class ConversationCompanion extends UpdateCompanion { ..write('key: $key, ') ..write('lastVersion: $lastVersion, ') ..write('updatedAt: $updatedAt, ') - ..write('readAt: $readAt, ') + ..write('reads: $reads, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -474,74 +579,114 @@ class $MessageTable extends Message with TableInfo<$MessageTable, MessageData> { static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _contentMeta = - const VerificationMeta('content'); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _contentMeta = const VerificationMeta( + 'content', + ); @override late final GeneratedColumn content = GeneratedColumn( - 'content', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _senderTokenMeta = - const VerificationMeta('senderToken'); + 'content', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _senderTokenMeta = const VerificationMeta( + 'senderToken', + ); @override late final GeneratedColumn senderToken = GeneratedColumn( - 'sender_token', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _senderAddressMeta = - const VerificationMeta('senderAddress'); + 'sender_token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _senderAddressMeta = const VerificationMeta( + 'senderAddress', + ); @override late final GeneratedColumn senderAddress = GeneratedColumn( - 'sender_address', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + 'sender_address', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); @override late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); - static const VerificationMeta _conversationMeta = - const VerificationMeta('conversation'); + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + static const VerificationMeta _conversationMeta = const VerificationMeta( + 'conversation', + ); @override late final GeneratedColumn conversation = GeneratedColumn( - 'conversation', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'conversation', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _editedMeta = const VerificationMeta('edited'); @override late final GeneratedColumn edited = GeneratedColumn( - 'edited', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("edited" IN (0, 1))')); - static const VerificationMeta _verifiedMeta = - const VerificationMeta('verified'); + 'edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("edited" IN (0, 1))', + ), + ); + static const VerificationMeta _verifiedMeta = const VerificationMeta( + 'verified', + ); @override late final GeneratedColumn verified = GeneratedColumn( - 'verified', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))')); + 'verified', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("verified" IN (0, 1))', + ), + ); @override List get $columns => [ - id, - content, - senderToken, - senderAddress, - createdAt, - conversation, - edited, - verified - ]; + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'message'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -550,50 +695,67 @@ class $MessageTable extends Message with TableInfo<$MessageTable, MessageData> { context.missing(_idMeta); } if (data.containsKey('content')) { - context.handle(_contentMeta, - content.isAcceptableOrUnknown(data['content']!, _contentMeta)); + context.handle( + _contentMeta, + content.isAcceptableOrUnknown(data['content']!, _contentMeta), + ); } else if (isInserting) { context.missing(_contentMeta); } if (data.containsKey('sender_token')) { context.handle( + _senderTokenMeta, + senderToken.isAcceptableOrUnknown( + data['sender_token']!, _senderTokenMeta, - senderToken.isAcceptableOrUnknown( - data['sender_token']!, _senderTokenMeta)); + ), + ); } else if (isInserting) { context.missing(_senderTokenMeta); } if (data.containsKey('sender_address')) { context.handle( + _senderAddressMeta, + senderAddress.isAcceptableOrUnknown( + data['sender_address']!, _senderAddressMeta, - senderAddress.isAcceptableOrUnknown( - data['sender_address']!, _senderAddressMeta)); + ), + ); } else if (isInserting) { context.missing(_senderAddressMeta); } if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); } else if (isInserting) { context.missing(_createdAtMeta); } if (data.containsKey('conversation')) { context.handle( + _conversationMeta, + conversation.isAcceptableOrUnknown( + data['conversation']!, _conversationMeta, - conversation.isAcceptableOrUnknown( - data['conversation']!, _conversationMeta)); + ), + ); } else if (isInserting) { context.missing(_conversationMeta); } if (data.containsKey('edited')) { - context.handle(_editedMeta, - edited.isAcceptableOrUnknown(data['edited']!, _editedMeta)); + context.handle( + _editedMeta, + edited.isAcceptableOrUnknown(data['edited']!, _editedMeta), + ); } else if (isInserting) { context.missing(_editedMeta); } if (data.containsKey('verified')) { - context.handle(_verifiedMeta, - verified.isAcceptableOrUnknown(data['verified']!, _verifiedMeta)); + context.handle( + _verifiedMeta, + verified.isAcceptableOrUnknown(data['verified']!, _verifiedMeta), + ); } else if (isInserting) { context.missing(_verifiedMeta); } @@ -606,22 +768,46 @@ class $MessageTable extends Message with TableInfo<$MessageTable, MessageData> { MessageData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MessageData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - content: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}content'])!, - senderToken: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}sender_token'])!, - senderAddress: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}sender_address'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}created_at'])!, - conversation: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}conversation'])!, - edited: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}edited'])!, - verified: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + content: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + )!, + senderToken: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_token'], + )!, + senderAddress: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_address'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + conversation: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation'], + )!, + edited: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}edited'], + )!, + verified: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}verified'], + )!, ); } @@ -640,15 +826,16 @@ class MessageData extends DataClass implements Insertable { final String conversation; final bool edited; final bool verified; - const MessageData( - {required this.id, - required this.content, - required this.senderToken, - required this.senderAddress, - required this.createdAt, - required this.conversation, - required this.edited, - required this.verified}); + const MessageData({ + required this.id, + required this.content, + required this.senderToken, + required this.senderAddress, + required this.createdAt, + required this.conversation, + required this.edited, + required this.verified, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -676,8 +863,10 @@ class MessageData extends DataClass implements Insertable { ); } - factory MessageData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory MessageData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return MessageData( id: serializer.fromJson(json['id']), @@ -705,38 +894,40 @@ class MessageData extends DataClass implements Insertable { }; } - MessageData copyWith( - {String? id, - String? content, - String? senderToken, - String? senderAddress, - BigInt? createdAt, - String? conversation, - bool? edited, - bool? verified}) => - MessageData( - id: id ?? this.id, - content: content ?? this.content, - senderToken: senderToken ?? this.senderToken, - senderAddress: senderAddress ?? this.senderAddress, - createdAt: createdAt ?? this.createdAt, - conversation: conversation ?? this.conversation, - edited: edited ?? this.edited, - verified: verified ?? this.verified, - ); + MessageData copyWith({ + String? id, + String? content, + String? senderToken, + String? senderAddress, + BigInt? createdAt, + String? conversation, + bool? edited, + bool? verified, + }) => MessageData( + id: id ?? this.id, + content: content ?? this.content, + senderToken: senderToken ?? this.senderToken, + senderAddress: senderAddress ?? this.senderAddress, + createdAt: createdAt ?? this.createdAt, + conversation: conversation ?? this.conversation, + edited: edited ?? this.edited, + verified: verified ?? this.verified, + ); MessageData copyWithCompanion(MessageCompanion data) { return MessageData( id: data.id.present ? data.id.value : this.id, content: data.content.present ? data.content.value : this.content, senderToken: data.senderToken.present ? data.senderToken.value : this.senderToken, - senderAddress: data.senderAddress.present - ? data.senderAddress.value - : this.senderAddress, + senderAddress: + data.senderAddress.present + ? data.senderAddress.value + : this.senderAddress, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - conversation: data.conversation.present - ? data.conversation.value - : this.conversation, + conversation: + data.conversation.present + ? data.conversation.value + : this.conversation, edited: data.edited.present ? data.edited.value : this.edited, verified: data.verified.present ? data.verified.value : this.verified, ); @@ -758,8 +949,16 @@ class MessageData extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(id, content, senderToken, senderAddress, - createdAt, conversation, edited, verified); + int get hashCode => Object.hash( + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -805,14 +1004,14 @@ class MessageCompanion extends UpdateCompanion { required bool edited, required bool verified, this.rowid = const Value.absent(), - }) : id = Value(id), - content = Value(content), - senderToken = Value(senderToken), - senderAddress = Value(senderAddress), - createdAt = Value(createdAt), - conversation = Value(conversation), - edited = Value(edited), - verified = Value(verified); + }) : id = Value(id), + content = Value(content), + senderToken = Value(senderToken), + senderAddress = Value(senderAddress), + createdAt = Value(createdAt), + conversation = Value(conversation), + edited = Value(edited), + verified = Value(verified); static Insertable custom({ Expression? id, Expression? content, @@ -837,16 +1036,17 @@ class MessageCompanion extends UpdateCompanion { }); } - MessageCompanion copyWith( - {Value? id, - Value? content, - Value? senderToken, - Value? senderAddress, - Value? createdAt, - Value? conversation, - Value? edited, - Value? verified, - Value? rowid}) { + MessageCompanion copyWith({ + Value? id, + Value? content, + Value? senderToken, + Value? senderAddress, + Value? createdAt, + Value? conversation, + Value? edited, + Value? verified, + Value? rowid, + }) { return MessageCompanion( id: id ?? this.id, content: content ?? this.content, @@ -918,25 +1118,43 @@ class $MemberTable extends Member with TableInfo<$MemberTable, MemberData> { static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _conversationIdMeta = - const VerificationMeta('conversationId'); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _conversationIdMeta = const VerificationMeta( + 'conversationId', + ); @override late final GeneratedColumn conversationId = GeneratedColumn( - 'conversation_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _accountIdMeta = - const VerificationMeta('accountId'); + 'conversation_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); @override late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _roleIdMeta = const VerificationMeta('roleId'); @override late final GeneratedColumn roleId = GeneratedColumn( - 'role_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'role_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); @override List get $columns => [id, conversationId, accountId, roleId]; @override @@ -945,8 +1163,10 @@ class $MemberTable extends Member with TableInfo<$MemberTable, MemberData> { String get actualTableName => $name; static const String $name = 'member'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -956,19 +1176,26 @@ class $MemberTable extends Member with TableInfo<$MemberTable, MemberData> { } if (data.containsKey('conversation_id')) { context.handle( + _conversationIdMeta, + conversationId.isAcceptableOrUnknown( + data['conversation_id']!, _conversationIdMeta, - conversationId.isAcceptableOrUnknown( - data['conversation_id']!, _conversationIdMeta)); + ), + ); } if (data.containsKey('account_id')) { - context.handle(_accountIdMeta, - accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta)); + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); } else if (isInserting) { context.missing(_accountIdMeta); } if (data.containsKey('role_id')) { - context.handle(_roleIdMeta, - roleId.isAcceptableOrUnknown(data['role_id']!, _roleIdMeta)); + context.handle( + _roleIdMeta, + roleId.isAcceptableOrUnknown(data['role_id']!, _roleIdMeta), + ); } else if (isInserting) { context.missing(_roleIdMeta); } @@ -981,14 +1208,25 @@ class $MemberTable extends Member with TableInfo<$MemberTable, MemberData> { MemberData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MemberData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - conversationId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}conversation_id']), - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}account_id'])!, - roleId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}role_id'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + conversationId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation_id'], + ), + accountId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + roleId: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role_id'], + )!, ); } @@ -1003,11 +1241,12 @@ class MemberData extends DataClass implements Insertable { final String? conversationId; final String accountId; final int roleId; - const MemberData( - {required this.id, - this.conversationId, - required this.accountId, - required this.roleId}); + const MemberData({ + required this.id, + this.conversationId, + required this.accountId, + required this.roleId, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1023,16 +1262,19 @@ class MemberData extends DataClass implements Insertable { MemberCompanion toCompanion(bool nullToAbsent) { return MemberCompanion( id: Value(id), - conversationId: conversationId == null && nullToAbsent - ? const Value.absent() - : Value(conversationId), + conversationId: + conversationId == null && nullToAbsent + ? const Value.absent() + : Value(conversationId), accountId: Value(accountId), roleId: Value(roleId), ); } - factory MemberData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory MemberData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return MemberData( id: serializer.fromJson(json['id']), @@ -1052,24 +1294,25 @@ class MemberData extends DataClass implements Insertable { }; } - MemberData copyWith( - {String? id, - Value conversationId = const Value.absent(), - String? accountId, - int? roleId}) => - MemberData( - id: id ?? this.id, - conversationId: - conversationId.present ? conversationId.value : this.conversationId, - accountId: accountId ?? this.accountId, - roleId: roleId ?? this.roleId, - ); + MemberData copyWith({ + String? id, + Value conversationId = const Value.absent(), + String? accountId, + int? roleId, + }) => MemberData( + id: id ?? this.id, + conversationId: + conversationId.present ? conversationId.value : this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + ); MemberData copyWithCompanion(MemberCompanion data) { return MemberData( id: data.id.present ? data.id.value : this.id, - conversationId: data.conversationId.present - ? data.conversationId.value - : this.conversationId, + conversationId: + data.conversationId.present + ? data.conversationId.value + : this.conversationId, accountId: data.accountId.present ? data.accountId.value : this.accountId, roleId: data.roleId.present ? data.roleId.value : this.roleId, ); @@ -1117,9 +1360,9 @@ class MemberCompanion extends UpdateCompanion { required String accountId, required int roleId, this.rowid = const Value.absent(), - }) : id = Value(id), - accountId = Value(accountId), - roleId = Value(roleId); + }) : id = Value(id), + accountId = Value(accountId), + roleId = Value(roleId); static Insertable custom({ Expression? id, Expression? conversationId, @@ -1136,12 +1379,13 @@ class MemberCompanion extends UpdateCompanion { }); } - MemberCompanion copyWith( - {Value? id, - Value? conversationId, - Value? accountId, - Value? roleId, - Value? rowid}) { + MemberCompanion copyWith({ + Value? id, + Value? conversationId, + Value? accountId, + Value? roleId, + Value? rowid, + }) { return MemberCompanion( id: id ?? this.id, conversationId: conversationId ?? this.conversationId, @@ -1193,13 +1437,21 @@ class $SettingTable extends Setting with TableInfo<$SettingTable, SettingData> { static const VerificationMeta _keyMeta = const VerificationMeta('key'); @override late final GeneratedColumn key = GeneratedColumn( - 'key', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _valueMeta = const VerificationMeta('value'); @override late final GeneratedColumn value = GeneratedColumn( - 'value', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [key, value]; @override @@ -1208,19 +1460,25 @@ class $SettingTable extends Setting with TableInfo<$SettingTable, SettingData> { String get actualTableName => $name; static const String $name = 'setting'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('key')) { context.handle( - _keyMeta, key.isAcceptableOrUnknown(data['key']!, _keyMeta)); + _keyMeta, + key.isAcceptableOrUnknown(data['key']!, _keyMeta), + ); } else if (isInserting) { context.missing(_keyMeta); } if (data.containsKey('value')) { context.handle( - _valueMeta, value.isAcceptableOrUnknown(data['value']!, _valueMeta)); + _valueMeta, + value.isAcceptableOrUnknown(data['value']!, _valueMeta), + ); } else if (isInserting) { context.missing(_valueMeta); } @@ -1233,10 +1491,16 @@ class $SettingTable extends Setting with TableInfo<$SettingTable, SettingData> { SettingData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return SettingData( - key: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}key'])!, - value: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}value'])!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, ); } @@ -1259,14 +1523,13 @@ class SettingData extends DataClass implements Insertable { } SettingCompanion toCompanion(bool nullToAbsent) { - return SettingCompanion( - key: Value(key), - value: Value(value), - ); + return SettingCompanion(key: Value(key), value: Value(value)); } - factory SettingData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return SettingData( key: serializer.fromJson(json['key']), @@ -1282,10 +1545,8 @@ class SettingData extends DataClass implements Insertable { }; } - SettingData copyWith({String? key, String? value}) => SettingData( - key: key ?? this.key, - value: value ?? this.value, - ); + SettingData copyWith({String? key, String? value}) => + SettingData(key: key ?? this.key, value: value ?? this.value); SettingData copyWithCompanion(SettingCompanion data) { return SettingData( key: data.key.present ? data.key.value : this.key, @@ -1325,8 +1586,8 @@ class SettingCompanion extends UpdateCompanion { required String key, required String value, this.rowid = const Value.absent(), - }) : key = Value(key), - value = Value(value); + }) : key = Value(key), + value = Value(value); static Insertable custom({ Expression? key, Expression? value, @@ -1339,8 +1600,11 @@ class SettingCompanion extends UpdateCompanion { }); } - SettingCompanion copyWith( - {Value? key, Value? value, Value? rowid}) { + SettingCompanion copyWith({ + Value? key, + Value? value, + Value? rowid, + }) { return SettingCompanion( key: key ?? this.key, value: value ?? this.value, @@ -1382,47 +1646,82 @@ class $FriendTable extends Friend with TableInfo<$FriendTable, FriendData> { static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _displayNameMeta = - const VerificationMeta('displayName'); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); @override late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _vaultIdMeta = - const VerificationMeta('vaultId'); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _vaultIdMeta = const VerificationMeta( + 'vaultId', + ); @override late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _keysMeta = const VerificationMeta('keys'); @override late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _updatedAtMeta = - const VerificationMeta('updatedAt'); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); @override late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); @override - List get $columns => - [id, name, displayName, vaultId, keys, updatedAt]; + List get $columns => [ + id, + name, + displayName, + vaultId, + keys, + updatedAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'friend'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -1432,33 +1731,44 @@ class $FriendTable extends Friend with TableInfo<$FriendTable, FriendData> { } if (data.containsKey('name')) { context.handle( - _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); } else if (isInserting) { context.missing(_nameMeta); } if (data.containsKey('display_name')) { context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, _displayNameMeta, - displayName.isAcceptableOrUnknown( - data['display_name']!, _displayNameMeta)); + ), + ); } else if (isInserting) { context.missing(_displayNameMeta); } if (data.containsKey('vault_id')) { - context.handle(_vaultIdMeta, - vaultId.isAcceptableOrUnknown(data['vault_id']!, _vaultIdMeta)); + context.handle( + _vaultIdMeta, + vaultId.isAcceptableOrUnknown(data['vault_id']!, _vaultIdMeta), + ); } else if (isInserting) { context.missing(_vaultIdMeta); } if (data.containsKey('keys')) { context.handle( - _keysMeta, keys.isAcceptableOrUnknown(data['keys']!, _keysMeta)); + _keysMeta, + keys.isAcceptableOrUnknown(data['keys']!, _keysMeta), + ); } else if (isInserting) { context.missing(_keysMeta); } if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); } else if (isInserting) { context.missing(_updatedAtMeta); } @@ -1471,18 +1781,36 @@ class $FriendTable extends Friend with TableInfo<$FriendTable, FriendData> { FriendData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return FriendData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, ); } @@ -1499,13 +1827,14 @@ class FriendData extends DataClass implements Insertable { final String vaultId; final String keys; final BigInt updatedAt; - const FriendData( - {required this.id, - required this.name, - required this.displayName, - required this.vaultId, - required this.keys, - required this.updatedAt}); + const FriendData({ + required this.id, + required this.name, + required this.displayName, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1529,8 +1858,10 @@ class FriendData extends DataClass implements Insertable { ); } - factory FriendData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory FriendData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return FriendData( id: serializer.fromJson(json['id']), @@ -1554,21 +1885,21 @@ class FriendData extends DataClass implements Insertable { }; } - FriendData copyWith( - {String? id, - String? name, - String? displayName, - String? vaultId, - String? keys, - BigInt? updatedAt}) => - FriendData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - vaultId: vaultId ?? this.vaultId, - keys: keys ?? this.keys, - updatedAt: updatedAt ?? this.updatedAt, - ); + FriendData copyWith({ + String? id, + String? name, + String? displayName, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => FriendData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); FriendData copyWithCompanion(FriendCompanion data) { return FriendData( id: data.id.present ? data.id.value : this.id, @@ -1634,12 +1965,12 @@ class FriendCompanion extends UpdateCompanion { required String keys, required BigInt updatedAt, this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - vaultId = Value(vaultId), - keys = Value(keys), - updatedAt = Value(updatedAt); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, Expression? name, @@ -1660,14 +1991,15 @@ class FriendCompanion extends UpdateCompanion { }); } - FriendCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? vaultId, - Value? keys, - Value? updatedAt, - Value? rowid}) { + FriendCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { return FriendCompanion( id: id ?? this.id, name: name ?? this.name, @@ -1729,55 +2061,95 @@ class $RequestTable extends Request with TableInfo<$RequestTable, RequestData> { static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _displayNameMeta = - const VerificationMeta('displayName'); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); @override late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _selfMeta = const VerificationMeta('self'); @override late final GeneratedColumn self = GeneratedColumn( - 'self', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("self" IN (0, 1))')); - static const VerificationMeta _vaultIdMeta = - const VerificationMeta('vaultId'); + 'self', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("self" IN (0, 1))', + ), + ); + static const VerificationMeta _vaultIdMeta = const VerificationMeta( + 'vaultId', + ); @override late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _keysMeta = const VerificationMeta('keys'); @override late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _updatedAtMeta = - const VerificationMeta('updatedAt'); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); @override late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); @override - List get $columns => - [id, name, displayName, self, vaultId, keys, updatedAt]; + List get $columns => [ + id, + name, + displayName, + self, + vaultId, + keys, + updatedAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'request'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -1787,39 +2159,52 @@ class $RequestTable extends Request with TableInfo<$RequestTable, RequestData> { } if (data.containsKey('name')) { context.handle( - _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); } else if (isInserting) { context.missing(_nameMeta); } if (data.containsKey('display_name')) { context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, _displayNameMeta, - displayName.isAcceptableOrUnknown( - data['display_name']!, _displayNameMeta)); + ), + ); } else if (isInserting) { context.missing(_displayNameMeta); } if (data.containsKey('self')) { context.handle( - _selfMeta, self.isAcceptableOrUnknown(data['self']!, _selfMeta)); + _selfMeta, + self.isAcceptableOrUnknown(data['self']!, _selfMeta), + ); } else if (isInserting) { context.missing(_selfMeta); } if (data.containsKey('vault_id')) { - context.handle(_vaultIdMeta, - vaultId.isAcceptableOrUnknown(data['vault_id']!, _vaultIdMeta)); + context.handle( + _vaultIdMeta, + vaultId.isAcceptableOrUnknown(data['vault_id']!, _vaultIdMeta), + ); } else if (isInserting) { context.missing(_vaultIdMeta); } if (data.containsKey('keys')) { context.handle( - _keysMeta, keys.isAcceptableOrUnknown(data['keys']!, _keysMeta)); + _keysMeta, + keys.isAcceptableOrUnknown(data['keys']!, _keysMeta), + ); } else if (isInserting) { context.missing(_keysMeta); } if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); } else if (isInserting) { context.missing(_updatedAtMeta); } @@ -1832,20 +2217,41 @@ class $RequestTable extends Request with TableInfo<$RequestTable, RequestData> { RequestData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return RequestData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - self: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}self'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + self: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}self'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, ); } @@ -1863,14 +2269,15 @@ class RequestData extends DataClass implements Insertable { final String vaultId; final String keys; final BigInt updatedAt; - const RequestData( - {required this.id, - required this.name, - required this.displayName, - required this.self, - required this.vaultId, - required this.keys, - required this.updatedAt}); + const RequestData({ + required this.id, + required this.name, + required this.displayName, + required this.self, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1896,8 +2303,10 @@ class RequestData extends DataClass implements Insertable { ); } - factory RequestData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory RequestData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return RequestData( id: serializer.fromJson(json['id']), @@ -1923,23 +2332,23 @@ class RequestData extends DataClass implements Insertable { }; } - RequestData copyWith( - {String? id, - String? name, - String? displayName, - bool? self, - String? vaultId, - String? keys, - BigInt? updatedAt}) => - RequestData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - self: self ?? this.self, - vaultId: vaultId ?? this.vaultId, - keys: keys ?? this.keys, - updatedAt: updatedAt ?? this.updatedAt, - ); + RequestData copyWith({ + String? id, + String? name, + String? displayName, + bool? self, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => RequestData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); RequestData copyWithCompanion(RequestCompanion data) { return RequestData( id: data.id.present ? data.id.value : this.id, @@ -2011,13 +2420,13 @@ class RequestCompanion extends UpdateCompanion { required String keys, required BigInt updatedAt, this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - self = Value(self), - vaultId = Value(vaultId), - keys = Value(keys), - updatedAt = Value(updatedAt); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + self = Value(self), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, Expression? name, @@ -2040,15 +2449,16 @@ class RequestCompanion extends UpdateCompanion { }); } - RequestCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? self, - Value? vaultId, - Value? keys, - Value? updatedAt, - Value? rowid}) { + RequestCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? self, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { return RequestCompanion( id: id ?? this.id, name: name ?? this.name, @@ -2116,34 +2526,71 @@ class $UnknownProfileTable extends UnknownProfile static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _displayNameMeta = - const VerificationMeta('displayName'); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); @override late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _keysMeta = const VerificationMeta('keys'); @override late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _lastFetchedMeta = const VerificationMeta( + 'lastFetched', + ); + @override + late final GeneratedColumn lastFetched = GeneratedColumn( + 'last_fetched', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: Constant(DateTime.fromMillisecondsSinceEpoch(0)), + ); @override - List get $columns => [id, name, displayName, keys]; + List get $columns => [ + id, + name, + displayName, + keys, + lastFetched, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'unknown_profile'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -2153,24 +2600,40 @@ class $UnknownProfileTable extends UnknownProfile } if (data.containsKey('name')) { context.handle( - _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); } else if (isInserting) { context.missing(_nameMeta); } if (data.containsKey('display_name')) { context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, _displayNameMeta, - displayName.isAcceptableOrUnknown( - data['display_name']!, _displayNameMeta)); + ), + ); } else if (isInserting) { context.missing(_displayNameMeta); } if (data.containsKey('keys')) { context.handle( - _keysMeta, keys.isAcceptableOrUnknown(data['keys']!, _keysMeta)); + _keysMeta, + keys.isAcceptableOrUnknown(data['keys']!, _keysMeta), + ); } else if (isInserting) { context.missing(_keysMeta); } + if (data.containsKey('last_fetched')) { + context.handle( + _lastFetchedMeta, + lastFetched.isAcceptableOrUnknown( + data['last_fetched']!, + _lastFetchedMeta, + ), + ); + } return context; } @@ -2180,14 +2643,31 @@ class $UnknownProfileTable extends UnknownProfile UnknownProfileData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return UnknownProfileData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + lastFetched: + attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_fetched'], + )!, ); } @@ -2203,11 +2683,14 @@ class UnknownProfileData extends DataClass final String name; final String displayName; final String keys; - const UnknownProfileData( - {required this.id, - required this.name, - required this.displayName, - required this.keys}); + final DateTime lastFetched; + const UnknownProfileData({ + required this.id, + required this.name, + required this.displayName, + required this.keys, + required this.lastFetched, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2215,6 +2698,7 @@ class UnknownProfileData extends DataClass map['name'] = Variable(name); map['display_name'] = Variable(displayName); map['keys'] = Variable(keys); + map['last_fetched'] = Variable(lastFetched); return map; } @@ -2224,17 +2708,21 @@ class UnknownProfileData extends DataClass name: Value(name), displayName: Value(displayName), keys: Value(keys), + lastFetched: Value(lastFetched), ); } - factory UnknownProfileData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory UnknownProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return UnknownProfileData( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), displayName: serializer.fromJson(json['displayName']), keys: serializer.fromJson(json['keys']), + lastFetched: serializer.fromJson(json['lastFetched']), ); } @override @@ -2245,17 +2733,23 @@ class UnknownProfileData extends DataClass 'name': serializer.toJson(name), 'displayName': serializer.toJson(displayName), 'keys': serializer.toJson(keys), + 'lastFetched': serializer.toJson(lastFetched), }; } - UnknownProfileData copyWith( - {String? id, String? name, String? displayName, String? keys}) => - UnknownProfileData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - keys: keys ?? this.keys, - ); + UnknownProfileData copyWith({ + String? id, + String? name, + String? displayName, + String? keys, + DateTime? lastFetched, + }) => UnknownProfileData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + lastFetched: lastFetched ?? this.lastFetched, + ); UnknownProfileData copyWithCompanion(UnknownProfileCompanion data) { return UnknownProfileData( id: data.id.present ? data.id.value : this.id, @@ -2263,6 +2757,8 @@ class UnknownProfileData extends DataClass displayName: data.displayName.present ? data.displayName.value : this.displayName, keys: data.keys.present ? data.keys.value : this.keys, + lastFetched: + data.lastFetched.present ? data.lastFetched.value : this.lastFetched, ); } @@ -2272,13 +2768,14 @@ class UnknownProfileData extends DataClass ..write('id: $id, ') ..write('name: $name, ') ..write('displayName: $displayName, ') - ..write('keys: $keys') + ..write('keys: $keys, ') + ..write('lastFetched: $lastFetched') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, name, displayName, keys); + int get hashCode => Object.hash(id, name, displayName, keys, lastFetched); @override bool operator ==(Object other) => identical(this, other) || @@ -2286,7 +2783,8 @@ class UnknownProfileData extends DataClass other.id == this.id && other.name == this.name && other.displayName == this.displayName && - other.keys == this.keys); + other.keys == this.keys && + other.lastFetched == this.lastFetched); } class UnknownProfileCompanion extends UpdateCompanion { @@ -2294,12 +2792,14 @@ class UnknownProfileCompanion extends UpdateCompanion { final Value name; final Value displayName; final Value keys; + final Value lastFetched; final Value rowid; const UnknownProfileCompanion({ this.id = const Value.absent(), this.name = const Value.absent(), this.displayName = const Value.absent(), this.keys = const Value.absent(), + this.lastFetched = const Value.absent(), this.rowid = const Value.absent(), }); UnknownProfileCompanion.insert({ @@ -2307,16 +2807,18 @@ class UnknownProfileCompanion extends UpdateCompanion { required String name, required String displayName, required String keys, + this.lastFetched = const Value.absent(), this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - keys = Value(keys); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + keys = Value(keys); static Insertable custom({ Expression? id, Expression? name, Expression? displayName, Expression? keys, + Expression? lastFetched, Expression? rowid, }) { return RawValuesInsertable({ @@ -2324,21 +2826,25 @@ class UnknownProfileCompanion extends UpdateCompanion { if (name != null) 'name': name, if (displayName != null) 'display_name': displayName, if (keys != null) 'keys': keys, + if (lastFetched != null) 'last_fetched': lastFetched, if (rowid != null) 'rowid': rowid, }); } - UnknownProfileCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? keys, - Value? rowid}) { + UnknownProfileCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? keys, + Value? lastFetched, + Value? rowid, + }) { return UnknownProfileCompanion( id: id ?? this.id, name: name ?? this.name, displayName: displayName ?? this.displayName, keys: keys ?? this.keys, + lastFetched: lastFetched ?? this.lastFetched, rowid: rowid ?? this.rowid, ); } @@ -2358,6 +2864,9 @@ class UnknownProfileCompanion extends UpdateCompanion { if (keys.present) { map['keys'] = Variable(keys.value); } + if (lastFetched.present) { + map['last_fetched'] = Variable(lastFetched.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -2371,6 +2880,7 @@ class UnknownProfileCompanion extends UpdateCompanion { ..write('name: $name, ') ..write('displayName: $displayName, ') ..write('keys: $keys, ') + ..write('lastFetched: $lastFetched, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -2385,19 +2895,32 @@ class $ProfileTable extends Profile with TableInfo<$ProfileTable, ProfileData> { static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _pictureContainerMeta = - const VerificationMeta('pictureContainer'); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _pictureContainerMeta = const VerificationMeta( + 'pictureContainer', + ); @override late final GeneratedColumn pictureContainer = GeneratedColumn( - 'picture_container', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'picture_container', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _dataMeta = const VerificationMeta('data'); @override late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [id, pictureContainer, data]; @override @@ -2406,8 +2929,10 @@ class $ProfileTable extends Profile with TableInfo<$ProfileTable, ProfileData> { String get actualTableName => $name; static const String $name = 'profile'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -2417,15 +2942,20 @@ class $ProfileTable extends Profile with TableInfo<$ProfileTable, ProfileData> { } if (data.containsKey('picture_container')) { context.handle( + _pictureContainerMeta, + pictureContainer.isAcceptableOrUnknown( + data['picture_container']!, _pictureContainerMeta, - pictureContainer.isAcceptableOrUnknown( - data['picture_container']!, _pictureContainerMeta)); + ), + ); } else if (isInserting) { context.missing(_pictureContainerMeta); } if (data.containsKey('data')) { context.handle( - _dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta)); + _dataMeta, + this.data.isAcceptableOrUnknown(data['data']!, _dataMeta), + ); } else if (isInserting) { context.missing(_dataMeta); } @@ -2438,12 +2968,21 @@ class $ProfileTable extends Profile with TableInfo<$ProfileTable, ProfileData> { ProfileData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ProfileData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - pictureContainer: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}picture_container'])!, - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + pictureContainer: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}picture_container'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, ); } @@ -2457,8 +2996,11 @@ class ProfileData extends DataClass implements Insertable { final String id; final String pictureContainer; final String data; - const ProfileData( - {required this.id, required this.pictureContainer, required this.data}); + const ProfileData({ + required this.id, + required this.pictureContainer, + required this.data, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2476,8 +3018,10 @@ class ProfileData extends DataClass implements Insertable { ); } - factory ProfileData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return ProfileData( id: serializer.fromJson(json['id']), @@ -2504,9 +3048,10 @@ class ProfileData extends DataClass implements Insertable { ProfileData copyWithCompanion(ProfileCompanion data) { return ProfileData( id: data.id.present ? data.id.value : this.id, - pictureContainer: data.pictureContainer.present - ? data.pictureContainer.value - : this.pictureContainer, + pictureContainer: + data.pictureContainer.present + ? data.pictureContainer.value + : this.pictureContainer, data: data.data.present ? data.data.value : this.data, ); } @@ -2548,9 +3093,9 @@ class ProfileCompanion extends UpdateCompanion { required String pictureContainer, required String data, this.rowid = const Value.absent(), - }) : id = Value(id), - pictureContainer = Value(pictureContainer), - data = Value(data); + }) : id = Value(id), + pictureContainer = Value(pictureContainer), + data = Value(data); static Insertable custom({ Expression? id, Expression? pictureContainer, @@ -2565,11 +3110,12 @@ class ProfileCompanion extends UpdateCompanion { }); } - ProfileCompanion copyWith( - {Value? id, - Value? pictureContainer, - Value? data, - Value? rowid}) { + ProfileCompanion copyWith({ + Value? id, + Value? pictureContainer, + Value? data, + Value? rowid, + }) { return ProfileCompanion( id: id ?? this.id, pictureContainer: pictureContainer ?? this.pictureContainer, @@ -2617,8 +3163,12 @@ class $TrustedLinkTable extends TrustedLink static const VerificationMeta _domainMeta = const VerificationMeta('domain'); @override late final GeneratedColumn domain = GeneratedColumn( - 'domain', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'domain', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [domain]; @override @@ -2627,13 +3177,17 @@ class $TrustedLinkTable extends TrustedLink String get actualTableName => $name; static const String $name = 'trusted_link'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('domain')) { - context.handle(_domainMeta, - domain.isAcceptableOrUnknown(data['domain']!, _domainMeta)); + context.handle( + _domainMeta, + domain.isAcceptableOrUnknown(data['domain']!, _domainMeta), + ); } else if (isInserting) { context.missing(_domainMeta); } @@ -2646,8 +3200,11 @@ class $TrustedLinkTable extends TrustedLink TrustedLinkData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return TrustedLinkData( - domain: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}domain'])!, + domain: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}domain'], + )!, ); } @@ -2668,29 +3225,24 @@ class TrustedLinkData extends DataClass implements Insertable { } TrustedLinkCompanion toCompanion(bool nullToAbsent) { - return TrustedLinkCompanion( - domain: Value(domain), - ); + return TrustedLinkCompanion(domain: Value(domain)); } - factory TrustedLinkData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory TrustedLinkData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return TrustedLinkData( - domain: serializer.fromJson(json['domain']), - ); + return TrustedLinkData(domain: serializer.fromJson(json['domain'])); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'domain': serializer.toJson(domain), - }; + return {'domain': serializer.toJson(domain)}; } - TrustedLinkData copyWith({String? domain}) => TrustedLinkData( - domain: domain ?? this.domain, - ); + TrustedLinkData copyWith({String? domain}) => + TrustedLinkData(domain: domain ?? this.domain); TrustedLinkData copyWithCompanion(TrustedLinkCompanion data) { return TrustedLinkData( domain: data.domain.present ? data.domain.value : this.domain, @@ -2772,46 +3324,91 @@ class $LibraryEntryTable extends LibraryEntry static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _typeMeta = const VerificationMeta('type'); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override late final GeneratedColumnWithTypeConverter type = - GeneratedColumn('type', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true) - .withConverter($LibraryEntryTable.$convertertype); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ).withConverter($LibraryEntryTable.$convertertype); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); @override late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + static const VerificationMeta _identifierHashMeta = const VerificationMeta( + 'identifierHash', + ); + @override + late final GeneratedColumn identifierHash = GeneratedColumn( + 'identifier_hash', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant("to-migrate"), + ); static const VerificationMeta _dataMeta = const VerificationMeta('data'); @override late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _widthMeta = const VerificationMeta('width'); @override late final GeneratedColumn width = GeneratedColumn( - 'width', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); static const VerificationMeta _heightMeta = const VerificationMeta('height'); @override late final GeneratedColumn height = GeneratedColumn( - 'height', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); @override - List get $columns => - [id, type, createdAt, data, width, height]; + List get $columns => [ + id, + type, + createdAt, + identifierHash, + data, + width, + height, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'library_entry'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -2819,28 +3416,44 @@ class $LibraryEntryTable extends LibraryEntry } else if (isInserting) { context.missing(_idMeta); } - context.handle(_typeMeta, const VerificationResult.success()); if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); } else if (isInserting) { context.missing(_createdAtMeta); } + if (data.containsKey('identifier_hash')) { + context.handle( + _identifierHashMeta, + identifierHash.isAcceptableOrUnknown( + data['identifier_hash']!, + _identifierHashMeta, + ), + ); + } if (data.containsKey('data')) { context.handle( - _dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta)); + _dataMeta, + this.data.isAcceptableOrUnknown(data['data']!, _dataMeta), + ); } else if (isInserting) { context.missing(_dataMeta); } if (data.containsKey('width')) { context.handle( - _widthMeta, width.isAcceptableOrUnknown(data['width']!, _widthMeta)); + _widthMeta, + width.isAcceptableOrUnknown(data['width']!, _widthMeta), + ); } else if (isInserting) { context.missing(_widthMeta); } if (data.containsKey('height')) { - context.handle(_heightMeta, - height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + context.handle( + _heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta), + ); } else if (isInserting) { context.missing(_heightMeta); } @@ -2853,19 +3466,42 @@ class $LibraryEntryTable extends LibraryEntry LibraryEntryData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return LibraryEntryData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - type: $LibraryEntryTable.$convertertype.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}type'])!), - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}created_at'])!, - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, - width: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}width'])!, - height: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}height'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + type: $LibraryEntryTable.$convertertype.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + ), + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + identifierHash: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}identifier_hash'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + width: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + )!, + height: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + )!, ); } @@ -2883,25 +3519,30 @@ class LibraryEntryData extends DataClass final String id; final LibraryEntryType type; final BigInt createdAt; + final String identifierHash; final String data; final int width; final int height; - const LibraryEntryData( - {required this.id, - required this.type, - required this.createdAt, - required this.data, - required this.width, - required this.height}); + const LibraryEntryData({ + required this.id, + required this.type, + required this.createdAt, + required this.identifierHash, + required this.data, + required this.width, + required this.height, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); { - map['type'] = - Variable($LibraryEntryTable.$convertertype.toSql(type)); + map['type'] = Variable( + $LibraryEntryTable.$convertertype.toSql(type), + ); } map['created_at'] = Variable(createdAt); + map['identifier_hash'] = Variable(identifierHash); map['data'] = Variable(data); map['width'] = Variable(width); map['height'] = Variable(height); @@ -2913,20 +3554,25 @@ class LibraryEntryData extends DataClass id: Value(id), type: Value(type), createdAt: Value(createdAt), + identifierHash: Value(identifierHash), data: Value(data), width: Value(width), height: Value(height), ); } - factory LibraryEntryData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory LibraryEntryData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return LibraryEntryData( id: serializer.fromJson(json['id']), - type: $LibraryEntryTable.$convertertype - .fromJson(serializer.fromJson(json['type'])), + type: $LibraryEntryTable.$convertertype.fromJson( + serializer.fromJson(json['type']), + ), createdAt: serializer.fromJson(json['createdAt']), + identifierHash: serializer.fromJson(json['identifierHash']), data: serializer.fromJson(json['data']), width: serializer.fromJson(json['width']), height: serializer.fromJson(json['height']), @@ -2937,35 +3583,43 @@ class LibraryEntryData extends DataClass serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'type': serializer - .toJson($LibraryEntryTable.$convertertype.toJson(type)), + 'type': serializer.toJson( + $LibraryEntryTable.$convertertype.toJson(type), + ), 'createdAt': serializer.toJson(createdAt), + 'identifierHash': serializer.toJson(identifierHash), 'data': serializer.toJson(data), 'width': serializer.toJson(width), 'height': serializer.toJson(height), }; } - LibraryEntryData copyWith( - {String? id, - LibraryEntryType? type, - BigInt? createdAt, - String? data, - int? width, - int? height}) => - LibraryEntryData( - id: id ?? this.id, - type: type ?? this.type, - createdAt: createdAt ?? this.createdAt, - data: data ?? this.data, - width: width ?? this.width, - height: height ?? this.height, - ); + LibraryEntryData copyWith({ + String? id, + LibraryEntryType? type, + BigInt? createdAt, + String? identifierHash, + String? data, + int? width, + int? height, + }) => LibraryEntryData( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + identifierHash: identifierHash ?? this.identifierHash, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + ); LibraryEntryData copyWithCompanion(LibraryEntryCompanion data) { return LibraryEntryData( id: data.id.present ? data.id.value : this.id, type: data.type.present ? data.type.value : this.type, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + identifierHash: + data.identifierHash.present + ? data.identifierHash.value + : this.identifierHash, data: data.data.present ? data.data.value : this.data, width: data.width.present ? data.width.value : this.width, height: data.height.present ? data.height.value : this.height, @@ -2978,6 +3632,7 @@ class LibraryEntryData extends DataClass ..write('id: $id, ') ..write('type: $type, ') ..write('createdAt: $createdAt, ') + ..write('identifierHash: $identifierHash, ') ..write('data: $data, ') ..write('width: $width, ') ..write('height: $height') @@ -2986,7 +3641,8 @@ class LibraryEntryData extends DataClass } @override - int get hashCode => Object.hash(id, type, createdAt, data, width, height); + int get hashCode => + Object.hash(id, type, createdAt, identifierHash, data, width, height); @override bool operator ==(Object other) => identical(this, other) || @@ -2994,6 +3650,7 @@ class LibraryEntryData extends DataClass other.id == this.id && other.type == this.type && other.createdAt == this.createdAt && + other.identifierHash == this.identifierHash && other.data == this.data && other.width == this.width && other.height == this.height); @@ -3003,6 +3660,7 @@ class LibraryEntryCompanion extends UpdateCompanion { final Value id; final Value type; final Value createdAt; + final Value identifierHash; final Value data; final Value width; final Value height; @@ -3011,6 +3669,7 @@ class LibraryEntryCompanion extends UpdateCompanion { this.id = const Value.absent(), this.type = const Value.absent(), this.createdAt = const Value.absent(), + this.identifierHash = const Value.absent(), this.data = const Value.absent(), this.width = const Value.absent(), this.height = const Value.absent(), @@ -3020,20 +3679,22 @@ class LibraryEntryCompanion extends UpdateCompanion { required String id, required LibraryEntryType type, required BigInt createdAt, + this.identifierHash = const Value.absent(), required String data, required int width, required int height, this.rowid = const Value.absent(), - }) : id = Value(id), - type = Value(type), - createdAt = Value(createdAt), - data = Value(data), - width = Value(width), - height = Value(height); + }) : id = Value(id), + type = Value(type), + createdAt = Value(createdAt), + data = Value(data), + width = Value(width), + height = Value(height); static Insertable custom({ Expression? id, Expression? type, Expression? createdAt, + Expression? identifierHash, Expression? data, Expression? width, Expression? height, @@ -3043,6 +3704,7 @@ class LibraryEntryCompanion extends UpdateCompanion { if (id != null) 'id': id, if (type != null) 'type': type, if (createdAt != null) 'created_at': createdAt, + if (identifierHash != null) 'identifier_hash': identifierHash, if (data != null) 'data': data, if (width != null) 'width': width, if (height != null) 'height': height, @@ -3050,18 +3712,21 @@ class LibraryEntryCompanion extends UpdateCompanion { }); } - LibraryEntryCompanion copyWith( - {Value? id, - Value? type, - Value? createdAt, - Value? data, - Value? width, - Value? height, - Value? rowid}) { + LibraryEntryCompanion copyWith({ + Value? id, + Value? type, + Value? createdAt, + Value? identifierHash, + Value? data, + Value? width, + Value? height, + Value? rowid, + }) { return LibraryEntryCompanion( id: id ?? this.id, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, + identifierHash: identifierHash ?? this.identifierHash, data: data ?? this.data, width: width ?? this.width, height: height ?? this.height, @@ -3076,12 +3741,16 @@ class LibraryEntryCompanion extends UpdateCompanion { map['id'] = Variable(id.value); } if (type.present) { - map['type'] = - Variable($LibraryEntryTable.$convertertype.toSql(type.value)); + map['type'] = Variable( + $LibraryEntryTable.$convertertype.toSql(type.value), + ); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (identifierHash.present) { + map['identifier_hash'] = Variable(identifierHash.value); + } if (data.present) { map['data'] = Variable(data.value); } @@ -3103,6 +3772,7 @@ class LibraryEntryCompanion extends UpdateCompanion { ..write('id: $id, ') ..write('type: $type, ') ..write('createdAt: $createdAt, ') + ..write('identifierHash: $identifierHash, ') ..write('data: $data, ') ..write('width: $width, ') ..write('height: $height, ') @@ -3125,50 +3795,85 @@ abstract class _$Database extends GeneratedDatabase { late final $ProfileTable profile = $ProfileTable(this); late final $TrustedLinkTable trustedLink = $TrustedLinkTable(this); late final $LibraryEntryTable libraryEntry = $LibraryEntryTable(this); + late final Index idxConversationUpdated = Index( + 'idx_conversation_updated', + 'CREATE INDEX idx_conversation_updated ON conversation (updated_at)', + ); + late final Index idxMessageCreated = Index( + 'idx_message_created', + 'CREATE INDEX idx_message_created ON message (created_at)', + ); + late final Index idxFriendsUpdated = Index( + 'idx_friends_updated', + 'CREATE INDEX idx_friends_updated ON friend (updated_at)', + ); + late final Index idxRequestsUpdated = Index( + 'idx_requests_updated', + 'CREATE INDEX idx_requests_updated ON request (updated_at)', + ); + late final Index idxUnknownProfilesLastFetched = Index( + 'idx_unknown_profiles_last_fetched', + 'CREATE INDEX idx_unknown_profiles_last_fetched ON unknown_profile (last_fetched)', + ); + late final Index idxLibraryEntryCreated = Index( + 'idx_library_entry_created', + 'CREATE INDEX idx_library_entry_created ON library_entry (created_at)', + ); + late final Index idxLibraryEntryIdhash = Index( + 'idx_library_entry_idhash', + 'CREATE INDEX idx_library_entry_idhash ON library_entry (identifier_hash)', + ); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override List get allSchemaEntities => [ - conversation, - message, - member, - setting, - friend, - request, - unknownProfile, - profile, - trustedLink, - libraryEntry - ]; + conversation, + message, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + idxConversationUpdated, + idxMessageCreated, + idxFriendsUpdated, + idxRequestsUpdated, + idxUnknownProfilesLastFetched, + idxLibraryEntryCreated, + idxLibraryEntryIdhash, + ]; } -typedef $$ConversationTableCreateCompanionBuilder = ConversationCompanion - Function({ - required String id, - required String vaultId, - required ConversationType type, - required String data, - required String token, - required String key, - required BigInt lastVersion, - required BigInt updatedAt, - required BigInt readAt, - Value rowid, -}); -typedef $$ConversationTableUpdateCompanionBuilder = ConversationCompanion - Function({ - Value id, - Value vaultId, - Value type, - Value data, - Value token, - Value key, - Value lastVersion, - Value updatedAt, - Value readAt, - Value rowid, -}); +typedef $$ConversationTableCreateCompanionBuilder = + ConversationCompanion Function({ + required String id, + required String vaultId, + required ConversationType type, + required String data, + required String token, + required String key, + required BigInt lastVersion, + required BigInt updatedAt, + Value reads, + Value rowid, + }); +typedef $$ConversationTableUpdateCompanionBuilder = + ConversationCompanion Function({ + Value id, + Value vaultId, + Value type, + Value data, + Value token, + Value key, + Value lastVersion, + Value updatedAt, + Value reads, + Value rowid, + }); class $$ConversationTableFilterComposer extends Composer<_$Database, $ConversationTable> { @@ -3180,33 +3885,50 @@ class $$ConversationTableFilterComposer super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + column: $table.id, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get vaultId => $composableBuilder( - column: $table.vaultId, builder: (column) => ColumnFilters(column)); + column: $table.vaultId, + builder: (column) => ColumnFilters(column), + ); ColumnWithTypeConverterFilters - get type => $composableBuilder( - column: $table.type, - builder: (column) => ColumnWithTypeConverterFilters(column)); + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); ColumnFilters get data => $composableBuilder( - column: $table.data, builder: (column) => ColumnFilters(column)); + column: $table.data, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get token => $composableBuilder( - column: $table.token, builder: (column) => ColumnFilters(column)); + column: $table.token, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get key => $composableBuilder( - column: $table.key, builder: (column) => ColumnFilters(column)); + column: $table.key, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get lastVersion => $composableBuilder( - column: $table.lastVersion, builder: (column) => ColumnFilters(column)); + column: $table.lastVersion, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get readAt => $composableBuilder( - column: $table.readAt, builder: (column) => ColumnFilters(column)); + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get reads => $composableBuilder( + column: $table.reads, + builder: (column) => ColumnFilters(column), + ); } class $$ConversationTableOrderingComposer @@ -3219,31 +3941,49 @@ class $$ConversationTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get vaultId => $composableBuilder( - column: $table.vaultId, builder: (column) => ColumnOrderings(column)); + column: $table.vaultId, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get data => $composableBuilder( - column: $table.data, builder: (column) => ColumnOrderings(column)); + column: $table.data, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get token => $composableBuilder( - column: $table.token, builder: (column) => ColumnOrderings(column)); + column: $table.token, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get key => $composableBuilder( - column: $table.key, builder: (column) => ColumnOrderings(column)); + column: $table.key, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get lastVersion => $composableBuilder( - column: $table.lastVersion, builder: (column) => ColumnOrderings(column)); + column: $table.lastVersion, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get readAt => $composableBuilder( - column: $table.readAt, builder: (column) => ColumnOrderings(column)); + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get reads => $composableBuilder( + column: $table.reads, + builder: (column) => ColumnOrderings(column), + ); } class $$ConversationTableAnnotationComposer @@ -3274,132 +4014,151 @@ class $$ConversationTableAnnotationComposer $composableBuilder(column: $table.key, builder: (column) => column); GeneratedColumn get lastVersion => $composableBuilder( - column: $table.lastVersion, builder: (column) => column); + column: $table.lastVersion, + builder: (column) => column, + ); GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumn get readAt => - $composableBuilder(column: $table.readAt, builder: (column) => column); + GeneratedColumn get reads => + $composableBuilder(column: $table.reads, builder: (column) => column); } -class $$ConversationTableTableManager extends RootTableManager< - _$Database, - $ConversationTable, - ConversationData, - $$ConversationTableFilterComposer, - $$ConversationTableOrderingComposer, - $$ConversationTableAnnotationComposer, - $$ConversationTableCreateCompanionBuilder, - $$ConversationTableUpdateCompanionBuilder, - ( - ConversationData, - BaseReferences<_$Database, $ConversationTable, ConversationData> - ), - ConversationData, - PrefetchHooks Function()> { +class $$ConversationTableTableManager + extends + RootTableManager< + _$Database, + $ConversationTable, + ConversationData, + $$ConversationTableFilterComposer, + $$ConversationTableOrderingComposer, + $$ConversationTableAnnotationComposer, + $$ConversationTableCreateCompanionBuilder, + $$ConversationTableUpdateCompanionBuilder, + ( + ConversationData, + BaseReferences<_$Database, $ConversationTable, ConversationData>, + ), + ConversationData, + PrefetchHooks Function() + > { $$ConversationTableTableManager(_$Database db, $ConversationTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$ConversationTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$ConversationTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$ConversationTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value vaultId = const Value.absent(), - Value type = const Value.absent(), - Value data = const Value.absent(), - Value token = const Value.absent(), - Value key = const Value.absent(), - Value lastVersion = const Value.absent(), - Value updatedAt = const Value.absent(), - Value readAt = const Value.absent(), - Value rowid = const Value.absent(), - }) => - ConversationCompanion( - id: id, - vaultId: vaultId, - type: type, - data: data, - token: token, - key: key, - lastVersion: lastVersion, - updatedAt: updatedAt, - readAt: readAt, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required String vaultId, - required ConversationType type, - required String data, - required String token, - required String key, - required BigInt lastVersion, - required BigInt updatedAt, - required BigInt readAt, - Value rowid = const Value.absent(), - }) => - ConversationCompanion.insert( - id: id, - vaultId: vaultId, - type: type, - data: data, - token: token, - key: key, - lastVersion: lastVersion, - updatedAt: updatedAt, - readAt: readAt, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$ConversationTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$ConversationTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => + $$ConversationTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value vaultId = const Value.absent(), + Value type = const Value.absent(), + Value data = const Value.absent(), + Value token = const Value.absent(), + Value key = const Value.absent(), + Value lastVersion = const Value.absent(), + Value updatedAt = const Value.absent(), + Value reads = const Value.absent(), + Value rowid = const Value.absent(), + }) => ConversationCompanion( + id: id, + vaultId: vaultId, + type: type, + data: data, + token: token, + key: key, + lastVersion: lastVersion, + updatedAt: updatedAt, + reads: reads, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String vaultId, + required ConversationType type, + required String data, + required String token, + required String key, + required BigInt lastVersion, + required BigInt updatedAt, + Value reads = const Value.absent(), + Value rowid = const Value.absent(), + }) => ConversationCompanion.insert( + id: id, + vaultId: vaultId, + type: type, + data: data, + token: token, + key: key, + lastVersion: lastVersion, + updatedAt: updatedAt, + reads: reads, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$ConversationTableProcessedTableManager = ProcessedTableManager< - _$Database, - $ConversationTable, - ConversationData, - $$ConversationTableFilterComposer, - $$ConversationTableOrderingComposer, - $$ConversationTableAnnotationComposer, - $$ConversationTableCreateCompanionBuilder, - $$ConversationTableUpdateCompanionBuilder, - ( +typedef $$ConversationTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $ConversationTable, ConversationData, - BaseReferences<_$Database, $ConversationTable, ConversationData> - ), - ConversationData, - PrefetchHooks Function()>; -typedef $$MessageTableCreateCompanionBuilder = MessageCompanion Function({ - required String id, - required String content, - required String senderToken, - required String senderAddress, - required BigInt createdAt, - required String conversation, - required bool edited, - required bool verified, - Value rowid, -}); -typedef $$MessageTableUpdateCompanionBuilder = MessageCompanion Function({ - Value id, - Value content, - Value senderToken, - Value senderAddress, - Value createdAt, - Value conversation, - Value edited, - Value verified, - Value rowid, -}); + $$ConversationTableFilterComposer, + $$ConversationTableOrderingComposer, + $$ConversationTableAnnotationComposer, + $$ConversationTableCreateCompanionBuilder, + $$ConversationTableUpdateCompanionBuilder, + ( + ConversationData, + BaseReferences<_$Database, $ConversationTable, ConversationData>, + ), + ConversationData, + PrefetchHooks Function() + >; +typedef $$MessageTableCreateCompanionBuilder = + MessageCompanion Function({ + required String id, + required String content, + required String senderToken, + required String senderAddress, + required BigInt createdAt, + required String conversation, + required bool edited, + required bool verified, + Value rowid, + }); +typedef $$MessageTableUpdateCompanionBuilder = + MessageCompanion Function({ + Value id, + Value content, + Value senderToken, + Value senderAddress, + Value createdAt, + Value conversation, + Value edited, + Value verified, + Value rowid, + }); class $$MessageTableFilterComposer extends Composer<_$Database, $MessageTable> { $$MessageTableFilterComposer({ @@ -3410,28 +4169,44 @@ class $$MessageTableFilterComposer extends Composer<_$Database, $MessageTable> { super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + column: $table.id, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get content => $composableBuilder( - column: $table.content, builder: (column) => ColumnFilters(column)); + column: $table.content, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get senderToken => $composableBuilder( - column: $table.senderToken, builder: (column) => ColumnFilters(column)); + column: $table.senderToken, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get senderAddress => $composableBuilder( - column: $table.senderAddress, builder: (column) => ColumnFilters(column)); + column: $table.senderAddress, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnFilters(column)); + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get conversation => $composableBuilder( - column: $table.conversation, builder: (column) => ColumnFilters(column)); + column: $table.conversation, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get edited => $composableBuilder( - column: $table.edited, builder: (column) => ColumnFilters(column)); + column: $table.edited, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get verified => $composableBuilder( - column: $table.verified, builder: (column) => ColumnFilters(column)); + column: $table.verified, + builder: (column) => ColumnFilters(column), + ); } class $$MessageTableOrderingComposer @@ -3444,30 +4219,44 @@ class $$MessageTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get content => $composableBuilder( - column: $table.content, builder: (column) => ColumnOrderings(column)); + column: $table.content, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get senderToken => $composableBuilder( - column: $table.senderToken, builder: (column) => ColumnOrderings(column)); + column: $table.senderToken, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get senderAddress => $composableBuilder( - column: $table.senderAddress, - builder: (column) => ColumnOrderings(column)); + column: $table.senderAddress, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get conversation => $composableBuilder( - column: $table.conversation, - builder: (column) => ColumnOrderings(column)); + column: $table.conversation, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get edited => $composableBuilder( - column: $table.edited, builder: (column) => ColumnOrderings(column)); + column: $table.edited, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get verified => $composableBuilder( - column: $table.verified, builder: (column) => ColumnOrderings(column)); + column: $table.verified, + builder: (column) => ColumnOrderings(column), + ); } class $$MessageTableAnnotationComposer @@ -3486,16 +4275,22 @@ class $$MessageTableAnnotationComposer $composableBuilder(column: $table.content, builder: (column) => column); GeneratedColumn get senderToken => $composableBuilder( - column: $table.senderToken, builder: (column) => column); + column: $table.senderToken, + builder: (column) => column, + ); GeneratedColumn get senderAddress => $composableBuilder( - column: $table.senderAddress, builder: (column) => column); + column: $table.senderAddress, + builder: (column) => column, + ); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); GeneratedColumn get conversation => $composableBuilder( - column: $table.conversation, builder: (column) => column); + column: $table.conversation, + builder: (column) => column, + ); GeneratedColumn get edited => $composableBuilder(column: $table.edited, builder: (column) => column); @@ -3504,105 +4299,121 @@ class $$MessageTableAnnotationComposer $composableBuilder(column: $table.verified, builder: (column) => column); } -class $$MessageTableTableManager extends RootTableManager< - _$Database, - $MessageTable, - MessageData, - $$MessageTableFilterComposer, - $$MessageTableOrderingComposer, - $$MessageTableAnnotationComposer, - $$MessageTableCreateCompanionBuilder, - $$MessageTableUpdateCompanionBuilder, - (MessageData, BaseReferences<_$Database, $MessageTable, MessageData>), - MessageData, - PrefetchHooks Function()> { +class $$MessageTableTableManager + extends + RootTableManager< + _$Database, + $MessageTable, + MessageData, + $$MessageTableFilterComposer, + $$MessageTableOrderingComposer, + $$MessageTableAnnotationComposer, + $$MessageTableCreateCompanionBuilder, + $$MessageTableUpdateCompanionBuilder, + (MessageData, BaseReferences<_$Database, $MessageTable, MessageData>), + MessageData, + PrefetchHooks Function() + > { $$MessageTableTableManager(_$Database db, $MessageTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$MessageTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$MessageTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$MessageTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value content = const Value.absent(), - Value senderToken = const Value.absent(), - Value senderAddress = const Value.absent(), - Value createdAt = const Value.absent(), - Value conversation = const Value.absent(), - Value edited = const Value.absent(), - Value verified = const Value.absent(), - Value rowid = const Value.absent(), - }) => - MessageCompanion( - id: id, - content: content, - senderToken: senderToken, - senderAddress: senderAddress, - createdAt: createdAt, - conversation: conversation, - edited: edited, - verified: verified, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required String content, - required String senderToken, - required String senderAddress, - required BigInt createdAt, - required String conversation, - required bool edited, - required bool verified, - Value rowid = const Value.absent(), - }) => - MessageCompanion.insert( - id: id, - content: content, - senderToken: senderToken, - senderAddress: senderAddress, - createdAt: createdAt, - conversation: conversation, - edited: edited, - verified: verified, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$MessageTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$MessageTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$MessageTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value content = const Value.absent(), + Value senderToken = const Value.absent(), + Value senderAddress = const Value.absent(), + Value createdAt = const Value.absent(), + Value conversation = const Value.absent(), + Value edited = const Value.absent(), + Value verified = const Value.absent(), + Value rowid = const Value.absent(), + }) => MessageCompanion( + id: id, + content: content, + senderToken: senderToken, + senderAddress: senderAddress, + createdAt: createdAt, + conversation: conversation, + edited: edited, + verified: verified, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String content, + required String senderToken, + required String senderAddress, + required BigInt createdAt, + required String conversation, + required bool edited, + required bool verified, + Value rowid = const Value.absent(), + }) => MessageCompanion.insert( + id: id, + content: content, + senderToken: senderToken, + senderAddress: senderAddress, + createdAt: createdAt, + conversation: conversation, + edited: edited, + verified: verified, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$MessageTableProcessedTableManager = ProcessedTableManager< - _$Database, - $MessageTable, - MessageData, - $$MessageTableFilterComposer, - $$MessageTableOrderingComposer, - $$MessageTableAnnotationComposer, - $$MessageTableCreateCompanionBuilder, - $$MessageTableUpdateCompanionBuilder, - (MessageData, BaseReferences<_$Database, $MessageTable, MessageData>), - MessageData, - PrefetchHooks Function()>; -typedef $$MemberTableCreateCompanionBuilder = MemberCompanion Function({ - required String id, - Value conversationId, - required String accountId, - required int roleId, - Value rowid, -}); -typedef $$MemberTableUpdateCompanionBuilder = MemberCompanion Function({ - Value id, - Value conversationId, - Value accountId, - Value roleId, - Value rowid, -}); +typedef $$MessageTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $MessageTable, + MessageData, + $$MessageTableFilterComposer, + $$MessageTableOrderingComposer, + $$MessageTableAnnotationComposer, + $$MessageTableCreateCompanionBuilder, + $$MessageTableUpdateCompanionBuilder, + (MessageData, BaseReferences<_$Database, $MessageTable, MessageData>), + MessageData, + PrefetchHooks Function() + >; +typedef $$MemberTableCreateCompanionBuilder = + MemberCompanion Function({ + required String id, + Value conversationId, + required String accountId, + required int roleId, + Value rowid, + }); +typedef $$MemberTableUpdateCompanionBuilder = + MemberCompanion Function({ + Value id, + Value conversationId, + Value accountId, + Value roleId, + Value rowid, + }); class $$MemberTableFilterComposer extends Composer<_$Database, $MemberTable> { $$MemberTableFilterComposer({ @@ -3613,17 +4424,24 @@ class $$MemberTableFilterComposer extends Composer<_$Database, $MemberTable> { super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + column: $table.id, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get conversationId => $composableBuilder( - column: $table.conversationId, - builder: (column) => ColumnFilters(column)); + column: $table.conversationId, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get accountId => $composableBuilder( - column: $table.accountId, builder: (column) => ColumnFilters(column)); + column: $table.accountId, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get roleId => $composableBuilder( - column: $table.roleId, builder: (column) => ColumnFilters(column)); + column: $table.roleId, + builder: (column) => ColumnFilters(column), + ); } class $$MemberTableOrderingComposer extends Composer<_$Database, $MemberTable> { @@ -3635,17 +4453,24 @@ class $$MemberTableOrderingComposer extends Composer<_$Database, $MemberTable> { super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get conversationId => $composableBuilder( - column: $table.conversationId, - builder: (column) => ColumnOrderings(column)); + column: $table.conversationId, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get accountId => $composableBuilder( - column: $table.accountId, builder: (column) => ColumnOrderings(column)); + column: $table.accountId, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get roleId => $composableBuilder( - column: $table.roleId, builder: (column) => ColumnOrderings(column)); + column: $table.roleId, + builder: (column) => ColumnOrderings(column), + ); } class $$MemberTableAnnotationComposer @@ -3661,7 +4486,9 @@ class $$MemberTableAnnotationComposer $composableBuilder(column: $table.id, builder: (column) => column); GeneratedColumn get conversationId => $composableBuilder( - column: $table.conversationId, builder: (column) => column); + column: $table.conversationId, + builder: (column) => column, + ); GeneratedColumn get accountId => $composableBuilder(column: $table.accountId, builder: (column) => column); @@ -3670,85 +4497,101 @@ class $$MemberTableAnnotationComposer $composableBuilder(column: $table.roleId, builder: (column) => column); } -class $$MemberTableTableManager extends RootTableManager< - _$Database, - $MemberTable, - MemberData, - $$MemberTableFilterComposer, - $$MemberTableOrderingComposer, - $$MemberTableAnnotationComposer, - $$MemberTableCreateCompanionBuilder, - $$MemberTableUpdateCompanionBuilder, - (MemberData, BaseReferences<_$Database, $MemberTable, MemberData>), - MemberData, - PrefetchHooks Function()> { +class $$MemberTableTableManager + extends + RootTableManager< + _$Database, + $MemberTable, + MemberData, + $$MemberTableFilterComposer, + $$MemberTableOrderingComposer, + $$MemberTableAnnotationComposer, + $$MemberTableCreateCompanionBuilder, + $$MemberTableUpdateCompanionBuilder, + (MemberData, BaseReferences<_$Database, $MemberTable, MemberData>), + MemberData, + PrefetchHooks Function() + > { $$MemberTableTableManager(_$Database db, $MemberTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$MemberTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$MemberTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$MemberTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value conversationId = const Value.absent(), - Value accountId = const Value.absent(), - Value roleId = const Value.absent(), - Value rowid = const Value.absent(), - }) => - MemberCompanion( - id: id, - conversationId: conversationId, - accountId: accountId, - roleId: roleId, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - Value conversationId = const Value.absent(), - required String accountId, - required int roleId, - Value rowid = const Value.absent(), - }) => - MemberCompanion.insert( - id: id, - conversationId: conversationId, - accountId: accountId, - roleId: roleId, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$MemberTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$MemberTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$MemberTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value conversationId = const Value.absent(), + Value accountId = const Value.absent(), + Value roleId = const Value.absent(), + Value rowid = const Value.absent(), + }) => MemberCompanion( + id: id, + conversationId: conversationId, + accountId: accountId, + roleId: roleId, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + Value conversationId = const Value.absent(), + required String accountId, + required int roleId, + Value rowid = const Value.absent(), + }) => MemberCompanion.insert( + id: id, + conversationId: conversationId, + accountId: accountId, + roleId: roleId, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$MemberTableProcessedTableManager = ProcessedTableManager< - _$Database, - $MemberTable, - MemberData, - $$MemberTableFilterComposer, - $$MemberTableOrderingComposer, - $$MemberTableAnnotationComposer, - $$MemberTableCreateCompanionBuilder, - $$MemberTableUpdateCompanionBuilder, - (MemberData, BaseReferences<_$Database, $MemberTable, MemberData>), - MemberData, - PrefetchHooks Function()>; -typedef $$SettingTableCreateCompanionBuilder = SettingCompanion Function({ - required String key, - required String value, - Value rowid, -}); -typedef $$SettingTableUpdateCompanionBuilder = SettingCompanion Function({ - Value key, - Value value, - Value rowid, -}); +typedef $$MemberTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $MemberTable, + MemberData, + $$MemberTableFilterComposer, + $$MemberTableOrderingComposer, + $$MemberTableAnnotationComposer, + $$MemberTableCreateCompanionBuilder, + $$MemberTableUpdateCompanionBuilder, + (MemberData, BaseReferences<_$Database, $MemberTable, MemberData>), + MemberData, + PrefetchHooks Function() + >; +typedef $$SettingTableCreateCompanionBuilder = + SettingCompanion Function({ + required String key, + required String value, + Value rowid, + }); +typedef $$SettingTableUpdateCompanionBuilder = + SettingCompanion Function({ + Value key, + Value value, + Value rowid, + }); class $$SettingTableFilterComposer extends Composer<_$Database, $SettingTable> { $$SettingTableFilterComposer({ @@ -3759,10 +4602,14 @@ class $$SettingTableFilterComposer extends Composer<_$Database, $SettingTable> { super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get key => $composableBuilder( - column: $table.key, builder: (column) => ColumnFilters(column)); + column: $table.key, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get value => $composableBuilder( - column: $table.value, builder: (column) => ColumnFilters(column)); + column: $table.value, + builder: (column) => ColumnFilters(column), + ); } class $$SettingTableOrderingComposer @@ -3775,10 +4622,14 @@ class $$SettingTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get key => $composableBuilder( - column: $table.key, builder: (column) => ColumnOrderings(column)); + column: $table.key, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get value => $composableBuilder( - column: $table.value, builder: (column) => ColumnOrderings(column)); + column: $table.value, + builder: (column) => ColumnOrderings(column), + ); } class $$SettingTableAnnotationComposer @@ -3797,85 +4648,94 @@ class $$SettingTableAnnotationComposer $composableBuilder(column: $table.value, builder: (column) => column); } -class $$SettingTableTableManager extends RootTableManager< - _$Database, - $SettingTable, - SettingData, - $$SettingTableFilterComposer, - $$SettingTableOrderingComposer, - $$SettingTableAnnotationComposer, - $$SettingTableCreateCompanionBuilder, - $$SettingTableUpdateCompanionBuilder, - (SettingData, BaseReferences<_$Database, $SettingTable, SettingData>), - SettingData, - PrefetchHooks Function()> { +class $$SettingTableTableManager + extends + RootTableManager< + _$Database, + $SettingTable, + SettingData, + $$SettingTableFilterComposer, + $$SettingTableOrderingComposer, + $$SettingTableAnnotationComposer, + $$SettingTableCreateCompanionBuilder, + $$SettingTableUpdateCompanionBuilder, + (SettingData, BaseReferences<_$Database, $SettingTable, SettingData>), + SettingData, + PrefetchHooks Function() + > { $$SettingTableTableManager(_$Database db, $SettingTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$SettingTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$SettingTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$SettingTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value key = const Value.absent(), - Value value = const Value.absent(), - Value rowid = const Value.absent(), - }) => - SettingCompanion( - key: key, - value: value, - rowid: rowid, - ), - createCompanionCallback: ({ - required String key, - required String value, - Value rowid = const Value.absent(), - }) => - SettingCompanion.insert( - key: key, - value: value, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$SettingTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$SettingTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$SettingTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value key = const Value.absent(), + Value value = const Value.absent(), + Value rowid = const Value.absent(), + }) => SettingCompanion(key: key, value: value, rowid: rowid), + createCompanionCallback: + ({ + required String key, + required String value, + Value rowid = const Value.absent(), + }) => + SettingCompanion.insert(key: key, value: value, rowid: rowid), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$SettingTableProcessedTableManager = ProcessedTableManager< - _$Database, - $SettingTable, - SettingData, - $$SettingTableFilterComposer, - $$SettingTableOrderingComposer, - $$SettingTableAnnotationComposer, - $$SettingTableCreateCompanionBuilder, - $$SettingTableUpdateCompanionBuilder, - (SettingData, BaseReferences<_$Database, $SettingTable, SettingData>), - SettingData, - PrefetchHooks Function()>; -typedef $$FriendTableCreateCompanionBuilder = FriendCompanion Function({ - required String id, - required String name, - required String displayName, - required String vaultId, - required String keys, - required BigInt updatedAt, - Value rowid, -}); -typedef $$FriendTableUpdateCompanionBuilder = FriendCompanion Function({ - Value id, - Value name, - Value displayName, - Value vaultId, - Value keys, - Value updatedAt, - Value rowid, -}); +typedef $$SettingTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $SettingTable, + SettingData, + $$SettingTableFilterComposer, + $$SettingTableOrderingComposer, + $$SettingTableAnnotationComposer, + $$SettingTableCreateCompanionBuilder, + $$SettingTableUpdateCompanionBuilder, + (SettingData, BaseReferences<_$Database, $SettingTable, SettingData>), + SettingData, + PrefetchHooks Function() + >; +typedef $$FriendTableCreateCompanionBuilder = + FriendCompanion Function({ + required String id, + required String name, + required String displayName, + required String vaultId, + required String keys, + required BigInt updatedAt, + Value rowid, + }); +typedef $$FriendTableUpdateCompanionBuilder = + FriendCompanion Function({ + Value id, + Value name, + Value displayName, + Value vaultId, + Value keys, + Value updatedAt, + Value rowid, + }); class $$FriendTableFilterComposer extends Composer<_$Database, $FriendTable> { $$FriendTableFilterComposer({ @@ -3886,22 +4746,34 @@ class $$FriendTableFilterComposer extends Composer<_$Database, $FriendTable> { super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + column: $table.id, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnFilters(column)); + column: $table.name, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => ColumnFilters(column)); + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get vaultId => $composableBuilder( - column: $table.vaultId, builder: (column) => ColumnFilters(column)); + column: $table.vaultId, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get keys => $composableBuilder( - column: $table.keys, builder: (column) => ColumnFilters(column)); + column: $table.keys, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); } class $$FriendTableOrderingComposer extends Composer<_$Database, $FriendTable> { @@ -3913,22 +4785,34 @@ class $$FriendTableOrderingComposer extends Composer<_$Database, $FriendTable> { super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnOrderings(column)); + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => ColumnOrderings(column)); + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get vaultId => $composableBuilder( - column: $table.vaultId, builder: (column) => ColumnOrderings(column)); + column: $table.vaultId, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get keys => $composableBuilder( - column: $table.keys, builder: (column) => ColumnOrderings(column)); + column: $table.keys, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); } class $$FriendTableAnnotationComposer @@ -3947,7 +4831,9 @@ class $$FriendTableAnnotationComposer $composableBuilder(column: $table.name, builder: (column) => column); GeneratedColumn get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => column); + column: $table.displayName, + builder: (column) => column, + ); GeneratedColumn get vaultId => $composableBuilder(column: $table.vaultId, builder: (column) => column); @@ -3959,103 +4845,119 @@ class $$FriendTableAnnotationComposer $composableBuilder(column: $table.updatedAt, builder: (column) => column); } -class $$FriendTableTableManager extends RootTableManager< - _$Database, - $FriendTable, - FriendData, - $$FriendTableFilterComposer, - $$FriendTableOrderingComposer, - $$FriendTableAnnotationComposer, - $$FriendTableCreateCompanionBuilder, - $$FriendTableUpdateCompanionBuilder, - (FriendData, BaseReferences<_$Database, $FriendTable, FriendData>), - FriendData, - PrefetchHooks Function()> { +class $$FriendTableTableManager + extends + RootTableManager< + _$Database, + $FriendTable, + FriendData, + $$FriendTableFilterComposer, + $$FriendTableOrderingComposer, + $$FriendTableAnnotationComposer, + $$FriendTableCreateCompanionBuilder, + $$FriendTableUpdateCompanionBuilder, + (FriendData, BaseReferences<_$Database, $FriendTable, FriendData>), + FriendData, + PrefetchHooks Function() + > { $$FriendTableTableManager(_$Database db, $FriendTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$FriendTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$FriendTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$FriendTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value name = const Value.absent(), - Value displayName = const Value.absent(), - Value vaultId = const Value.absent(), - Value keys = const Value.absent(), - Value updatedAt = const Value.absent(), - Value rowid = const Value.absent(), - }) => - FriendCompanion( - id: id, - name: name, - displayName: displayName, - vaultId: vaultId, - keys: keys, - updatedAt: updatedAt, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required String name, - required String displayName, - required String vaultId, - required String keys, - required BigInt updatedAt, - Value rowid = const Value.absent(), - }) => - FriendCompanion.insert( - id: id, - name: name, - displayName: displayName, - vaultId: vaultId, - keys: keys, - updatedAt: updatedAt, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$FriendTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$FriendTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$FriendTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value displayName = const Value.absent(), + Value vaultId = const Value.absent(), + Value keys = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => FriendCompanion( + id: id, + name: name, + displayName: displayName, + vaultId: vaultId, + keys: keys, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required String displayName, + required String vaultId, + required String keys, + required BigInt updatedAt, + Value rowid = const Value.absent(), + }) => FriendCompanion.insert( + id: id, + name: name, + displayName: displayName, + vaultId: vaultId, + keys: keys, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$FriendTableProcessedTableManager = ProcessedTableManager< - _$Database, - $FriendTable, - FriendData, - $$FriendTableFilterComposer, - $$FriendTableOrderingComposer, - $$FriendTableAnnotationComposer, - $$FriendTableCreateCompanionBuilder, - $$FriendTableUpdateCompanionBuilder, - (FriendData, BaseReferences<_$Database, $FriendTable, FriendData>), - FriendData, - PrefetchHooks Function()>; -typedef $$RequestTableCreateCompanionBuilder = RequestCompanion Function({ - required String id, - required String name, - required String displayName, - required bool self, - required String vaultId, - required String keys, - required BigInt updatedAt, - Value rowid, -}); -typedef $$RequestTableUpdateCompanionBuilder = RequestCompanion Function({ - Value id, - Value name, - Value displayName, - Value self, - Value vaultId, - Value keys, - Value updatedAt, - Value rowid, -}); +typedef $$FriendTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $FriendTable, + FriendData, + $$FriendTableFilterComposer, + $$FriendTableOrderingComposer, + $$FriendTableAnnotationComposer, + $$FriendTableCreateCompanionBuilder, + $$FriendTableUpdateCompanionBuilder, + (FriendData, BaseReferences<_$Database, $FriendTable, FriendData>), + FriendData, + PrefetchHooks Function() + >; +typedef $$RequestTableCreateCompanionBuilder = + RequestCompanion Function({ + required String id, + required String name, + required String displayName, + required bool self, + required String vaultId, + required String keys, + required BigInt updatedAt, + Value rowid, + }); +typedef $$RequestTableUpdateCompanionBuilder = + RequestCompanion Function({ + Value id, + Value name, + Value displayName, + Value self, + Value vaultId, + Value keys, + Value updatedAt, + Value rowid, + }); class $$RequestTableFilterComposer extends Composer<_$Database, $RequestTable> { $$RequestTableFilterComposer({ @@ -4066,25 +4968,39 @@ class $$RequestTableFilterComposer extends Composer<_$Database, $RequestTable> { super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + column: $table.id, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnFilters(column)); + column: $table.name, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => ColumnFilters(column)); + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get self => $composableBuilder( - column: $table.self, builder: (column) => ColumnFilters(column)); + column: $table.self, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get vaultId => $composableBuilder( - column: $table.vaultId, builder: (column) => ColumnFilters(column)); + column: $table.vaultId, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get keys => $composableBuilder( - column: $table.keys, builder: (column) => ColumnFilters(column)); + column: $table.keys, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); } class $$RequestTableOrderingComposer @@ -4097,25 +5013,39 @@ class $$RequestTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnOrderings(column)); + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => ColumnOrderings(column)); + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get self => $composableBuilder( - column: $table.self, builder: (column) => ColumnOrderings(column)); + column: $table.self, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get vaultId => $composableBuilder( - column: $table.vaultId, builder: (column) => ColumnOrderings(column)); + column: $table.vaultId, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get keys => $composableBuilder( - column: $table.keys, builder: (column) => ColumnOrderings(column)); + column: $table.keys, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); } class $$RequestTableAnnotationComposer @@ -4134,7 +5064,9 @@ class $$RequestTableAnnotationComposer $composableBuilder(column: $table.name, builder: (column) => column); GeneratedColumn get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => column); + column: $table.displayName, + builder: (column) => column, + ); GeneratedColumn get self => $composableBuilder(column: $table.self, builder: (column) => column); @@ -4149,103 +5081,119 @@ class $$RequestTableAnnotationComposer $composableBuilder(column: $table.updatedAt, builder: (column) => column); } -class $$RequestTableTableManager extends RootTableManager< - _$Database, - $RequestTable, - RequestData, - $$RequestTableFilterComposer, - $$RequestTableOrderingComposer, - $$RequestTableAnnotationComposer, - $$RequestTableCreateCompanionBuilder, - $$RequestTableUpdateCompanionBuilder, - (RequestData, BaseReferences<_$Database, $RequestTable, RequestData>), - RequestData, - PrefetchHooks Function()> { +class $$RequestTableTableManager + extends + RootTableManager< + _$Database, + $RequestTable, + RequestData, + $$RequestTableFilterComposer, + $$RequestTableOrderingComposer, + $$RequestTableAnnotationComposer, + $$RequestTableCreateCompanionBuilder, + $$RequestTableUpdateCompanionBuilder, + (RequestData, BaseReferences<_$Database, $RequestTable, RequestData>), + RequestData, + PrefetchHooks Function() + > { $$RequestTableTableManager(_$Database db, $RequestTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$RequestTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$RequestTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$RequestTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value name = const Value.absent(), - Value displayName = const Value.absent(), - Value self = const Value.absent(), - Value vaultId = const Value.absent(), - Value keys = const Value.absent(), - Value updatedAt = const Value.absent(), - Value rowid = const Value.absent(), - }) => - RequestCompanion( - id: id, - name: name, - displayName: displayName, - self: self, - vaultId: vaultId, - keys: keys, - updatedAt: updatedAt, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required String name, - required String displayName, - required bool self, - required String vaultId, - required String keys, - required BigInt updatedAt, - Value rowid = const Value.absent(), - }) => - RequestCompanion.insert( - id: id, - name: name, - displayName: displayName, - self: self, - vaultId: vaultId, - keys: keys, - updatedAt: updatedAt, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$RequestTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$RequestTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$RequestTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value displayName = const Value.absent(), + Value self = const Value.absent(), + Value vaultId = const Value.absent(), + Value keys = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => RequestCompanion( + id: id, + name: name, + displayName: displayName, + self: self, + vaultId: vaultId, + keys: keys, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required String displayName, + required bool self, + required String vaultId, + required String keys, + required BigInt updatedAt, + Value rowid = const Value.absent(), + }) => RequestCompanion.insert( + id: id, + name: name, + displayName: displayName, + self: self, + vaultId: vaultId, + keys: keys, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$RequestTableProcessedTableManager = ProcessedTableManager< - _$Database, - $RequestTable, - RequestData, - $$RequestTableFilterComposer, - $$RequestTableOrderingComposer, - $$RequestTableAnnotationComposer, - $$RequestTableCreateCompanionBuilder, - $$RequestTableUpdateCompanionBuilder, - (RequestData, BaseReferences<_$Database, $RequestTable, RequestData>), - RequestData, - PrefetchHooks Function()>; -typedef $$UnknownProfileTableCreateCompanionBuilder = UnknownProfileCompanion - Function({ - required String id, - required String name, - required String displayName, - required String keys, - Value rowid, -}); -typedef $$UnknownProfileTableUpdateCompanionBuilder = UnknownProfileCompanion - Function({ - Value id, - Value name, - Value displayName, - Value keys, - Value rowid, -}); +typedef $$RequestTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $RequestTable, + RequestData, + $$RequestTableFilterComposer, + $$RequestTableOrderingComposer, + $$RequestTableAnnotationComposer, + $$RequestTableCreateCompanionBuilder, + $$RequestTableUpdateCompanionBuilder, + (RequestData, BaseReferences<_$Database, $RequestTable, RequestData>), + RequestData, + PrefetchHooks Function() + >; +typedef $$UnknownProfileTableCreateCompanionBuilder = + UnknownProfileCompanion Function({ + required String id, + required String name, + required String displayName, + required String keys, + Value lastFetched, + Value rowid, + }); +typedef $$UnknownProfileTableUpdateCompanionBuilder = + UnknownProfileCompanion Function({ + Value id, + Value name, + Value displayName, + Value keys, + Value lastFetched, + Value rowid, + }); class $$UnknownProfileTableFilterComposer extends Composer<_$Database, $UnknownProfileTable> { @@ -4257,16 +5205,29 @@ class $$UnknownProfileTableFilterComposer super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + column: $table.id, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnFilters(column)); + column: $table.name, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => ColumnFilters(column)); + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get keys => $composableBuilder( - column: $table.keys, builder: (column) => ColumnFilters(column)); + column: $table.keys, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastFetched => $composableBuilder( + column: $table.lastFetched, + builder: (column) => ColumnFilters(column), + ); } class $$UnknownProfileTableOrderingComposer @@ -4279,16 +5240,29 @@ class $$UnknownProfileTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnOrderings(column)); + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => ColumnOrderings(column)); + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get keys => $composableBuilder( - column: $table.keys, builder: (column) => ColumnOrderings(column)); + column: $table.keys, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastFetched => $composableBuilder( + column: $table.lastFetched, + builder: (column) => ColumnOrderings(column), + ); } class $$UnknownProfileTableAnnotationComposer @@ -4307,99 +5281,134 @@ class $$UnknownProfileTableAnnotationComposer $composableBuilder(column: $table.name, builder: (column) => column); GeneratedColumn get displayName => $composableBuilder( - column: $table.displayName, builder: (column) => column); + column: $table.displayName, + builder: (column) => column, + ); GeneratedColumn get keys => $composableBuilder(column: $table.keys, builder: (column) => column); + + GeneratedColumn get lastFetched => $composableBuilder( + column: $table.lastFetched, + builder: (column) => column, + ); } -class $$UnknownProfileTableTableManager extends RootTableManager< - _$Database, - $UnknownProfileTable, - UnknownProfileData, - $$UnknownProfileTableFilterComposer, - $$UnknownProfileTableOrderingComposer, - $$UnknownProfileTableAnnotationComposer, - $$UnknownProfileTableCreateCompanionBuilder, - $$UnknownProfileTableUpdateCompanionBuilder, - ( - UnknownProfileData, - BaseReferences<_$Database, $UnknownProfileTable, UnknownProfileData> - ), - UnknownProfileData, - PrefetchHooks Function()> { +class $$UnknownProfileTableTableManager + extends + RootTableManager< + _$Database, + $UnknownProfileTable, + UnknownProfileData, + $$UnknownProfileTableFilterComposer, + $$UnknownProfileTableOrderingComposer, + $$UnknownProfileTableAnnotationComposer, + $$UnknownProfileTableCreateCompanionBuilder, + $$UnknownProfileTableUpdateCompanionBuilder, + ( + UnknownProfileData, + BaseReferences< + _$Database, + $UnknownProfileTable, + UnknownProfileData + >, + ), + UnknownProfileData, + PrefetchHooks Function() + > { $$UnknownProfileTableTableManager(_$Database db, $UnknownProfileTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$UnknownProfileTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$UnknownProfileTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$UnknownProfileTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value name = const Value.absent(), - Value displayName = const Value.absent(), - Value keys = const Value.absent(), - Value rowid = const Value.absent(), - }) => - UnknownProfileCompanion( - id: id, - name: name, - displayName: displayName, - keys: keys, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required String name, - required String displayName, - required String keys, - Value rowid = const Value.absent(), - }) => - UnknownProfileCompanion.insert( - id: id, - name: name, - displayName: displayName, - keys: keys, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$UnknownProfileTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => + $$UnknownProfileTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$UnknownProfileTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value displayName = const Value.absent(), + Value keys = const Value.absent(), + Value lastFetched = const Value.absent(), + Value rowid = const Value.absent(), + }) => UnknownProfileCompanion( + id: id, + name: name, + displayName: displayName, + keys: keys, + lastFetched: lastFetched, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required String displayName, + required String keys, + Value lastFetched = const Value.absent(), + Value rowid = const Value.absent(), + }) => UnknownProfileCompanion.insert( + id: id, + name: name, + displayName: displayName, + keys: keys, + lastFetched: lastFetched, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$UnknownProfileTableProcessedTableManager = ProcessedTableManager< - _$Database, - $UnknownProfileTable, - UnknownProfileData, - $$UnknownProfileTableFilterComposer, - $$UnknownProfileTableOrderingComposer, - $$UnknownProfileTableAnnotationComposer, - $$UnknownProfileTableCreateCompanionBuilder, - $$UnknownProfileTableUpdateCompanionBuilder, - ( +typedef $$UnknownProfileTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $UnknownProfileTable, UnknownProfileData, - BaseReferences<_$Database, $UnknownProfileTable, UnknownProfileData> - ), - UnknownProfileData, - PrefetchHooks Function()>; -typedef $$ProfileTableCreateCompanionBuilder = ProfileCompanion Function({ - required String id, - required String pictureContainer, - required String data, - Value rowid, -}); -typedef $$ProfileTableUpdateCompanionBuilder = ProfileCompanion Function({ - Value id, - Value pictureContainer, - Value data, - Value rowid, -}); + $$UnknownProfileTableFilterComposer, + $$UnknownProfileTableOrderingComposer, + $$UnknownProfileTableAnnotationComposer, + $$UnknownProfileTableCreateCompanionBuilder, + $$UnknownProfileTableUpdateCompanionBuilder, + ( + UnknownProfileData, + BaseReferences<_$Database, $UnknownProfileTable, UnknownProfileData>, + ), + UnknownProfileData, + PrefetchHooks Function() + >; +typedef $$ProfileTableCreateCompanionBuilder = + ProfileCompanion Function({ + required String id, + required String pictureContainer, + required String data, + Value rowid, + }); +typedef $$ProfileTableUpdateCompanionBuilder = + ProfileCompanion Function({ + Value id, + Value pictureContainer, + Value data, + Value rowid, + }); class $$ProfileTableFilterComposer extends Composer<_$Database, $ProfileTable> { $$ProfileTableFilterComposer({ @@ -4410,14 +5419,19 @@ class $$ProfileTableFilterComposer extends Composer<_$Database, $ProfileTable> { super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + column: $table.id, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get pictureContainer => $composableBuilder( - column: $table.pictureContainer, - builder: (column) => ColumnFilters(column)); + column: $table.pictureContainer, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get data => $composableBuilder( - column: $table.data, builder: (column) => ColumnFilters(column)); + column: $table.data, + builder: (column) => ColumnFilters(column), + ); } class $$ProfileTableOrderingComposer @@ -4430,14 +5444,19 @@ class $$ProfileTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get pictureContainer => $composableBuilder( - column: $table.pictureContainer, - builder: (column) => ColumnOrderings(column)); + column: $table.pictureContainer, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get data => $composableBuilder( - column: $table.data, builder: (column) => ColumnOrderings(column)); + column: $table.data, + builder: (column) => ColumnOrderings(column), + ); } class $$ProfileTableAnnotationComposer @@ -4453,87 +5472,97 @@ class $$ProfileTableAnnotationComposer $composableBuilder(column: $table.id, builder: (column) => column); GeneratedColumn get pictureContainer => $composableBuilder( - column: $table.pictureContainer, builder: (column) => column); + column: $table.pictureContainer, + builder: (column) => column, + ); GeneratedColumn get data => $composableBuilder(column: $table.data, builder: (column) => column); } -class $$ProfileTableTableManager extends RootTableManager< - _$Database, - $ProfileTable, - ProfileData, - $$ProfileTableFilterComposer, - $$ProfileTableOrderingComposer, - $$ProfileTableAnnotationComposer, - $$ProfileTableCreateCompanionBuilder, - $$ProfileTableUpdateCompanionBuilder, - (ProfileData, BaseReferences<_$Database, $ProfileTable, ProfileData>), - ProfileData, - PrefetchHooks Function()> { +class $$ProfileTableTableManager + extends + RootTableManager< + _$Database, + $ProfileTable, + ProfileData, + $$ProfileTableFilterComposer, + $$ProfileTableOrderingComposer, + $$ProfileTableAnnotationComposer, + $$ProfileTableCreateCompanionBuilder, + $$ProfileTableUpdateCompanionBuilder, + (ProfileData, BaseReferences<_$Database, $ProfileTable, ProfileData>), + ProfileData, + PrefetchHooks Function() + > { $$ProfileTableTableManager(_$Database db, $ProfileTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$ProfileTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$ProfileTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$ProfileTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value pictureContainer = const Value.absent(), - Value data = const Value.absent(), - Value rowid = const Value.absent(), - }) => - ProfileCompanion( - id: id, - pictureContainer: pictureContainer, - data: data, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required String pictureContainer, - required String data, - Value rowid = const Value.absent(), - }) => - ProfileCompanion.insert( - id: id, - pictureContainer: pictureContainer, - data: data, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$ProfileTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$ProfileTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$ProfileTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value pictureContainer = const Value.absent(), + Value data = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProfileCompanion( + id: id, + pictureContainer: pictureContainer, + data: data, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String pictureContainer, + required String data, + Value rowid = const Value.absent(), + }) => ProfileCompanion.insert( + id: id, + pictureContainer: pictureContainer, + data: data, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$ProfileTableProcessedTableManager = ProcessedTableManager< - _$Database, - $ProfileTable, - ProfileData, - $$ProfileTableFilterComposer, - $$ProfileTableOrderingComposer, - $$ProfileTableAnnotationComposer, - $$ProfileTableCreateCompanionBuilder, - $$ProfileTableUpdateCompanionBuilder, - (ProfileData, BaseReferences<_$Database, $ProfileTable, ProfileData>), - ProfileData, - PrefetchHooks Function()>; -typedef $$TrustedLinkTableCreateCompanionBuilder = TrustedLinkCompanion - Function({ - required String domain, - Value rowid, -}); -typedef $$TrustedLinkTableUpdateCompanionBuilder = TrustedLinkCompanion - Function({ - Value domain, - Value rowid, -}); +typedef $$ProfileTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $ProfileTable, + ProfileData, + $$ProfileTableFilterComposer, + $$ProfileTableOrderingComposer, + $$ProfileTableAnnotationComposer, + $$ProfileTableCreateCompanionBuilder, + $$ProfileTableUpdateCompanionBuilder, + (ProfileData, BaseReferences<_$Database, $ProfileTable, ProfileData>), + ProfileData, + PrefetchHooks Function() + >; +typedef $$TrustedLinkTableCreateCompanionBuilder = + TrustedLinkCompanion Function({required String domain, Value rowid}); +typedef $$TrustedLinkTableUpdateCompanionBuilder = + TrustedLinkCompanion Function({Value domain, Value rowid}); class $$TrustedLinkTableFilterComposer extends Composer<_$Database, $TrustedLinkTable> { @@ -4545,7 +5574,9 @@ class $$TrustedLinkTableFilterComposer super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get domain => $composableBuilder( - column: $table.domain, builder: (column) => ColumnFilters(column)); + column: $table.domain, + builder: (column) => ColumnFilters(column), + ); } class $$TrustedLinkTableOrderingComposer @@ -4558,7 +5589,9 @@ class $$TrustedLinkTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get domain => $composableBuilder( - column: $table.domain, builder: (column) => ColumnOrderings(column)); + column: $table.domain, + builder: (column) => ColumnOrderings(column), + ); } class $$TrustedLinkTableAnnotationComposer @@ -4574,89 +5607,100 @@ class $$TrustedLinkTableAnnotationComposer $composableBuilder(column: $table.domain, builder: (column) => column); } -class $$TrustedLinkTableTableManager extends RootTableManager< - _$Database, - $TrustedLinkTable, - TrustedLinkData, - $$TrustedLinkTableFilterComposer, - $$TrustedLinkTableOrderingComposer, - $$TrustedLinkTableAnnotationComposer, - $$TrustedLinkTableCreateCompanionBuilder, - $$TrustedLinkTableUpdateCompanionBuilder, - ( - TrustedLinkData, - BaseReferences<_$Database, $TrustedLinkTable, TrustedLinkData> - ), - TrustedLinkData, - PrefetchHooks Function()> { +class $$TrustedLinkTableTableManager + extends + RootTableManager< + _$Database, + $TrustedLinkTable, + TrustedLinkData, + $$TrustedLinkTableFilterComposer, + $$TrustedLinkTableOrderingComposer, + $$TrustedLinkTableAnnotationComposer, + $$TrustedLinkTableCreateCompanionBuilder, + $$TrustedLinkTableUpdateCompanionBuilder, + ( + TrustedLinkData, + BaseReferences<_$Database, $TrustedLinkTable, TrustedLinkData>, + ), + TrustedLinkData, + PrefetchHooks Function() + > { $$TrustedLinkTableTableManager(_$Database db, $TrustedLinkTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$TrustedLinkTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$TrustedLinkTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$TrustedLinkTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value domain = const Value.absent(), - Value rowid = const Value.absent(), - }) => - TrustedLinkCompanion( - domain: domain, - rowid: rowid, - ), - createCompanionCallback: ({ - required String domain, - Value rowid = const Value.absent(), - }) => - TrustedLinkCompanion.insert( - domain: domain, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$TrustedLinkTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$TrustedLinkTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => + $$TrustedLinkTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value domain = const Value.absent(), + Value rowid = const Value.absent(), + }) => TrustedLinkCompanion(domain: domain, rowid: rowid), + createCompanionCallback: + ({ + required String domain, + Value rowid = const Value.absent(), + }) => TrustedLinkCompanion.insert(domain: domain, rowid: rowid), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$TrustedLinkTableProcessedTableManager = ProcessedTableManager< - _$Database, - $TrustedLinkTable, - TrustedLinkData, - $$TrustedLinkTableFilterComposer, - $$TrustedLinkTableOrderingComposer, - $$TrustedLinkTableAnnotationComposer, - $$TrustedLinkTableCreateCompanionBuilder, - $$TrustedLinkTableUpdateCompanionBuilder, - ( +typedef $$TrustedLinkTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $TrustedLinkTable, TrustedLinkData, - BaseReferences<_$Database, $TrustedLinkTable, TrustedLinkData> - ), - TrustedLinkData, - PrefetchHooks Function()>; -typedef $$LibraryEntryTableCreateCompanionBuilder = LibraryEntryCompanion - Function({ - required String id, - required LibraryEntryType type, - required BigInt createdAt, - required String data, - required int width, - required int height, - Value rowid, -}); -typedef $$LibraryEntryTableUpdateCompanionBuilder = LibraryEntryCompanion - Function({ - Value id, - Value type, - Value createdAt, - Value data, - Value width, - Value height, - Value rowid, -}); + $$TrustedLinkTableFilterComposer, + $$TrustedLinkTableOrderingComposer, + $$TrustedLinkTableAnnotationComposer, + $$TrustedLinkTableCreateCompanionBuilder, + $$TrustedLinkTableUpdateCompanionBuilder, + ( + TrustedLinkData, + BaseReferences<_$Database, $TrustedLinkTable, TrustedLinkData>, + ), + TrustedLinkData, + PrefetchHooks Function() + >; +typedef $$LibraryEntryTableCreateCompanionBuilder = + LibraryEntryCompanion Function({ + required String id, + required LibraryEntryType type, + required BigInt createdAt, + Value identifierHash, + required String data, + required int width, + required int height, + Value rowid, + }); +typedef $$LibraryEntryTableUpdateCompanionBuilder = + LibraryEntryCompanion Function({ + Value id, + Value type, + Value createdAt, + Value identifierHash, + Value data, + Value width, + Value height, + Value rowid, + }); class $$LibraryEntryTableFilterComposer extends Composer<_$Database, $LibraryEntryTable> { @@ -4668,24 +5712,40 @@ class $$LibraryEntryTableFilterComposer super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + column: $table.id, + builder: (column) => ColumnFilters(column), + ); ColumnWithTypeConverterFilters - get type => $composableBuilder( - column: $table.type, - builder: (column) => ColumnWithTypeConverterFilters(column)); + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnFilters(column)); + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get identifierHash => $composableBuilder( + column: $table.identifierHash, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get data => $composableBuilder( - column: $table.data, builder: (column) => ColumnFilters(column)); + column: $table.data, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get width => $composableBuilder( - column: $table.width, builder: (column) => ColumnFilters(column)); + column: $table.width, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get height => $composableBuilder( - column: $table.height, builder: (column) => ColumnFilters(column)); + column: $table.height, + builder: (column) => ColumnFilters(column), + ); } class $$LibraryEntryTableOrderingComposer @@ -4698,22 +5758,39 @@ class $$LibraryEntryTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get identifierHash => $composableBuilder( + column: $table.identifierHash, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get data => $composableBuilder( - column: $table.data, builder: (column) => ColumnOrderings(column)); + column: $table.data, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get width => $composableBuilder( - column: $table.width, builder: (column) => ColumnOrderings(column)); + column: $table.width, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get height => $composableBuilder( - column: $table.height, builder: (column) => ColumnOrderings(column)); + column: $table.height, + builder: (column) => ColumnOrderings(column), + ); } class $$LibraryEntryTableAnnotationComposer @@ -4734,6 +5811,11 @@ class $$LibraryEntryTableAnnotationComposer GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get identifierHash => $composableBuilder( + column: $table.identifierHash, + builder: (column) => column, + ); + GeneratedColumn get data => $composableBuilder(column: $table.data, builder: (column) => column); @@ -4744,89 +5826,108 @@ class $$LibraryEntryTableAnnotationComposer $composableBuilder(column: $table.height, builder: (column) => column); } -class $$LibraryEntryTableTableManager extends RootTableManager< - _$Database, - $LibraryEntryTable, - LibraryEntryData, - $$LibraryEntryTableFilterComposer, - $$LibraryEntryTableOrderingComposer, - $$LibraryEntryTableAnnotationComposer, - $$LibraryEntryTableCreateCompanionBuilder, - $$LibraryEntryTableUpdateCompanionBuilder, - ( - LibraryEntryData, - BaseReferences<_$Database, $LibraryEntryTable, LibraryEntryData> - ), - LibraryEntryData, - PrefetchHooks Function()> { +class $$LibraryEntryTableTableManager + extends + RootTableManager< + _$Database, + $LibraryEntryTable, + LibraryEntryData, + $$LibraryEntryTableFilterComposer, + $$LibraryEntryTableOrderingComposer, + $$LibraryEntryTableAnnotationComposer, + $$LibraryEntryTableCreateCompanionBuilder, + $$LibraryEntryTableUpdateCompanionBuilder, + ( + LibraryEntryData, + BaseReferences<_$Database, $LibraryEntryTable, LibraryEntryData>, + ), + LibraryEntryData, + PrefetchHooks Function() + > { $$LibraryEntryTableTableManager(_$Database db, $LibraryEntryTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$LibraryEntryTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$LibraryEntryTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$LibraryEntryTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value type = const Value.absent(), - Value createdAt = const Value.absent(), - Value data = const Value.absent(), - Value width = const Value.absent(), - Value height = const Value.absent(), - Value rowid = const Value.absent(), - }) => - LibraryEntryCompanion( - id: id, - type: type, - createdAt: createdAt, - data: data, - width: width, - height: height, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required LibraryEntryType type, - required BigInt createdAt, - required String data, - required int width, - required int height, - Value rowid = const Value.absent(), - }) => - LibraryEntryCompanion.insert( - id: id, - type: type, - createdAt: createdAt, - data: data, - width: width, - height: height, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: + () => $$LibraryEntryTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$LibraryEntryTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => + $$LibraryEntryTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value type = const Value.absent(), + Value createdAt = const Value.absent(), + Value identifierHash = const Value.absent(), + Value data = const Value.absent(), + Value width = const Value.absent(), + Value height = const Value.absent(), + Value rowid = const Value.absent(), + }) => LibraryEntryCompanion( + id: id, + type: type, + createdAt: createdAt, + identifierHash: identifierHash, + data: data, + width: width, + height: height, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required LibraryEntryType type, + required BigInt createdAt, + Value identifierHash = const Value.absent(), + required String data, + required int width, + required int height, + Value rowid = const Value.absent(), + }) => LibraryEntryCompanion.insert( + id: id, + type: type, + createdAt: createdAt, + identifierHash: identifierHash, + data: data, + width: width, + height: height, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$LibraryEntryTableProcessedTableManager = ProcessedTableManager< - _$Database, - $LibraryEntryTable, - LibraryEntryData, - $$LibraryEntryTableFilterComposer, - $$LibraryEntryTableOrderingComposer, - $$LibraryEntryTableAnnotationComposer, - $$LibraryEntryTableCreateCompanionBuilder, - $$LibraryEntryTableUpdateCompanionBuilder, - ( +typedef $$LibraryEntryTableProcessedTableManager = + ProcessedTableManager< + _$Database, + $LibraryEntryTable, LibraryEntryData, - BaseReferences<_$Database, $LibraryEntryTable, LibraryEntryData> - ), - LibraryEntryData, - PrefetchHooks Function()>; + $$LibraryEntryTableFilterComposer, + $$LibraryEntryTableOrderingComposer, + $$LibraryEntryTableAnnotationComposer, + $$LibraryEntryTableCreateCompanionBuilder, + $$LibraryEntryTableUpdateCompanionBuilder, + ( + LibraryEntryData, + BaseReferences<_$Database, $LibraryEntryTable, LibraryEntryData>, + ), + LibraryEntryData, + PrefetchHooks Function() + >; class $DatabaseManager { final _$Database _db; diff --git a/lib/database/database.steps.dart b/lib/database/database.steps.dart index 8a0b9f5a..d4d38dfc 100644 --- a/lib/database/database.steps.dart +++ b/lib/database/database.steps.dart @@ -1,3 +1,4 @@ +// dart format width=80 import 'package:drift/internal/versioned_schema.dart' as i0; import 'package:drift/drift.dart' as i1; import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import @@ -19,185 +20,156 @@ final class Schema2 extends i0.VersionedSchema { libraryEntry, ]; late final Shape0 conversation = Shape0( - source: i0.VersionedTable( - entityName: 'conversation', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(id)', - ], - columns: [ - _column_0, - _column_1, - _column_2, - _column_3, - _column_4, - _column_5, - _column_6, - _column_7, - _column_8, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'conversation', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + ], + attachedDatabase: database, + ), + alias: null, + ); late final Shape1 message = Shape1( - source: i0.VersionedTable( - entityName: 'message', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(id)', - ], - columns: [ - _column_0, - _column_9, - _column_10, - _column_11, - _column_12, - _column_13, - _column_14, - _column_15, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'message', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + ], + attachedDatabase: database, + ), + alias: null, + ); late final Shape2 member = Shape2( - source: i0.VersionedTable( - entityName: 'member', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(id)', - ], - columns: [ - _column_0, - _column_16, - _column_17, - _column_18, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'member', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_16, _column_17, _column_18], + attachedDatabase: database, + ), + alias: null, + ); late final Shape3 setting = Shape3( - source: i0.VersionedTable( - entityName: 'setting', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY("key")', - ], - columns: [ - _column_5, - _column_19, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'setting', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_5, _column_19], + attachedDatabase: database, + ), + alias: null, + ); late final Shape4 friend = Shape4( - source: i0.VersionedTable( - entityName: 'friend', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(id)', - ], - columns: [ - _column_0, - _column_20, - _column_21, - _column_1, - _column_22, - _column_7, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'friend', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_20, + _column_21, + _column_1, + _column_22, + _column_7, + ], + attachedDatabase: database, + ), + alias: null, + ); late final Shape5 request = Shape5( - source: i0.VersionedTable( - entityName: 'request', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(id)', - ], - columns: [ - _column_0, - _column_20, - _column_21, - _column_23, - _column_1, - _column_22, - _column_7, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'request', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_20, + _column_21, + _column_23, + _column_1, + _column_22, + _column_7, + ], + attachedDatabase: database, + ), + alias: null, + ); late final Shape6 unknownProfile = Shape6( - source: i0.VersionedTable( - entityName: 'unknown_profile', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(id)', - ], - columns: [ - _column_0, - _column_20, - _column_21, - _column_22, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'unknown_profile', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_20, _column_21, _column_22], + attachedDatabase: database, + ), + alias: null, + ); late final Shape7 profile = Shape7( - source: i0.VersionedTable( - entityName: 'profile', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(id)', - ], - columns: [ - _column_0, - _column_24, - _column_3, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'profile', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_24, _column_3], + attachedDatabase: database, + ), + alias: null, + ); late final Shape8 trustedLink = Shape8( - source: i0.VersionedTable( - entityName: 'trusted_link', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(domain)', - ], - columns: [ - _column_25, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'trusted_link', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(domain)'], + columns: [_column_25], + attachedDatabase: database, + ), + alias: null, + ); late final Shape9 libraryEntry = Shape9( - source: i0.VersionedTable( - entityName: 'library_entry', - withoutRowId: false, - isStrict: false, - tableConstraints: [ - 'PRIMARY KEY(id)', - ], - columns: [ - _column_0, - _column_2, - _column_12, - _column_3, - _column_26, - _column_27, - ], - attachedDatabase: database, - ), - alias: null); + source: i0.VersionedTable( + entityName: 'library_entry', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_2, + _column_12, + _column_3, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null, + ); } class Shape0 extends i0.VersionedTable { @@ -223,32 +195,68 @@ class Shape0 extends i0.VersionedTable { } i1.GeneratedColumn _column_0(String aliasedName) => - i1.GeneratedColumn('id', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'id', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_1(String aliasedName) => - i1.GeneratedColumn('vault_id', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_2(String aliasedName) => - i1.GeneratedColumn('type', aliasedName, false, - type: i1.DriftSqlType.int); + i1.GeneratedColumn( + 'type', + aliasedName, + false, + type: i1.DriftSqlType.int, + ); i1.GeneratedColumn _column_3(String aliasedName) => - i1.GeneratedColumn('data', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'data', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_4(String aliasedName) => - i1.GeneratedColumn('token', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'token', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_5(String aliasedName) => - i1.GeneratedColumn('key', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'key', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_6(String aliasedName) => - i1.GeneratedColumn('last_version', aliasedName, false, - type: i1.DriftSqlType.bigInt); + i1.GeneratedColumn( + 'last_version', + aliasedName, + false, + type: i1.DriftSqlType.bigInt, + ); i1.GeneratedColumn _column_7(String aliasedName) => - i1.GeneratedColumn('updated_at', aliasedName, false, - type: i1.DriftSqlType.bigInt); + i1.GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: i1.DriftSqlType.bigInt, + ); i1.GeneratedColumn _column_8(String aliasedName) => - i1.GeneratedColumn('read_at', aliasedName, false, - type: i1.DriftSqlType.bigInt); + i1.GeneratedColumn( + 'read_at', + aliasedName, + false, + type: i1.DriftSqlType.bigInt, + ); class Shape1 extends i0.VersionedTable { Shape1({required super.source, required super.alias}) : super.aliased(); @@ -271,30 +279,60 @@ class Shape1 extends i0.VersionedTable { } i1.GeneratedColumn _column_9(String aliasedName) => - i1.GeneratedColumn('content', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'content', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_10(String aliasedName) => - i1.GeneratedColumn('sender_token', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'sender_token', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_11(String aliasedName) => - i1.GeneratedColumn('sender_address', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'sender_address', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_12(String aliasedName) => - i1.GeneratedColumn('created_at', aliasedName, false, - type: i1.DriftSqlType.bigInt); + i1.GeneratedColumn( + 'created_at', + aliasedName, + false, + type: i1.DriftSqlType.bigInt, + ); i1.GeneratedColumn _column_13(String aliasedName) => - i1.GeneratedColumn('conversation', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'conversation', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_14(String aliasedName) => - i1.GeneratedColumn('edited', aliasedName, false, - type: i1.DriftSqlType.bool, - defaultConstraints: i1.GeneratedColumn.constraintIsAlways( - 'CHECK ("edited" IN (0, 1))')); + i1.GeneratedColumn( + 'edited', + aliasedName, + false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("edited" IN (0, 1))', + ), + ); i1.GeneratedColumn _column_15(String aliasedName) => - i1.GeneratedColumn('verified', aliasedName, false, - type: i1.DriftSqlType.bool, - defaultConstraints: i1.GeneratedColumn.constraintIsAlways( - 'CHECK ("verified" IN (0, 1))')); + i1.GeneratedColumn( + 'verified', + aliasedName, + false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("verified" IN (0, 1))', + ), + ); class Shape2 extends i0.VersionedTable { Shape2({required super.source, required super.alias}) : super.aliased(); @@ -309,14 +347,26 @@ class Shape2 extends i0.VersionedTable { } i1.GeneratedColumn _column_16(String aliasedName) => - i1.GeneratedColumn('conversation_id', aliasedName, true, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'conversation_id', + aliasedName, + true, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_17(String aliasedName) => - i1.GeneratedColumn('account_id', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'account_id', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_18(String aliasedName) => - i1.GeneratedColumn('role_id', aliasedName, false, - type: i1.DriftSqlType.int); + i1.GeneratedColumn( + 'role_id', + aliasedName, + false, + type: i1.DriftSqlType.int, + ); class Shape3 extends i0.VersionedTable { Shape3({required super.source, required super.alias}) : super.aliased(); @@ -327,8 +377,12 @@ class Shape3 extends i0.VersionedTable { } i1.GeneratedColumn _column_19(String aliasedName) => - i1.GeneratedColumn('value', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'value', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); class Shape4 extends i0.VersionedTable { Shape4({required super.source, required super.alias}) : super.aliased(); @@ -347,14 +401,26 @@ class Shape4 extends i0.VersionedTable { } i1.GeneratedColumn _column_20(String aliasedName) => - i1.GeneratedColumn('name', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'name', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_21(String aliasedName) => - i1.GeneratedColumn('display_name', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'display_name', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); i1.GeneratedColumn _column_22(String aliasedName) => - i1.GeneratedColumn('keys', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'keys', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); class Shape5 extends i0.VersionedTable { Shape5({required super.source, required super.alias}) : super.aliased(); @@ -375,10 +441,15 @@ class Shape5 extends i0.VersionedTable { } i1.GeneratedColumn _column_23(String aliasedName) => - i1.GeneratedColumn('self', aliasedName, false, - type: i1.DriftSqlType.bool, - defaultConstraints: - i1.GeneratedColumn.constraintIsAlways('CHECK ("self" IN (0, 1))')); + i1.GeneratedColumn( + 'self', + aliasedName, + false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("self" IN (0, 1))', + ), + ); class Shape6 extends i0.VersionedTable { Shape6({required super.source, required super.alias}) : super.aliased(); @@ -403,8 +474,12 @@ class Shape7 extends i0.VersionedTable { } i1.GeneratedColumn _column_24(String aliasedName) => - i1.GeneratedColumn('picture_container', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'picture_container', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); class Shape8 extends i0.VersionedTable { Shape8({required super.source, required super.alias}) : super.aliased(); @@ -413,8 +488,12 @@ class Shape8 extends i0.VersionedTable { } i1.GeneratedColumn _column_25(String aliasedName) => - i1.GeneratedColumn('domain', aliasedName, false, - type: i1.DriftSqlType.string); + i1.GeneratedColumn( + 'domain', + aliasedName, + false, + type: i1.DriftSqlType.string, + ); class Shape9 extends i0.VersionedTable { Shape9({required super.source, required super.alias}) : super.aliased(); @@ -433,13 +512,701 @@ class Shape9 extends i0.VersionedTable { } i1.GeneratedColumn _column_26(String aliasedName) => - i1.GeneratedColumn('width', aliasedName, false, - type: i1.DriftSqlType.int); + i1.GeneratedColumn( + 'width', + aliasedName, + false, + type: i1.DriftSqlType.int, + ); i1.GeneratedColumn _column_27(String aliasedName) => - i1.GeneratedColumn('height', aliasedName, false, - type: i1.DriftSqlType.int); + i1.GeneratedColumn( + 'height', + aliasedName, + false, + type: i1.DriftSqlType.int, + ); + +final class Schema3 extends i0.VersionedSchema { + Schema3({required super.database}) : super(version: 3); + @override + late final List entities = [ + conversation, + message, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + idxConversationUpdated, + idxMessageCreated, + idxFriendsUpdated, + idxLibraryEntryCreated, + ]; + late final Shape0 conversation = Shape0( + source: i0.VersionedTable( + entityName: 'conversation', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape1 message = Shape1( + source: i0.VersionedTable( + entityName: 'message', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 member = Shape2( + source: i0.VersionedTable( + entityName: 'member', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_16, _column_17, _column_18], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 setting = Shape3( + source: i0.VersionedTable( + entityName: 'setting', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_5, _column_19], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 friend = Shape4( + source: i0.VersionedTable( + entityName: 'friend', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_20, + _column_21, + _column_1, + _column_22, + _column_7, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 request = Shape5( + source: i0.VersionedTable( + entityName: 'request', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_20, + _column_21, + _column_23, + _column_1, + _column_22, + _column_7, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape6 unknownProfile = Shape6( + source: i0.VersionedTable( + entityName: 'unknown_profile', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_20, _column_21, _column_22], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 profile = Shape7( + source: i0.VersionedTable( + entityName: 'profile', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_24, _column_3], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 trustedLink = Shape8( + source: i0.VersionedTable( + entityName: 'trusted_link', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(domain)'], + columns: [_column_25], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 libraryEntry = Shape9( + source: i0.VersionedTable( + entityName: 'library_entry', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_2, + _column_12, + _column_3, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxConversationUpdated = i1.Index( + 'idx_conversation_updated', + 'CREATE INDEX idx_conversation_updated ON conversation (updated_at)', + ); + final i1.Index idxMessageCreated = i1.Index( + 'idx_message_created', + 'CREATE INDEX idx_message_created ON message (created_at)', + ); + final i1.Index idxFriendsUpdated = i1.Index( + 'idx_friends_updated', + 'CREATE INDEX idx_friends_updated ON friend (updated_at)', + ); + final i1.Index idxLibraryEntryCreated = i1.Index( + 'idx_library_entry_created', + 'CREATE INDEX idx_library_entry_created ON library_entry (created_at)', + ); +} + +final class Schema4 extends i0.VersionedSchema { + Schema4({required super.database}) : super(version: 4); + @override + late final List entities = [ + conversation, + message, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + idxConversationUpdated, + idxMessageCreated, + idxFriendsUpdated, + idxRequestsUpdated, + idxUnknownProfilesLastFetched, + idxLibraryEntryCreated, + idxLibraryEntryIdhash, + ]; + late final Shape0 conversation = Shape0( + source: i0.VersionedTable( + entityName: 'conversation', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape1 message = Shape1( + source: i0.VersionedTable( + entityName: 'message', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 member = Shape2( + source: i0.VersionedTable( + entityName: 'member', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_16, _column_17, _column_18], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 setting = Shape3( + source: i0.VersionedTable( + entityName: 'setting', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_5, _column_19], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 friend = Shape4( + source: i0.VersionedTable( + entityName: 'friend', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_20, + _column_21, + _column_1, + _column_22, + _column_7, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 request = Shape5( + source: i0.VersionedTable( + entityName: 'request', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_20, + _column_21, + _column_23, + _column_1, + _column_22, + _column_7, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 unknownProfile = Shape10( + source: i0.VersionedTable( + entityName: 'unknown_profile', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_20, _column_21, _column_22, _column_28], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 profile = Shape7( + source: i0.VersionedTable( + entityName: 'profile', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_24, _column_3], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 trustedLink = Shape8( + source: i0.VersionedTable( + entityName: 'trusted_link', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(domain)'], + columns: [_column_25], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 libraryEntry = Shape11( + source: i0.VersionedTable( + entityName: 'library_entry', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_2, + _column_12, + _column_29, + _column_3, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxConversationUpdated = i1.Index( + 'idx_conversation_updated', + 'CREATE INDEX idx_conversation_updated ON conversation (updated_at)', + ); + final i1.Index idxMessageCreated = i1.Index( + 'idx_message_created', + 'CREATE INDEX idx_message_created ON message (created_at)', + ); + final i1.Index idxFriendsUpdated = i1.Index( + 'idx_friends_updated', + 'CREATE INDEX idx_friends_updated ON friend (updated_at)', + ); + final i1.Index idxRequestsUpdated = i1.Index( + 'idx_requests_updated', + 'CREATE INDEX idx_requests_updated ON request (updated_at)', + ); + final i1.Index idxUnknownProfilesLastFetched = i1.Index( + 'idx_unknown_profiles_last_fetched', + 'CREATE INDEX idx_unknown_profiles_last_fetched ON unknown_profile (last_fetched)', + ); + final i1.Index idxLibraryEntryCreated = i1.Index( + 'idx_library_entry_created', + 'CREATE INDEX idx_library_entry_created ON library_entry (created_at)', + ); + final i1.Index idxLibraryEntryIdhash = i1.Index( + 'idx_library_entry_idhash', + 'CREATE INDEX idx_library_entry_idhash ON library_entry (identifier_hash)', + ); +} + +class Shape10 extends i0.VersionedTable { + Shape10({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get displayName => + columnsByName['display_name']! as i1.GeneratedColumn; + i1.GeneratedColumn get keys => + columnsByName['keys']! as i1.GeneratedColumn; + i1.GeneratedColumn get lastFetched => + columnsByName['last_fetched']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_28(String aliasedName) => + i1.GeneratedColumn( + 'last_fetched', + aliasedName, + false, + type: i1.DriftSqlType.dateTime, + defaultValue: const CustomExpression('0'), + ); + +class Shape11 extends i0.VersionedTable { + Shape11({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get identifierHash => + columnsByName['identifier_hash']! as i1.GeneratedColumn; + i1.GeneratedColumn get data => + columnsByName['data']! as i1.GeneratedColumn; + i1.GeneratedColumn get width => + columnsByName['width']! as i1.GeneratedColumn; + i1.GeneratedColumn get height => + columnsByName['height']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_29(String aliasedName) => + i1.GeneratedColumn( + 'identifier_hash', + aliasedName, + false, + type: i1.DriftSqlType.string, + defaultValue: const CustomExpression('\'to-migrate\''), + ); + +final class Schema5 extends i0.VersionedSchema { + Schema5({required super.database}) : super(version: 5); + @override + late final List entities = [ + conversation, + message, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + idxConversationUpdated, + idxMessageCreated, + idxFriendsUpdated, + idxRequestsUpdated, + idxUnknownProfilesLastFetched, + idxLibraryEntryCreated, + idxLibraryEntryIdhash, + ]; + late final Shape12 conversation = Shape12( + source: i0.VersionedTable( + entityName: 'conversation', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_30, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape1 message = Shape1( + source: i0.VersionedTable( + entityName: 'message', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 member = Shape2( + source: i0.VersionedTable( + entityName: 'member', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_16, _column_17, _column_18], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 setting = Shape3( + source: i0.VersionedTable( + entityName: 'setting', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_5, _column_19], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 friend = Shape4( + source: i0.VersionedTable( + entityName: 'friend', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_20, + _column_21, + _column_1, + _column_22, + _column_7, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 request = Shape5( + source: i0.VersionedTable( + entityName: 'request', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_20, + _column_21, + _column_23, + _column_1, + _column_22, + _column_7, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 unknownProfile = Shape10( + source: i0.VersionedTable( + entityName: 'unknown_profile', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_20, _column_21, _column_22, _column_28], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 profile = Shape7( + source: i0.VersionedTable( + entityName: 'profile', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_24, _column_3], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 trustedLink = Shape8( + source: i0.VersionedTable( + entityName: 'trusted_link', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(domain)'], + columns: [_column_25], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 libraryEntry = Shape11( + source: i0.VersionedTable( + entityName: 'library_entry', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_2, + _column_12, + _column_29, + _column_3, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxConversationUpdated = i1.Index( + 'idx_conversation_updated', + 'CREATE INDEX idx_conversation_updated ON conversation (updated_at)', + ); + final i1.Index idxMessageCreated = i1.Index( + 'idx_message_created', + 'CREATE INDEX idx_message_created ON message (created_at)', + ); + final i1.Index idxFriendsUpdated = i1.Index( + 'idx_friends_updated', + 'CREATE INDEX idx_friends_updated ON friend (updated_at)', + ); + final i1.Index idxRequestsUpdated = i1.Index( + 'idx_requests_updated', + 'CREATE INDEX idx_requests_updated ON request (updated_at)', + ); + final i1.Index idxUnknownProfilesLastFetched = i1.Index( + 'idx_unknown_profiles_last_fetched', + 'CREATE INDEX idx_unknown_profiles_last_fetched ON unknown_profile (last_fetched)', + ); + final i1.Index idxLibraryEntryCreated = i1.Index( + 'idx_library_entry_created', + 'CREATE INDEX idx_library_entry_created ON library_entry (created_at)', + ); + final i1.Index idxLibraryEntryIdhash = i1.Index( + 'idx_library_entry_idhash', + 'CREATE INDEX idx_library_entry_idhash ON library_entry (identifier_hash)', + ); +} + +class Shape12 extends i0.VersionedTable { + Shape12({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get vaultId => + columnsByName['vault_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get data => + columnsByName['data']! as i1.GeneratedColumn; + i1.GeneratedColumn get token => + columnsByName['token']! as i1.GeneratedColumn; + i1.GeneratedColumn get key => + columnsByName['key']! as i1.GeneratedColumn; + i1.GeneratedColumn get lastVersion => + columnsByName['last_version']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get reads => + columnsByName['reads']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_30(String aliasedName) => + i1.GeneratedColumn( + 'reads', + aliasedName, + false, + type: i1.DriftSqlType.string, + defaultValue: const CustomExpression('\'\''), + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, + required Future Function(i1.Migrator m, Schema3 schema) from2To3, + required Future Function(i1.Migrator m, Schema4 schema) from3To4, + required Future Function(i1.Migrator m, Schema5 schema) from4To5, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -448,6 +1215,21 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from1To2(migrator, schema); return 2; + case 2: + final schema = Schema3(database: database); + final migrator = i1.Migrator(database, schema); + await from2To3(migrator, schema); + return 3; + case 3: + final schema = Schema4(database: database); + final migrator = i1.Migrator(database, schema); + await from3To4(migrator, schema); + return 4; + case 4: + final schema = Schema5(database: database); + final migrator = i1.Migrator(database, schema); + await from4To5(migrator, schema); + return 5; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -456,8 +1238,14 @@ i0.MigrationStepWithVersion migrationSteps({ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, -}) => - i0.VersionedSchema.stepByStepHelper( - step: migrationSteps( - from1To2: from1To2, - )); + required Future Function(i1.Migrator m, Schema3 schema) from2To3, + required Future Function(i1.Migrator m, Schema4 schema) from3To4, + required Future Function(i1.Migrator m, Schema5 schema) from4To5, +}) => i0.VersionedSchema.stepByStepHelper( + step: migrationSteps( + from1To2: from1To2, + from2To3: from2To3, + from3To4: from3To4, + from4To5: from4To5, + ), +); diff --git a/lib/database/database_entities.dart b/lib/database/database_entities.dart index f370ac33..bf6f56dc 100644 --- a/lib/database/database_entities.dart +++ b/lib/database/database_entities.dart @@ -1,8 +1,9 @@ import 'package:chat_interface/pages/settings/town/file_settings.dart'; import 'package:drift/drift.dart'; -enum ConversationType { directMessage, group } +enum ConversationType { directMessage, group, square } +@TableIndex(name: "idx_conversation_updated", columns: {#updatedAt}) class Conversation extends Table { TextColumn get id => text()(); TextColumn get vaultId => text()(); @@ -12,12 +13,13 @@ class Conversation extends Table { TextColumn get key => text()(); Int64Column get lastVersion => int64()(); Int64Column get updatedAt => int64()(); - Int64Column get readAt => int64()(); + TextColumn get reads => text().withDefault(Constant(""))(); @override Set>? get primaryKey => {id}; } +@TableIndex(name: "idx_message_created", columns: {#createdAt}) class Message extends Table { TextColumn get id => text()(); TextColumn get content => text()(); @@ -44,6 +46,7 @@ class Member extends Table { Set>? get primaryKey => {id}; } +@TableIndex(name: "idx_friends_updated", columns: {#updatedAt}) class Friend extends Table { TextColumn get id => text()(); TextColumn get name => text()(); @@ -56,10 +59,13 @@ class Friend extends Table { Set get primaryKey => {id}; } +@TableIndex(name: "idx_library_entry_created", columns: {#createdAt}) +@TableIndex(name: "idx_library_entry_idhash", columns: {#identifierHash}) class LibraryEntry extends Table { TextColumn get id => text()(); IntColumn get type => intEnum()(); Int64Column get createdAt => int64()(); + TextColumn get identifierHash => text().withDefault(Constant("to-migrate"))(); TextColumn get data => text()(); IntColumn get width => integer()(); IntColumn get height => integer()(); @@ -94,6 +100,7 @@ class Profile extends Table { Set get primaryKey => {id}; } +@TableIndex(name: "idx_requests_updated", columns: {#updatedAt}) class Request extends Table { TextColumn get id => text()(); TextColumn get name => text()(); @@ -115,11 +122,13 @@ class Setting extends Table { Set>? get primaryKey => {key}; } +@TableIndex(name: "idx_unknown_profiles_last_fetched", columns: {#lastFetched}) class UnknownProfile extends Table { TextColumn get id => text()(); TextColumn get name => text()(); TextColumn get displayName => text()(); TextColumn get keys => text()(); + DateTimeColumn get lastFetched => dateTime().withDefault(Constant(DateTime.fromMillisecondsSinceEpoch(0)))(); @override Set get primaryKey => {id}; diff --git a/lib/database/trusted_links.dart b/lib/database/trusted_links.dart index 365ab321..db024aa5 100644 --- a/lib/database/trusted_links.dart +++ b/lib/database/trusted_links.dart @@ -21,9 +21,8 @@ class TrustedLinkHelper { static late Setting _unsafeSetting; static void init() { - final controller = Get.find(); - _unsafeSetting = controller.settings[TrustedLinkSettings.unsafeSources]!; - _trustModeSetting = controller.settings[TrustedLinkSettings.trustMode]!; + _unsafeSetting = SettingController.settings[TrustedLinkSettings.unsafeSources]!; + _trustModeSetting = SettingController.settings[TrustedLinkSettings.trustMode]!; } /// Show a confirm popup to confirm the user wants to add a new domain (returns whether the domain was trusted) @@ -42,12 +41,9 @@ class TrustedLinkHelper { return false; } - final result = await showConfirmPopup(ConfirmWindow( - title: "file.links.title".tr, - text: "file.links.description".trParams({ - "domain": domain, - }), - )); + final result = await showConfirmPopup( + ConfirmWindow(title: "file.links.title".tr, text: "file.links.description".trParams({"domain": domain})), + ); if (result) { await db.trustedLink.insertOnConflictUpdate(TrustedLinkData(domain: domain)); @@ -128,7 +124,7 @@ void main() { "http://www.domain.co.uk", "http://something.domain.com", "http://some.some.some.domain.com/hello_world", - "hello.domain.com/something" + "hello.domain.com/something", ]; for (var testCase in testCases) { sendLog(TrustedLinkHelper.extractDomain(testCase)); diff --git a/lib/input_demo.dart b/lib/input_demo.dart deleted file mode 100644 index 0395dfeb..00000000 --- a/lib/input_demo.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:chat_interface/pages/chat/messages/message_formatter.dart'; -import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -class InputDemo extends StatefulWidget { - const InputDemo({super.key}); - - @override - State createState() => _InputDemoState(); -} - -class _InputDemoState extends State { - final MessageFormatter formatter = MessageFormatter(Get.theme.textTheme.labelLarge!, Get.theme.textTheme.bodyLarge!); - final text = "".obs; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: SizedBox( - width: 300, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Obx(() { - return RichText(text: formatter.build(text.value)); - }), - verticalSpacing(sectionSpacing), - FJTextField( - onChange: (value) => text.value = value, - ) - ], - ), - ), - ), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index c10df282..9061e63d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'dart:async'; -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; import 'package:chat_interface/controller/controller_manager.dart'; import 'package:chat_interface/pages/settings/app/log_settings.dart'; +import 'package:chat_interface/src/rust/api/engine.dart'; +import 'package:chat_interface/src/rust/api/general.dart'; +import 'package:chat_interface/src/rust/frb_generated.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; @@ -18,7 +20,9 @@ import 'app.dart'; // Configuration constants const appTag = "liphium_chat"; const appTagSpaces = "liphium_spaces"; -const protocolVersion = 7; +const protocolVersion = 8; +const currentVersionName = "1.0.0 Beta"; +const linuxDbusAppName = "com.liphium.chat"; final dio = Dio(); late final Sodium sodiumLib; @@ -30,35 +34,23 @@ const bool isWeb = kIsWeb || kIsWasm; const bool isDebug = bool.fromEnvironment("DEBUG_MODE", defaultValue: true); const bool checkVersion = bool.fromEnvironment("CHECK_VERSION", defaultValue: true); -// Authentication types -enum AuthType { - password(0, "password"), - totp(1, "totp"), - recoveryCode(2, "recoveryCode"), - passkey(3, "passkey"); - - final int id; - final String name; - - const AuthType(this.id, this.name); - - static AuthType fromId(int id) { - return AuthType.values.firstWhere((element) => element.id == id); - } -} - -const liveKitURL = ""; - Future initSodium() async { sodiumLib = await SodiumInit.init(); return true; } -final list = [].obs; - var executableArguments = []; void main(List args) async { + // Initialize libspaceship + await RustLib.init(); + await stopAllEngines(); + + // Create a log stream for communication with libspaceship + createLogStream().listen((log) { + sendLog("rust: $log"); + }); + // Handle errors from flutter final originalFunction = FlutterError.onError!; FlutterError.onError = (details) { @@ -71,20 +63,22 @@ void main(List args) async { unawaited(initApp(args)); } else { // Run everything in a zone for error collection - unawaited(runZonedGuarded( - () async { - unawaited(initApp(args)); - }, - (error, stack) { - LogManager.addError(error, stack); - }, - zoneSpecification: ZoneSpecification( - print: (self, parent, zone, line) async { - await LogManager.addLog(line); - parent.print(zone, line); + unawaited( + runZonedGuarded( + () async { + unawaited(initApp(args)); }, + (error, stack) { + LogManager.addError(error, stack); + }, + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) async { + await LogManager.addLog(line); + parent.print(zone, line); + }, + ), ), - )); + ); } } @@ -105,10 +99,6 @@ Future initApp(List args) async { // Wait for it to be finished await Future.delayed(100.ms); - if (isDebug) { - await encryptionTest(); - } - // Initialize controllers initializeControllers(); @@ -132,18 +122,3 @@ bool isDesktopPlatform() { } return GetPlatform.isDesktop; } - -Future encryptionTest() async { - final bob = generateAsymmetricKeyPair(); - final alice = generateAsymmetricKeyPair(); - - const message = "Hello world!"; - final encrypted = encryptAsymmetricAuth(bob.publicKey, alice.secretKey, message); - - // This should throw an exception - final result = decryptAsymmetricAuth(bob.publicKey, bob.secretKey, encrypted); - if (!result.success) { - sendLog("Authenticated encryption works!"); - } - return true; -} diff --git a/lib/pages/chat/chat_page_desktop.dart b/lib/pages/chat/chat_page_desktop.dart index fbaa03e4..c9a8c414 100644 --- a/lib/pages/chat/chat_page_desktop.dart +++ b/lib/pages/chat/chat_page_desktop.dart @@ -1,19 +1,24 @@ -import 'package:chat_interface/controller/conversation/message_controller.dart'; +import 'dart:math'; + +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/main.dart'; import 'package:chat_interface/pages/chat/chat_page_mobile.dart'; import 'package:chat_interface/pages/chat/components/conversations/message_bar.dart'; -import 'package:chat_interface/pages/chat/components/conversations/message_search_window.dart'; import 'package:chat_interface/pages/chat/components/message/message_feed.dart'; -import 'package:chat_interface/pages/chat/components/townsquare/townsquare_page.dart'; -import 'package:chat_interface/pages/chat/messages_page.dart'; import 'package:chat_interface/pages/chat/sidebar/sidebar.dart'; +import 'package:chat_interface/pages/settings/app/general_settings.dart'; import 'package:chat_interface/pages/spaces/space_rectangle.dart'; -import 'package:chat_interface/theme/desktop_widgets.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/util/platform_callback.dart'; +import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; +/// Get the correct chat page for the current platform Widget getChatPage() { if (isMobileMode()) { return const ChatPageMobile(); @@ -47,136 +52,146 @@ class _ChatPageDesktopState extends State { @override Widget build(BuildContext context) { - final controller = Get.find(); - return Scaffold( backgroundColor: Get.theme.colorScheme.inverseSurface, - body: CloseToTray( - child: SafeArea( - top: false, - bottom: false, - left: false, - child: PlatformCallback( - mobile: () { - final controller = Get.find(); - if (controller.currentProvider.value != null) { - Get.off(const ChatPageMobile()); - Get.to(MessagesPageMobile(provider: controller.currentProvider.value!)); - } else { - Get.off(const ChatPageMobile()); - } - }, - child: Row( - children: [ - // Render the sidebar (with an animation when it's hidden/shown) - SelectionContainer.disabled( - child: Obx( - () => Animate( - effects: [ - ExpandEffect( - curve: Curves.easeInOut, - duration: 250.ms, - axis: Axis.horizontal, - alignment: Alignment.centerRight, - ), - FadeEffect( - duration: 250.ms, - ) - ], - onInit: (ac) => ac.value = controller.hideSidebar.value ? 0 : 1, - target: controller.hideSidebar.value ? 0 : 1, - child: SizedBox( - width: 350, - child: Sidebar(), + body: SafeArea( + top: false, + bottom: false, + left: false, + child: PlatformCallback( + mobile: () { + Get.off(const ChatPageMobile()); + }, + child: Row( + children: [ + // Render the sidebar (with an animation when it's hidden/shown) + SelectionContainer.disabled( + child: Watch( + (ctx) => Animate( + effects: [ + ExpandEffect( + curve: Curves.easeInOut, + duration: 250.ms, + axis: Axis.horizontal, + alignment: Alignment.centerRight, ), - ), + FadeEffect(duration: 250.ms), + ], + onInit: (ac) => ac.value = SidebarController.hideSidebar.value ? 0 : 1, + target: SidebarController.hideSidebar.value ? 0 : 1, + child: SizedBox(width: 350, child: Sidebar()), ), ), + ), + + // Render the current sidebar tab + Expanded(child: Watch((ctx) => SidebarController.currentOpenTab.value.build(ctx))), + ], + ), + ), + ), + ); + } +} + +/// Default sidebar tab when the app is started +class DefaultSidebarTab extends SidebarTab { + DefaultSidebarTab() : super(SidebarTabType.none, "def"); + + @override + Widget build(BuildContext context) { + final message = Random().nextInt(19) + 1; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset("assets/tray/icon_macos.png", width: 150, height: 150), + verticalSpacing(sectionSpacing * 2), + SizedBox( + width: 400, + child: Text( + 'app.welcome.$message'.tr, + style: Theme.of(context).textTheme.labelLarge, + textAlign: TextAlign.center, + ), + ), + if (message == 8) + Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: FJElevatedButton( + onTap: () { + showErrorPopup("Our AI assistant", "Liph with deez nuts in your mouth :)"); + }, + child: Text("What is Liph?", style: Get.textTheme.labelMedium), + ), + ), + + verticalSpacing(defaultSpacing), + Text('app.build'.trParams({"build": currentVersionName}), style: Theme.of(context).textTheme.bodyMedium), + ], + ); + } +} + +/// Sidebar tab for a conversation +class ConversationSidebarTab extends SidebarTab { + final ConversationMessageProvider provider; + + ConversationSidebarTab(this.provider) + : super(SidebarTabType.conversation, "conv-${provider.conversation.id.encode()}"); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Render the message bar for desktop + DevicePadding( + top: true, + padding: const EdgeInsets.all(0), + child: MessageBar(conversation: provider.conversation, provider: provider), + ), + + // Render the message feed + search sidebar + Expanded( + child: Row( + children: [ + // Render the chat messages + Expanded(child: MessageFeed()), - // Render the conversation/space/other stuff - Expanded( - child: Obx( - () { - // Check if a space is selected (show the page if it is) - final controller = Get.find(); - switch (controller.currentOpenType.value) { - case OpenTabType.townsquare: - return const TownsquarePage(); - case OpenTabType.conversation: - if (controller.currentProvider.value == null) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('app.title'.tr, style: Theme.of(context).textTheme.headlineMedium), - verticalSpacing(sectionSpacing), - Text('app.welcome'.tr, style: Theme.of(context).textTheme.bodyLarge), - verticalSpacing(elementSpacing), - Text('app.build'.trParams({"build": "Alpha"}), style: Theme.of(context).textTheme.bodyLarge), - ], - ); - } - - return Column( - children: [ - // Render the message bar for desktop - DevicePadding( - top: true, - padding: const EdgeInsets.all(0), - child: MessageBar( - conversation: controller.currentProvider.value!.conversation, - provider: controller.currentProvider.value!, - ), - ), - - // Render the message feed + search sidebar - Expanded( - child: Row( - children: [ - // Render the chat messages - Expanded( - child: MessageFeed(), - ), - - // Render the search window - SelectionContainer.disabled( - child: Obx( - () => Animate( - effects: [ - ExpandEffect( - curve: Curves.easeInOut, - duration: 250.ms, - axis: Axis.horizontal, - alignment: Alignment.centerLeft, - ), - FadeEffect( - duration: 250.ms, - ) - ], - onInit: (ac) => ac.value = controller.showSearch.value ? 1 : 0, - target: controller.showSearch.value ? 1 : 0, - child: SizedBox( - width: 350, - child: MessageSearchWindow(), - ), - ), - ), - ), - ], - ), - ), - ], - ); - default: - return const SpaceRectangle(); - } - }, + // Render the search window + SelectionContainer.disabled( + child: Watch( + (ctx) => Animate( + key: ValueKey("rsa-${provider.conversation.id.encode()}"), + effects: [ + ExpandEffect( + curve: Curves.easeInOut, + duration: 250.ms, + axis: Axis.horizontal, + alignment: Alignment.centerLeft, + ), + FadeEffect(duration: 250.ms), + ], + onInit: (ac) => ac.value = SidebarController.rightSidebar[key] != null ? 1 : 0, + target: SidebarController.rightSidebar[key] != null ? 1 : 0, + child: SizedBox(width: 350, child: SidebarController.rightSidebar[key]?.build(ctx)), ), ), - ], - ), + ), + ], ), ), - ), + ], ); } } + +/// Sidebar tab for the Space the user is currently in +class SpaceSidebarTab extends SidebarTab { + SpaceSidebarTab() : super(SidebarTabType.space, "space"); + + @override + Widget build(BuildContext context) { + return const SpaceRectangle(); + } +} diff --git a/lib/pages/chat/chat_page_mobile.dart b/lib/pages/chat/chat_page_mobile.dart index 7c38360b..4a885154 100644 --- a/lib/pages/chat/chat_page_mobile.dart +++ b/lib/pages/chat/chat_page_mobile.dart @@ -10,14 +10,12 @@ import 'package:chat_interface/util/platform_callback.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ChatPageMobile extends StatefulWidget { final int selected; - const ChatPageMobile({ - super.key, - this.selected = 0, - }); + const ChatPageMobile({super.key, this.selected = 0}); @override State createState() => _ChatPageState(); @@ -25,10 +23,10 @@ class ChatPageMobile extends StatefulWidget { class _ChatPageState extends State { // The currently selected tab - final selected = 0.obs; + final _selected = signal(0); // All tabs that can be selected - final tabs = { + final _tabs = { 0: const ConversationListMobile(), 1: const OwnProfileMobile(), 2: const FriendsPage(), @@ -37,10 +35,16 @@ class _ChatPageState extends State { @override void initState() { - selected.value = widget.selected; + _selected.value = widget.selected; super.initState(); } + @override + void dispose() { + _selected.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -51,13 +55,9 @@ class _ChatPageState extends State { }, child: Column( children: [ - Expanded( - child: Obx(() => tabs[selected.value]!), - ), + Expanded(child: Watch((ctx) => _tabs[_selected.value]!)), Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.primaryContainer, - ), + decoration: BoxDecoration(color: Get.theme.colorScheme.primaryContainer), child: DevicePadding( bottom: true, right: true, @@ -75,42 +75,42 @@ class _ChatPageState extends State { right: defaultSpacing, left: defaultSpacing, ), - message: Get.find().error, + message: ConnectionController.error, ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SidebarIconButton( - onTap: () => selected.value = 0, + onTap: () => _selected.value = 0, icon: Icons.chat_bubble, index: 0, - selected: selected, + selected: _selected, ), SidebarIconButton( - onTap: () => selected.value = 1, + onTap: () => _selected.value = 1, icon: Icons.public, index: 1, - selected: selected, + selected: _selected, ), SidebarIconButton( - onTap: () => selected.value = 2, + onTap: () => _selected.value = 2, icon: Icons.group, index: 2, - selected: selected, + selected: _selected, ), SidebarIconButton( - onTap: () => selected.value = 3, + onTap: () => _selected.value = 3, icon: Icons.settings, index: 3, - selected: selected, + selected: _selected, ), ], ), ], ), ), - ) + ), ], ), ), diff --git a/lib/pages/chat/components/conversations/conversation_dev_window.dart b/lib/pages/chat/components/conversations/conversation_dev_window.dart index 4a983479..c7b76e8a 100644 --- a/lib/pages/chat/components/conversations/conversation_dev_window.dart +++ b/lib/pages/chat/components/conversations/conversation_dev_window.dart @@ -6,6 +6,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ConversationDevWindow extends StatefulWidget { final Conversation conversation; @@ -17,12 +18,12 @@ class ConversationDevWindow extends StatefulWidget { } class _ConversationAddWindowState extends State { - final messageDeletionLoading = false.obs; + final messageDeletionLoading = signal(false); @override Widget build(BuildContext context) { - final readDate = DateTime.fromMillisecondsSinceEpoch(widget.conversation.readAt.value.toInt()); - final updateDate = DateTime.fromMillisecondsSinceEpoch(widget.conversation.updatedAt.value.toInt()); + final readDate = DateTime.fromMillisecondsSinceEpoch(widget.conversation.reads.getMain()); + final updateDate = DateTime.fromMillisecondsSinceEpoch(widget.conversation.updatedAt.toInt()); sendLog(widget.conversation.lastVersion); return DialogBase( @@ -31,9 +32,7 @@ class _ConversationAddWindowState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "conversation.info.id".trParams({ - "id": widget.conversation.id.toString(), - }), + "conversation.info.id".trParams({"id": widget.conversation.id.toString()}), style: Get.textTheme.bodyMedium, ), verticalSpacing(elementSpacing), @@ -68,16 +67,12 @@ class _ConversationAddWindowState extends State { ), verticalSpacing(elementSpacing), Text( - "conversation.info.members".trParams({ - "count": widget.conversation.members.length.toString(), - }), + "conversation.info.members".trParams({"count": widget.conversation.members.length.toString()}), style: Get.textTheme.bodyMedium, ), verticalSpacing(elementSpacing), Text( - "conversation.info.version".trParams({ - "version": widget.conversation.lastVersion.toString(), - }), + "conversation.info.version".trParams({"version": widget.conversation.lastVersion.toString()}), style: Get.textTheme.bodyMedium, ), verticalSpacing(defaultSpacing), @@ -88,17 +83,17 @@ class _ConversationAddWindowState extends State { Clipboard.setData(ClipboardData(text: widget.conversation.id.toString())); Get.back(); }, - loading: false.obs, ), verticalSpacing(elementSpacing), ProfileButton( icon: Icons.copy, label: "conversation.info.copy_token".tr, onTap: () { - Clipboard.setData(ClipboardData(text: "${widget.conversation.token.id}:${widget.conversation.token.token}")); + Clipboard.setData( + ClipboardData(text: "${widget.conversation.token.id}:${widget.conversation.token.token}"), + ); Get.back(); }, - loading: false.obs, ), verticalSpacing(elementSpacing), ProfileButton( @@ -107,7 +102,6 @@ class _ConversationAddWindowState extends State { icon: Icons.close, label: "close".tr, onTap: () => Get.back(), - loading: false.obs, ), ], ), diff --git a/lib/pages/chat/components/conversations/conversation_edit_window.dart b/lib/pages/chat/components/conversations/conversation_edit_window.dart index 74eb5d7c..52505cc8 100644 --- a/lib/pages/chat/components/conversations/conversation_edit_window.dart +++ b/lib/pages/chat/components/conversations/conversation_edit_window.dart @@ -1,31 +1,34 @@ import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; import 'package:chat_interface/pages/chat/components/conversations/conversation_dev_window.dart'; +import 'package:chat_interface/pages/chat/components/conversations/conversation_rename_window.dart'; +import 'package:chat_interface/pages/chat/components/squares/topic_manage_window.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/services/squares/square_service.dart'; +import 'package:chat_interface/theme/ui/conversation_util.dart'; import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; -import 'package:chat_interface/theme/ui/profile/profile.dart'; import 'package:chat_interface/theme/ui/profile/profile_button.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class ConversationInfoWindow extends StatefulWidget { +class ConversationEditWindow extends StatefulWidget { final ContextMenuData position; final Conversation conversation; + final String extra; - const ConversationInfoWindow({ - super.key, - required this.position, - required this.conversation, - }); + const ConversationEditWindow({super.key, required this.position, required this.conversation, this.extra = ""}); @override - State createState() => _ConversationInfoWindowState(); + State createState() => _ConversationEditWindowState(); } -class _ConversationInfoWindowState extends State { +class _ConversationEditWindowState extends State { // Loading states - final deleteLoading = false.obs; + final deleteLoading = signal(false); @override Widget build(BuildContext context) { @@ -34,10 +37,16 @@ class _ConversationInfoWindowState extends State { title: [ Row( children: [ - Icon(widget.conversation.isGroup ? Icons.group : Icons.person, size: 30, color: Theme.of(context).colorScheme.onPrimary), + Icon( + ConversationUtil.getIconForConversation(widget.conversation, extra: widget.extra), + size: 30, + color: Theme.of(context).colorScheme.onPrimary, + ), horizontalSpacing(defaultSpacing), - Text(widget.conversation.isGroup ? widget.conversation.containerSub.value.name : widget.conversation.dmName, - style: Theme.of(context).textTheme.titleMedium), + Text( + ConversationUtil.getNameForConversation(widget.conversation, extra: widget.extra), + style: Theme.of(context).textTheme.titleMedium, + ), ], ), ], @@ -45,59 +54,107 @@ class _ConversationInfoWindowState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Render a button for changing the title of the conversation/square (only when no topic) Visibility( - visible: widget.conversation.isGroup, + visible: widget.conversation.isGroup && widget.extra == "", child: Padding( padding: const EdgeInsets.only(bottom: elementSpacing), child: ProfileButton( icon: Icons.edit, - label: "Edit title", - onTap: () => {}, - loading: false.obs, + label: "conversations.name.edit".tr, + onTap: () { + Get.back(); + Get.dialog(ConversationRenameWindow(conversation: widget.conversation)); + }, + ), + ), + ), + + // Render a button for editing the topic (only when one is there) + Visibility( + visible: widget.extra != "", + child: Padding( + padding: const EdgeInsets.only(bottom: elementSpacing), + child: ProfileButton( + icon: Icons.edit, + label: "squares.topics.edit".tr, + onTap: () { + // Get the topic + final container = widget.conversation.container as SquareContainer; + final topic = container.topics.firstWhereOrNull((t) => t.id == widget.extra); + if (topic == null) { + showErrorPopup("error", "not.found".tr); + return; + } + + // Open the window to manage the topic + Get.back(); + Get.dialog(TopicManageWindow(square: widget.conversation as Square, toEdit: topic)); + }, ), ), ), ProfileButton( icon: Icons.developer_mode, label: "For developers", - onTap: () => showModal(ConversationDevWindow(conversation: widget.conversation)), - loading: false.obs, + onTap: () { + Get.back(); + showModal(ConversationDevWindow(conversation: widget.conversation)); + }, ), verticalSpacing(sectionSpacing), - Text( - "Danger zone", - style: Get.theme.textTheme.bodyMedium, - ), + Text("Danger zone", style: Get.theme.textTheme.bodyMedium), verticalSpacing(elementSpacing), + + // Create a button for deleting the topic (in case we are editing one) Visibility( - visible: !widget.conversation.isGroup, - child: ProfileButton( - color: Get.theme.colorScheme.errorContainer, - iconColor: Get.theme.colorScheme.error, - icon: Icons.delete, - label: "Remove friend", - onTap: () { - ProfileDefaults.deleteAction.call(widget.conversation.otherMember, deleteLoading); - }, - loading: deleteLoading, + visible: widget.extra != "", + child: Padding( + padding: const EdgeInsets.only(top: elementSpacing), + child: ProfileButton( + color: Get.theme.colorScheme.errorContainer, + iconColor: Get.theme.colorScheme.error, + icon: Icons.delete, + label: "squares.topics.delete".tr, + loading: deleteLoading, + onTap: () async { + deleteLoading.value = true; + final error = await SquareService.deleteTopic(widget.conversation as Square, widget.extra); + deleteLoading.value = false; + if (error != null) { + showErrorPopup("error", error); + } else { + Get.back(); + } + }, + ), ), ), - verticalSpacing(elementSpacing), - ProfileButton( - color: Get.theme.colorScheme.errorContainer, - iconColor: Get.theme.colorScheme.error, - icon: Icons.logout, - label: "Leave conversation", - onTap: () => showConfirmPopup(ConfirmWindow( - title: "conversations.leave".tr, - text: "conversations.leave.text".tr, - onConfirm: () { - widget.conversation.delete(); - Get.back(); - }, - onDecline: () => {}, - )), - loading: false.obs, + + // Create a button for leaving the conversation (only show when not a topic) + Visibility( + visible: widget.extra == "", + child: Padding( + padding: const EdgeInsets.only(top: elementSpacing), + child: ProfileButton( + color: Get.theme.colorScheme.errorContainer, + iconColor: Get.theme.colorScheme.error, + icon: Icons.logout, + label: "conversations.leave".tr, + onTap: + () => showConfirmPopup( + ConfirmWindow( + title: "conversations.leave".tr, + text: "conversations.leave.text".tr, + onConfirm: () { + widget.conversation.delete(); + Get.back(); + }, + onDecline: () => {}, + ), + ), + ), + ), ), ], ), diff --git a/lib/pages/chat/components/conversations/conversation_info_mobile.dart b/lib/pages/chat/components/conversations/conversation_info_mobile.dart index 198396cf..e48416bf 100644 --- a/lib/pages/chat/components/conversations/conversation_info_mobile.dart +++ b/lib/pages/chat/components/conversations/conversation_info_mobile.dart @@ -6,21 +6,19 @@ import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/profile.dart'; import 'package:chat_interface/theme/ui/profile/profile_button.dart'; +import 'package:chat_interface/util/constants.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ConversationInfoMobile extends StatefulWidget { final ContextMenuData position; final Conversation conversation; - const ConversationInfoMobile({ - super.key, - required this.position, - required this.conversation, - }); + const ConversationInfoMobile({super.key, required this.position, required this.conversation}); @override State createState() => _ConversationInfoMobileState(); @@ -28,7 +26,7 @@ class ConversationInfoMobile extends StatefulWidget { class _ConversationInfoMobileState extends State { // Loading states - final deleteLoading = false.obs; + final deleteLoading = signal(false); @override Widget build(BuildContext context) { @@ -37,7 +35,11 @@ class _ConversationInfoMobileState extends State { title: [ Row( children: [ - Icon(widget.conversation.isGroup ? Icons.group : Icons.person, size: 30, color: Theme.of(context).colorScheme.onPrimary), + Icon( + widget.conversation.isGroup ? Icons.group : Icons.person, + size: 30, + color: Theme.of(context).colorScheme.onPrimary, + ), horizontalSpacing(defaultSpacing), Text( widget.conversation.isGroup ? widget.conversation.containerSub.value.name : widget.conversation.dmName, @@ -52,18 +54,13 @@ class _ConversationInfoMobileState extends State { children: [ // Show basic information about the conversation Text( - "conversation.info.town".trParams({ - "town": widget.conversation.id.server, - }), + "conversation.info.town".trParams({"town": widget.conversation.id.server}), style: Get.textTheme.bodyMedium, ), verticalSpacing(sectionSpacing), // Show things that can be done with the current conversation - Text( - "Actions", - style: Get.theme.textTheme.labelMedium, - ), + Text("Actions", style: Get.theme.textTheme.labelMedium), verticalSpacing(defaultSpacing), // The conversation search is here to make it easier to access @@ -71,7 +68,6 @@ class _ConversationInfoMobileState extends State { icon: Icons.search, label: "chat.search".tr, onTap: () => showModal(ConversationDevWindow(conversation: widget.conversation)), - loading: false.obs, ), verticalSpacing(elementSpacing), @@ -83,8 +79,8 @@ class _ConversationInfoMobileState extends State { child: ProfileButton( icon: Icons.electric_bolt, label: "chat.zapshare".tr, - onTap: () => Get.find().openWindow(widget.conversation, ContextMenuData.fromPosition(Offset.zero)), - loading: false.obs, + onTap: + () => ZapShareController.openWindow(widget.conversation, ContextMenuData.fromPosition(Offset.zero)), ), ), ), @@ -92,27 +88,18 @@ class _ConversationInfoMobileState extends State { visible: widget.conversation.isGroup, child: Padding( padding: const EdgeInsets.only(bottom: elementSpacing), - child: ProfileButton( - icon: Icons.edit, - label: "Edit title", - onTap: () => {}, - loading: false.obs, - ), + child: ProfileButton(icon: Icons.edit, label: "Edit title", onTap: () => {}), ), ), ProfileButton( icon: Icons.developer_mode, label: "dev.details".tr, onTap: () => showModal(ConversationDevWindow(conversation: widget.conversation)), - loading: false.obs, ), verticalSpacing(sectionSpacing), // Show that the conversation is encrypted (to make the user feel safe ig) - Text( - "Encryption", - style: Get.theme.textTheme.labelMedium, - ), + Text("Encryption", style: Get.theme.textTheme.labelMedium), verticalSpacing(defaultSpacing), Text("conversation.info.encrypted".tr, style: Get.textTheme.bodyMedium), verticalSpacing(defaultSpacing), @@ -121,15 +108,11 @@ class _ConversationInfoMobileState extends State { ProfileButton( icon: Icons.launch, label: "learn_more".tr, - onTap: () => launchUrlString("https://liphium.com"), // TODO: Make a page about encryption - loading: false.obs, + onTap: () => launchUrlString(Constants.docsEncryptionAndPrivacy), ), verticalSpacing(sectionSpacing), - Text( - "Danger zone", - style: Get.theme.textTheme.labelMedium, - ), + Text("Danger zone", style: Get.theme.textTheme.labelMedium), verticalSpacing(defaultSpacing), Visibility( visible: !widget.conversation.isGroup, @@ -150,16 +133,18 @@ class _ConversationInfoMobileState extends State { iconColor: Get.theme.colorScheme.error, icon: Icons.logout, label: "Leave conversation", - onTap: () => showConfirmPopup(ConfirmWindow( - title: "conversations.leave".tr, - text: "conversations.leave.text".tr, - onConfirm: () { - widget.conversation.delete(); - Get.back(); - }, - onDecline: () => {}, - )), - loading: false.obs, + onTap: + () => showConfirmPopup( + ConfirmWindow( + title: "conversations.leave".tr, + text: "conversations.leave.text".tr, + onConfirm: () { + widget.conversation.delete(); + Get.back(); + }, + onDecline: () => {}, + ), + ), ), ], ), diff --git a/lib/pages/chat/components/conversations/conversation_members.dart b/lib/pages/chat/components/conversations/conversation_members.dart deleted file mode 100644 index 04091146..00000000 --- a/lib/pages/chat/components/conversations/conversation_members.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/member_controller.dart'; -import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/theme/components/forms/icon_button.dart'; -import 'package:chat_interface/theme/components/user_renderer.dart'; -import 'package:chat_interface/theme/ui/profile/profile.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -class ConversationMembers extends StatelessWidget { - final Conversation conversation; - - const ConversationMembers({super.key, required this.conversation}); - - @override - Widget build(BuildContext context) { - final ownRole = conversation.members[conversation.token.id]!.role; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: elementSpacing, vertical: defaultSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: defaultSpacing + elementSpacing), - child: Obx( - () => Text( - 'chat.members'.trParams({"count": conversation.members.length.toString()}), - style: Get.theme.textTheme.titleMedium, - ), - ), - ), - LoadingIconButton( - loading: conversation.membersLoading, - onTap: () => conversation.fetchData(), - icon: Icons.refresh, - ), - ], - ), - verticalSpacing(defaultSpacing), - Padding( - padding: const EdgeInsets.symmetric(horizontal: elementSpacing), - child: Obx( - () => ListView.builder( - shrinkWrap: true, - itemCount: conversation.members.length, - itemBuilder: (context, index) { - final GlobalKey listKey = GlobalKey(); - final member = conversation.members.values.elementAt(index); - return Padding( - key: listKey, - padding: const EdgeInsets.only(bottom: elementSpacing), - child: Material( - color: Get.theme.colorScheme.onInverseSurface, - child: InkWell( - borderRadius: BorderRadius.circular(defaultSpacing), - onTap: () { - final friend = Get.find().friends[member.address]; - if (StatusController.ownAddress != member.address) { - final RenderBox box = listKey.currentContext?.findRenderObject() as RenderBox; - Get.dialog( - Profile( - position: box.localToGlobal(box.size.bottomLeft(Offset.zero)), - friend: friend ?? Friend.unknown(member.address), - size: box.size.width.toInt(), - actions: (friend) { - return [ - //* Promotion actions - if (ownRole.higherOrEqual(MemberRole.moderator) && member.role == MemberRole.user) - ProfileAction( - icon: Icons.add_moderator, - label: "chat.make_moderator".tr, - loading: false.obs, - onTap: (f, l) => member.promote(conversation.id)) - else if (ownRole == MemberRole.admin && member.role == MemberRole.moderator) - ProfileAction( - icon: Icons.add_moderator, - label: "chat.make_admin".tr, - loading: false.obs, - onTap: (f, l) => member.promote(conversation.id)), - - //* Demotion actions - if (ownRole.higherOrEqual(MemberRole.moderator) && member.role == MemberRole.moderator) - ProfileAction( - icon: Icons.remove_moderator, - label: "chat.remove_moderator".tr, - loading: false.obs, - onTap: (f, l) => member.demote(conversation.id), - ) - else if (ownRole == MemberRole.admin && member.role.higherOrEqual(MemberRole.moderator)) - ProfileAction( - icon: Icons.remove_moderator, - label: "chat.remove_admin".tr, - loading: false.obs, - onTap: (f, l) => member.demote(conversation.id), - ), - - //* Removal actions - if (ownRole.higherOrEqual(MemberRole.moderator) && member.role.lowerThan(ownRole)) - ProfileAction( - icon: Icons.person_remove, - label: "chat.remove_member".tr, - loading: false.obs, - color: Get.theme.colorScheme.errorContainer, - iconColor: Get.theme.colorScheme.error, - onTap: (f, l) => member.remove(conversation.id), - ), - ] + - ProfileDefaults.buildDefaultActions(friend); - }, - ), - ); - } - return; - }, - child: Padding( - padding: const EdgeInsets.all(elementSpacing), - child: Row( - children: [ - Expanded( - child: UserRenderer( - id: member.address, - ), - ), - horizontalSpacing(elementSpacing), - if (member.role != MemberRole.user) - Padding( - padding: const EdgeInsets.only(left: defaultSpacing), - child: Tooltip( - message: member.role == MemberRole.admin ? "chat.admin".tr : "chat.moderator".tr, - child: Icon( - Icons.shield, - color: member.role == MemberRole.admin ? Get.theme.colorScheme.error : Get.theme.colorScheme.onPrimary, - ), - ), - ), - ], - ), - ), - ), - ), - ); - }, - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/chat/components/conversations/conversation_members_bar.dart b/lib/pages/chat/components/conversations/conversation_members_bar.dart new file mode 100644 index 00000000..4ced3007 --- /dev/null +++ b/lib/pages/chat/components/conversations/conversation_members_bar.dart @@ -0,0 +1,231 @@ +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/pages/chat/components/squares/square_shared_spaces.dart'; +import 'package:chat_interface/services/chat/conversation_member.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/theme/components/forms/icon_button.dart'; +import 'package:chat_interface/theme/components/user_renderer.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/theme/ui/profile/profile.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +/// Right sidebar implementation +class ConversationMembersRightSidebar extends RightSidebar { + final Conversation conversation; + ConversationMembersRightSidebar(this.conversation) : super("conv-members"); + + @override + Widget build(BuildContext context) { + return ConversationMembers(conversation: conversation); + } +} + +/// The actual widget behind it +class ConversationMembers extends StatelessWidget { + final Conversation conversation; + + const ConversationMembers({super.key, required this.conversation}); + + @override + Widget build(BuildContext context) { + final ownRole = conversation.members[conversation.token.id]!.role; + + return Container( + color: Get.theme.colorScheme.onInverseSurface, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: elementSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Render the shared spaces in case it's a square + if (conversation is Square) + Padding( + padding: const EdgeInsets.only(bottom: defaultSpacing), + child: SquareSharedSpaces(square: conversation as Square), + ), + + // Render the rest + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing + elementSpacing), + child: Watch( + (ctx) => Text( + 'chat.members'.trParams({"count": conversation.members.length.toString()}), + style: Get.theme.textTheme.titleMedium, + ), + ), + ), + LoadingIconButton( + loading: conversation.membersLoading, + onTap: () => ConversationService.fetchNewestVersion(conversation), + icon: Icons.refresh, + ), + ], + ), + verticalSpacing(defaultSpacing), + Padding( + padding: const EdgeInsets.symmetric(horizontal: elementSpacing), + child: Watch( + (ctx) => ListView.builder( + shrinkWrap: true, + itemCount: conversation.members.length, + itemBuilder: (context, index) { + final GlobalKey listKey = GlobalKey(); + final member = conversation.members.values.elementAt(index); + return Padding( + key: listKey, + padding: const EdgeInsets.only(bottom: elementSpacing), + child: Material( + color: Get.theme.colorScheme.onInverseSurface, + child: InkWell( + borderRadius: BorderRadius.circular(defaultSpacing), + onTap: () { + final friend = FriendController.friends[member.address]; + if (StatusController.ownAddress != member.address) { + final RenderBox box = listKey.currentContext?.findRenderObject() as RenderBox; + Get.dialog( + Profile( + data: ContextMenuData.fromKey(listKey, below: true), + friend: friend ?? Friend.unknown(member.address), + size: box.size.width.toInt(), + actions: (friend) { + return [ + //* Promotion actions + if (ownRole.higherOrEqual(MemberRole.moderator) && + member.role == MemberRole.user) + ProfileAction( + icon: Icons.add_moderator, + label: "chat.make_moderator".tr, + onTap: (f, loading) async { + loading.value = true; + final error = await member.promote(conversation); + if (error != null) { + showErrorPopup("error", error); + } else { + Get.back(); + } + loading.value = false; + }, + ) + else if (ownRole == MemberRole.admin && member.role == MemberRole.moderator) + ProfileAction( + icon: Icons.add_moderator, + label: "chat.make_admin".tr, + onTap: (f, loading) async { + loading.value = true; + final error = await member.promote(conversation); + if (error != null) { + showErrorPopup("error", error); + } else { + Get.back(); + } + loading.value = false; + }, + ), + + //* Demotion actions + if (ownRole.higherOrEqual(MemberRole.moderator) && + member.role == MemberRole.moderator) + ProfileAction( + icon: Icons.remove_moderator, + label: "chat.remove_moderator".tr, + onTap: (f, loading) async { + loading.value = true; + final error = await member.demote(conversation); + if (error != null) { + showErrorPopup("error", error); + } else { + Get.back(); + } + loading.value = false; + }, + ) + else if (ownRole == MemberRole.admin && + member.role.higherOrEqual(MemberRole.moderator)) + ProfileAction( + icon: Icons.remove_moderator, + label: "chat.remove_admin".tr, + onTap: (f, loading) async { + loading.value = true; + final error = await member.demote(conversation); + if (error != null) { + showErrorPopup("error", error); + } else { + Get.back(); + } + loading.value = false; + }, + ), + + //* Removal actions + if (ownRole.higherOrEqual(MemberRole.moderator) && + member.role.lowerThan(ownRole)) + ProfileAction( + icon: Icons.person_remove, + label: "chat.remove_member".tr, + color: Get.theme.colorScheme.errorContainer, + iconColor: Get.theme.colorScheme.error, + onTap: (f, loading) async { + loading.value = true; + final error = await member.remove(conversation); + if (error != null) { + showErrorPopup("error", error); + } else { + Get.back(); + } + loading.value = false; + }, + ), + ] + + ProfileDefaults.buildDefaultActions(friend); + }, + ), + ); + } + return; + }, + child: Padding( + padding: const EdgeInsets.all(elementSpacing), + child: Row( + children: [ + Expanded(child: UserRenderer(id: member.address)), + horizontalSpacing(elementSpacing), + if (member.role != MemberRole.user) + Padding( + padding: const EdgeInsets.only(left: defaultSpacing), + child: Tooltip( + message: member.role == MemberRole.admin ? "chat.admin".tr : "chat.moderator".tr, + child: Icon( + Icons.shield, + color: + member.role == MemberRole.admin + ? Get.theme.colorScheme.error + : Get.theme.colorScheme.onPrimary, + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat/components/conversations/conversation_members_page.dart b/lib/pages/chat/components/conversations/conversation_members_page.dart index 8cf1e81f..ad7ff861 100644 --- a/lib/pages/chat/components/conversations/conversation_members_page.dart +++ b/lib/pages/chat/components/conversations/conversation_members_page.dart @@ -4,10 +4,7 @@ import 'package:flutter/material.dart'; class ConversationMembersPage extends StatefulWidget { final Conversation conversation; - const ConversationMembersPage({ - super.key, - required this.conversation, - }); + const ConversationMembersPage({super.key, required this.conversation}); @override State createState() => _ConversationMembersPageState(); diff --git a/lib/pages/chat/components/conversations/conversation_rename_window.dart b/lib/pages/chat/components/conversations/conversation_rename_window.dart new file mode 100644 index 00000000..eb626d22 --- /dev/null +++ b/lib/pages/chat/components/conversations/conversation_rename_window.dart @@ -0,0 +1,106 @@ +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/theme/components/forms/fj_button.dart'; +import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/constants.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class ConversationRenameWindow extends StatefulWidget { + final Conversation conversation; + + const ConversationRenameWindow({super.key, required this.conversation}); + + @override + State createState() => _ConversationRenameWindowState(); +} + +class _ConversationRenameWindowState extends State { + // Text controllers + final _titleController = TextEditingController(); + + // State + final _errorText = signal(''); + final _loading = signal(false); + + @override + void initState() { + _titleController.text = widget.conversation.containerSub.value.name; + super.initState(); + } + + @override + void dispose() { + _errorText.dispose(); + _loading.dispose(); + _titleController.dispose(); + super.dispose(); + } + + /// Save the conversation title + Future save() async { + if (_loading.value) return; + _loading.value = true; + _errorText.value = ""; + + // Make sure the name fits within the requirements + final name = _titleController.text; + if (name.isEmpty || name == "") { + _errorText.value = "enter.name".tr; + _loading.value = false; + return; + } + if (name.length > specialConstants[Constants.specialConstantMaxConversationNameLength]!) { + _errorText.value = "conversations.name.length".trParams({ + "length": specialConstants["max_conversation_name_length"].toString(), + }); + _loading.value = false; + return; + } + + // Change the data of the conversation + final error = await ConversationService.setData(widget.conversation, ConversationContainer(_titleController.text)); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + + _loading.value = false; + Get.back(); + } + + @override + Widget build(BuildContext context) { + return DialogBase( + title: [Text("conversations.name.edit".tr, style: Get.textTheme.labelLarge)], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FJTextField( + hintText: 'conversations.name.placeholder'.tr, + controller: _titleController, + maxLength: specialConstants[Constants.specialConstantMaxConversationNameLength], + autofocus: true, + onSubmitted: (t) => save(), + ), + verticalSpacing(defaultSpacing), + AnimatedErrorContainer( + message: _errorText, + padding: const EdgeInsets.only(bottom: defaultSpacing), + expand: true, + ), + FJElevatedLoadingButtonCustom( + loading: _loading, + onTap: () => save(), + child: Center(child: Text("save".tr, style: Get.theme.textTheme.labelLarge)), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat/components/conversations/conversation_ringing_window.dart b/lib/pages/chat/components/conversations/conversation_ringing_window.dart index d139ed89..76f00eb9 100644 --- a/lib/pages/chat/components/conversations/conversation_ringing_window.dart +++ b/lib/pages/chat/components/conversations/conversation_ringing_window.dart @@ -2,8 +2,8 @@ import 'dart:math'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; import 'package:chat_interface/controller/spaces/ringing_manager.dart'; -import 'package:chat_interface/controller/spaces/space_container.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -13,11 +13,7 @@ class ConversationRingingWindow extends StatefulWidget { final Conversation conversation; final SpaceConnectionContainer container; - const ConversationRingingWindow({ - super.key, - required this.conversation, - required this.container, - }); + const ConversationRingingWindow({super.key, required this.conversation, required this.container}); @override State createState() => ConversationRingingWindowState(); @@ -28,89 +24,66 @@ class ConversationRingingWindowState extends State { Widget build(BuildContext context) { return Center( child: Animate( - effects: [ - ScaleEffect( - duration: 250.ms, - curve: Curves.ease, - ) - ], - child: LayoutBuilder(builder: (context, constraints) { - return Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.onInverseSurface, - borderRadius: BorderRadius.circular(sectionSpacing), - ), - padding: const EdgeInsets.all(dialogPadding), - width: min(constraints.maxWidth * 0.9, 350), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Animate( - effects: [ - ShakeEffect( - delay: 1000.ms, - rotation: 0.1, - duration: 500.ms, - ), - ], - onInit: (controller) { - controller.forward(from: 0); - }, - onComplete: (controller) { - controller.forward(from: 0); - }, - child: Icon( - Icons.call, - color: Get.theme.colorScheme.onPrimary, - size: 55, + effects: [ScaleEffect(duration: 250.ms, curve: Curves.ease)], + child: LayoutBuilder( + builder: (context, constraints) { + return Container( + decoration: BoxDecoration( + color: Get.theme.colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(sectionSpacing), + ), + padding: const EdgeInsets.all(dialogPadding), + width: min(constraints.maxWidth * 0.9, 350), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Animate( + effects: [ShakeEffect(delay: 1000.ms, rotation: 0.1, duration: 500.ms)], + onInit: (controller) { + controller.forward(from: 0); + }, + onComplete: (controller) { + controller.forward(from: 0); + }, + child: Icon(Icons.call, color: Get.theme.colorScheme.onPrimary, size: 55), ), - ), - verticalSpacing(elementSpacing), - Text( - widget.conversation.isGroup ? widget.conversation.container.name : widget.conversation.dmName, - style: Get.textTheme.headlineMedium, - textHeightBehavior: noTextHeight, - ), - verticalSpacing(elementSpacing), - Text( - "spaces.calling".tr, - style: Get.textTheme.bodyLarge, - textHeightBehavior: noTextHeight, - ), - verticalSpacing(sectionSpacing), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.done, - size: 30, + verticalSpacing(elementSpacing), + Text( + widget.conversation.isGroup ? widget.conversation.container.name : widget.conversation.dmName, + style: Get.textTheme.headlineMedium, + textHeightBehavior: noTextHeight, + ), + verticalSpacing(elementSpacing), + Text("spaces.calling".tr, style: Get.textTheme.bodyLarge, textHeightBehavior: noTextHeight), + verticalSpacing(sectionSpacing), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.done, size: 30), + onPressed: () { + Get.back(); + RingingManager.stopRingtone(); + SpaceController.join(widget.container); + }, + color: Get.theme.colorScheme.secondary, ), - onPressed: () { - Get.back(); - RingingManager.stopRingtone(); - Get.find().join(widget.container); - }, - color: Get.theme.colorScheme.secondary, - ), - horizontalSpacing(defaultSpacing), - IconButton( - icon: const Icon( - Icons.close, - size: 30, + horizontalSpacing(defaultSpacing), + IconButton( + icon: const Icon(Icons.close, size: 30), + onPressed: () { + RingingManager.stopRingtone(); + Get.back(); + }, + color: Get.theme.colorScheme.error, ), - onPressed: () { - RingingManager.stopRingtone(); - Get.back(); - }, - color: Get.theme.colorScheme.error, - ), - ], - ), - ], - ), - ); - }), + ], + ), + ], + ), + ); + }, + ), ), ); } diff --git a/lib/pages/chat/components/conversations/message_bar.dart b/lib/pages/chat/components/conversations/message_bar.dart index f6e661f9..d72f14b6 100644 --- a/lib/pages/chat/components/conversations/message_bar.dart +++ b/lib/pages/chat/components/conversations/message_bar.dart @@ -1,15 +1,18 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/conversation/message_provider.dart'; -import 'package:chat_interface/controller/conversation/message_search_controller.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; import 'package:chat_interface/controller/conversation/zap_share_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/database/database_entities.dart' as model; -import 'package:chat_interface/pages/chat/components/conversations/conversation_edit_window.dart'; +import 'package:chat_interface/pages/chat/components/conversations/conversation_members_bar.dart'; +import 'package:chat_interface/pages/chat/components/conversations/message_search_bar.dart'; import 'package:chat_interface/pages/settings/data/settings_controller.dart'; import 'package:chat_interface/pages/status/error/offline_hider.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/theme/ui/dialogs/conversation_add_window.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; @@ -19,11 +22,12 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:liphium_bridge/liphium_bridge.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:url_launcher/url_launcher_string.dart'; class MessageBar extends StatefulWidget { final Conversation conversation; - final MessageProvider provider; + final ConversationMessageProvider provider; const MessageBar({super.key, required this.conversation, required this.provider}); @@ -33,13 +37,16 @@ class MessageBar extends StatefulWidget { class _MessageBarState extends State { final GlobalKey _infoKey = GlobalKey(), _zapShareKey = GlobalKey(); - final callLoading = false.obs; + final additionLoading = signal(false); @override - Widget build(BuildContext context) { - final zapShareController = Get.find(); - final controller = Get.find(); + void dispose() { + additionLoading.dispose(); + super.dispose(); + } + @override + Widget build(BuildContext context) { if (widget.conversation.borked) { return Material( color: Get.theme.colorScheme.onInverseSurface, @@ -68,10 +75,10 @@ class _MessageBarState extends State { mainAxisSize: MainAxisSize.min, children: [ // Show a hide sidebar icon for more focus on the current conversation - Obx( - () => LoadingIconButton( - onTap: () => Get.find().toggleSidebar(), - icon: Get.find().hideSidebar.value ? Icons.arrow_forward : Icons.arrow_back, + Watch( + (ctx) => LoadingIconButton( + onTap: () => SidebarController.toggleSidebar(), + icon: SidebarController.hideSidebar.value ? Icons.arrow_forward : Icons.arrow_back, ), ), horizontalSpacing(elementSpacing), @@ -85,22 +92,24 @@ class _MessageBarState extends State { borderRadius: BorderRadius.circular(defaultSpacing), hoverColor: Get.theme.hoverColor, onTap: () { - showModal(ConversationInfoWindow( - conversation: widget.conversation, - position: ContextMenuData.fromKey(_infoKey, below: true), - )); + widget.provider.openDialogForConversation(ContextMenuData.fromKey(_infoKey, below: true)); }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: elementSpacing, - horizontal: defaultSpacing, - ), + padding: const EdgeInsets.symmetric(vertical: elementSpacing, horizontal: defaultSpacing), child: Row( children: [ - Icon(widget.conversation.isGroup ? Icons.group : Icons.person, size: 30, color: Theme.of(context).colorScheme.onPrimary), + Icon( + widget.provider.getIconForConversation(), + size: 30, + color: Theme.of(context).colorScheme.onPrimary, + ), horizontalSpacing(defaultSpacing), - Text(widget.conversation.isGroup ? widget.conversation.containerSub.value.name : widget.conversation.dmName, - style: Theme.of(context).textTheme.titleMedium), + Watch( + (ctx) => Text( + widget.provider.getNameForConversation(), + style: Theme.of(context).textTheme.titleMedium, + ), + ), ], ), ), @@ -110,7 +119,7 @@ class _MessageBarState extends State { ), //* Conversation actions - Obx(() { + Watch((ctx) { final error = widget.conversation.error.value != null; return Row( @@ -122,13 +131,18 @@ class _MessageBarState extends State { child: Row( children: [ //* Zap share - if (widget.conversation.type == model.ConversationType.directMessage && isDirectorySupported && !error) + if (widget.conversation.type == model.ConversationType.directMessage && + isDirectorySupported && + !error) Stack( key: _zapShareKey, children: [ IconButton( onPressed: () async { - await zapShareController.openWindow(widget.conversation, ContextMenuData.fromKey(_zapShareKey, below: true)); + await ZapShareController.openWindow( + widget.conversation, + ContextMenuData.fromKey(_zapShareKey, below: true), + ); }, icon: Icon(Icons.electric_bolt, color: Get.theme.colorScheme.onPrimary), tooltip: "chat.zapshare".tr, @@ -139,63 +153,83 @@ class _MessageBarState extends State { height: 48 - defaultSpacing, child: Padding( padding: const EdgeInsets.all(2.0), - child: Obx( - () => CircularProgressIndicator( - value: zapShareController.waiting.value ? null : zapShareController.progress.value.clamp(0, 1), + child: Watch( + (ctx) => CircularProgressIndicator( + value: + ZapShareController.waiting.value + ? null + : ZapShareController.progress.value.clamp(0, 1), strokeWidth: 3, valueColor: AlwaysStoppedAnimation(Get.theme.colorScheme.onPrimary), ), ), ), ), - ) + ), ], ), - if (Get.find().inSpace.value && areSpacesSupported && !error) - LoadingIconButton( - icon: Icons.forward_to_inbox, - iconSize: 27, - loading: callLoading, - tooltip: "chat.invite_to_space".tr, - onTap: () { - final controller = Get.find(); - controller.inviteToCall(widget.provider); - }, - ), + // Render an invite button in case the user is currently in a Space + Watch((context) { + if (SpaceController.connected.value && + widget.conversation is! Square && + areSpacesSupported && + !error) { + return LoadingIconButton( + icon: Icons.forward_to_inbox, + iconSize: 27, + loading: additionLoading, + tooltip: "chat.invite_to_space".tr, + onTap: () { + SpaceController.inviteToCall(widget.provider); + }, + ); + } + + return const SizedBox(); + }), // Only show launch button in case supported - if (areSpacesSupported && !error) + if (areSpacesSupported && widget.conversation is! Square && !error) LoadingIconButton( icon: Icons.rocket_launch, iconSize: 27, - loading: callLoading, + loading: additionLoading, tooltip: "chat.start_space".tr, onTap: () { - final controller = Get.find(); - controller.createAndConnect(widget.provider); + SpaceController.createAndConnect(widget.provider); }, ), // Give the user the ability to add people to a conversation - if (!error) - ConversationAddButton( - conversation: widget.conversation, - loading: callLoading, - ), + if (!error) ConversationAddButton(conversation: widget.conversation, loading: additionLoading), Visibility( visible: widget.conversation.isGroup, - child: Obx( - () => IconButton( + child: Watch( + (ctx) => IconButton( iconSize: 27, - icon: Icon(Icons.group, - color: controller.settings[AppSettings.showGroupMembers]!.value.value - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onSurface), + icon: Icon( + Icons.group, + color: + SidebarController.rightSidebar[SidebarController.getCurrentKey()] + is ConversationMembersRightSidebar + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface, + ), onPressed: () { - controller.settings[AppSettings.showGroupMembers]! - .setValue(!controller.settings[AppSettings.showGroupMembers]!.value.value); + if (SidebarController.rightSidebar[SidebarController.getCurrentKey()] + is ConversationMembersRightSidebar) { + // Hide the sidebar in case it is currently there + SettingController.settings[AppSettings.showGroupMembers]!.setValue(false); + SidebarController.setRightSidebar(null); + } else { + // Show the sidebar + SettingController.settings[AppSettings.showGroupMembers]!.setValue(true); + SidebarController.setRightSidebar( + ConversationMembersRightSidebar(widget.conversation), + ); + } }, ), ), @@ -204,18 +238,29 @@ class _MessageBarState extends State { ), ), - // Search the entire conversation - Obx( - () => IconButton( + // Search the conversation + Watch( + (ctx) => IconButton( iconSize: 27, - icon: Icon(Icons.search, - color: Get.find().showSearch.value - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onSurface), + icon: Icon( + Icons.search, + color: + SidebarController.rightSidebar[SidebarController.getCurrentKey()] + is MessageSearchRightSidebar + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface, + ), onPressed: () { - Get.find().toggleSearchView(); - if (Get.find().showSearch.value) { - Get.find().currentFocus!.requestFocus(); + if (SidebarController.rightSidebar[SidebarController.getCurrentKey()] + is MessageSearchRightSidebar) { + MessageController.restoreRightSidebar(); + } else { + SidebarController.setRightSidebar( + MessageSearchRightSidebar( + SidebarController.getCurrentKey(), + widget.conversation.id.encode(), + ), + ); } }, ), @@ -241,7 +286,7 @@ class _MessageBarState extends State { class ConversationAddButton extends StatefulWidget { final Conversation conversation; - final RxBool loading; + final Signal loading; const ConversationAddButton({super.key, required this.conversation, required this.loading}); @@ -280,43 +325,50 @@ class _ConversationAddButtonState extends State { } initial.add(friend); } - Get.dialog(ConversationAddWindow( - title: "conversations.add", - action: "add", - nameField: false, - position: ContextMenuData(position, true, false), - initial: initial, - onDone: (friends, name) async { - final finalList = []; - for (var friend in friends) { - if (!initial.any((element) => element.id == friend.id)) { - finalList.add(friend); + Get.dialog( + ConversationAddWindow( + title: "conversations.add", + action: "add", + nameField: false, + position: ContextMenuData(position, true, false), + initial: initial, + onDone: (friends, name) async { + final finalList = []; + for (var friend in friends) { + if (!initial.any((element) => element.id == friend.id)) { + finalList.add(friend); + } } - } - // Add the people to the conversation - for (var friend in finalList) { - final res = await addToConversation(widget.conversation, friend); - if (!res) { - showErrorPopup("error", "server.error".tr); - return null; + // Add the people to the conversation + for (var friend in finalList) { + final error = await ConversationService.addToConversation(widget.conversation, friend); + if (error != null) { + showErrorPopup("error", error); + return null; + } } - } - return null; - }, - )); + return null; + }, + ), + ); } else { // Get the friend and open the window - final friend = widget.conversation.members.values.firstWhere((element) => element.address != StatusController.ownAddress).getFriend(); + final friend = + widget.conversation.members.values + .firstWhere((element) => element.address != StatusController.ownAddress) + .getFriend(); if (friend.unknown) { return; } - showModal(ConversationAddWindow( - title: "conversations.add.create", - position: ContextMenuData(position, true, false), - initial: [friend], - )); + showModal( + ConversationAddWindow( + title: "conversations.add.create", + position: ContextMenuData(position, true, false), + initial: [friend], + ), + ); } }, ); diff --git a/lib/pages/chat/components/conversations/message_bar_mobile.dart b/lib/pages/chat/components/conversations/message_bar_mobile.dart index 55ef98b5..145ff1e8 100644 --- a/lib/pages/chat/components/conversations/message_bar_mobile.dart +++ b/lib/pages/chat/components/conversations/message_bar_mobile.dart @@ -7,21 +7,14 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -class MobileMessageBar extends StatefulWidget { +class MobileMessageBar extends StatelessWidget { final Conversation conversation; const MobileMessageBar({super.key, required this.conversation}); - @override - State createState() => _MessageBarState(); -} - -class _MessageBarState extends State { - final callLoading = false.obs; - @override Widget build(BuildContext context) { - if (widget.conversation.borked) { + if (conversation.borked) { return Material( color: Get.theme.colorScheme.onInverseSurface, child: Padding( @@ -40,20 +33,13 @@ class _MessageBarState extends State { return Material( color: Get.theme.colorScheme.onInverseSurface, child: InkWell( - onTap: () => Get.to(ConversationMembersPage( - conversation: widget.conversation, - )), + onTap: () => Get.to(ConversationMembersPage(conversation: conversation)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: defaultSpacing, vertical: elementSpacing), child: Row( children: [ //* Back button - LoadingIconButton( - icon: Icons.arrow_back, - iconSize: 27, - loading: callLoading, - onTap: () => Get.back(), - ), + LoadingIconButton(icon: Icons.arrow_back, iconSize: 27, onTap: () => Get.back()), //* Conversation label Expanded( @@ -61,11 +47,15 @@ class _MessageBarState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(widget.conversation.isGroup ? Icons.group : Icons.person, size: 30, color: Theme.of(context).colorScheme.onPrimary), + Icon( + conversation.isGroup ? Icons.group : Icons.person, + size: 30, + color: Theme.of(context).colorScheme.onPrimary, + ), horizontalSpacing(elementSpacing), Flexible( child: Text( - widget.conversation.isGroup ? widget.conversation.containerSub.value.name : widget.conversation.dmName, + conversation.isGroup ? conversation.containerSub.value.name : conversation.dmName, style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.ellipsis, ), @@ -79,11 +69,13 @@ class _MessageBarState extends State { LoadingIconButton( icon: Icons.more_vert, iconSize: 27, - loading: callLoading, - onTap: () => showModal(ConversationInfoMobile( - conversation: widget.conversation, - position: const ContextMenuData(Offset(0, 0), false, false), - )), + onTap: + () => showModal( + ConversationInfoMobile( + conversation: conversation, + position: const ContextMenuData(Offset(0, 0), false, false), + ), + ), ), ], ), diff --git a/lib/pages/chat/components/conversations/message_search_bar.dart b/lib/pages/chat/components/conversations/message_search_bar.dart new file mode 100644 index 00000000..8ed73b06 --- /dev/null +++ b/lib/pages/chat/components/conversations/message_search_bar.dart @@ -0,0 +1,165 @@ +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/pages/chat/components/message/renderer/material/material_message_renderer.dart'; +import 'package:chat_interface/services/chat/message_search_query.dart'; +import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +/// Right sidebar implementation for the sidebar controller +class MessageSearchRightSidebar extends RightSidebar { + late final MessageSearchQuery query; + + // Make sure the thing is cached + MessageSearchRightSidebar(super.key, String conversationId) : super(cache: true) { + query = MessageSearchQuery(); + query.filters.add(ConversationFilter(conversationId)); + } + + @override + Widget build(BuildContext context) { + return MessageSearchSidebar(key: ValueKey(key), query: query); + } +} + +/// The actual widget doing the heavy lifting +class MessageSearchSidebar extends StatefulWidget { + final MessageSearchQuery query; + + const MessageSearchSidebar({super.key, required this.query}); + + @override + State createState() => _MessageSearchSidebarState(); +} + +class _MessageSearchSidebarState extends State { + final _queryController = TextEditingController(); + final ScrollController _controller = ScrollController(); + + @override + void initState() { + sendLog("initialized"); + final filter = widget.query.filters.peek().firstWhereOrNull((f) => f is ContentFilter); + _queryController.text = filter == null ? "" : (filter as ContentFilter).content; + _controller.addListener(checkForScrollChanges); + super.initState(); + } + + @override + void dispose() { + _queryController.dispose(); + _controller.dispose(); + super.dispose(); + } + + void checkForScrollChanges() { + if (_controller.position.pixels >= _controller.position.maxScrollExtent - 200) { + widget.query.search(increment: true); + } + } + + @override + Widget build(BuildContext context) { + return Container( + color: Get.theme.colorScheme.onInverseSurface, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + top: elementSpacing, + right: defaultSpacing + elementSpacing, + left: defaultSpacing + elementSpacing, + ), + child: FJTextField( + prefixIcon: Icons.search, + hintText: "search".tr, + autofocus: true, + controller: _queryController, + onChange: (query) { + final provider = SidebarController.getCurrentProvider(); + if (provider == null) { + return; + } + + // Add the new filter for the query + batch(() { + widget.query.filters.removeWhere((f) => f is ContentFilter); + widget.query.filters.add(ContentFilter(query)); + }); + + // Restart the search + widget.query.search(); + }, + ), + ), + verticalSpacing(defaultSpacing), + Expanded( + child: Watch((ctx) { + return ListView.builder( + controller: _controller, + itemCount: widget.query.results.length, + itemBuilder: (context, index) { + final message = widget.query.results[index]; + final friend = FriendController.friends[message.senderAddress]; + + // Check if a timestamp should be rendered + bool newHeading = false; + if (index != 0) { + final lastMessage = widget.query.results[index - 1]; + + // Check if the last message was a day before the current one + if (lastMessage.createdAt.day != message.createdAt.day) { + newHeading = true; + } + } + + return Padding( + padding: const EdgeInsets.only( + bottom: defaultSpacing, + right: defaultSpacing + elementSpacing, + left: defaultSpacing + elementSpacing, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (newHeading || index == 0) + Padding( + padding: const EdgeInsets.only(top: defaultSpacing, bottom: defaultSpacing), + child: Text(formatDay(message.createdAt), style: Get.theme.textTheme.labelMedium), + ), + Material( + borderRadius: BorderRadius.circular(defaultSpacing), + color: Get.theme.colorScheme.inverseSurface, + child: InkWell( + borderRadius: BorderRadius.circular(defaultSpacing), + onTap: () => SidebarController.getCurrentProvider()!.scrollToMessage(message.id), + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: MaterialMessageRenderer( + message: message, + provider: null, + senderAddress: message.senderAddress, + sender: friend, + overwritePadding: 0, + ), + ), + ), + ), + if (index == widget.query.results.length - 1) verticalSpacing(elementSpacing), + ], + ), + ); + }, + ); + }), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat/components/conversations/message_search_window.dart b/lib/pages/chat/components/conversations/message_search_window.dart deleted file mode 100644 index 61f9a7f7..00000000 --- a/lib/pages/chat/components/conversations/message_search_window.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/conversation/message_search_controller.dart'; -import 'package:chat_interface/pages/chat/components/message/renderer/material/material_message_renderer.dart'; -import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -class MessageSearchWindow extends StatefulWidget { - const MessageSearchWindow({super.key}); - - @override - State createState() => _MessageSearchWindowState(); -} - -class _MessageSearchWindowState extends State { - final ScrollController _controller = ScrollController(); - final FocusNode _focus = FocusNode(); - - @override - void initState() { - _controller.addListener(checkForScrollChanges); - Get.find().currentFocus = _focus; - super.initState(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void checkForScrollChanges() { - if (_controller.position.pixels >= _controller.position.maxScrollExtent - 200) { - Get.find().search(increment: true); - } - } - - @override - Widget build(BuildContext context) { - final friendController = Get.find(); - final searchController = Get.find(); - - return Container( - color: Get.theme.colorScheme.onInverseSurface, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only( - top: elementSpacing, - right: defaultSpacing + elementSpacing, - left: defaultSpacing + elementSpacing, - ), - child: FJTextField( - focusNode: _focus, - prefixIcon: Icons.search, - hintText: "search".tr, - onChange: (query) { - final controller = Get.find(); - if (controller.currentProvider.value == null) { - return; - } - searchController.filters.value = [ - ConversationFilter(controller.currentProvider.value!.conversation.id.encode()), - ContentFilter(query), - ]; - searchController.search(); - }, - ), - ), - verticalSpacing(defaultSpacing), - Expanded( - child: Obx(() { - return FadingEdgeScrollView.fromScrollView( - gradientFractionOnEnd: 0, - child: ListView.builder( - controller: _controller, - itemCount: searchController.results.length, - itemBuilder: (context, index) { - final message = searchController.results[index]; - final friend = friendController.friends[message.senderAddress]; - - // Check if a timestamp should be rendered - bool newHeading = false; - if (index != 0) { - final lastMessage = searchController.results[index - 1]; - - // Check if the last message was a day before the current one - if (lastMessage.createdAt.day != message.createdAt.day) { - newHeading = true; - } - } - - return Padding( - padding: const EdgeInsets.only( - bottom: defaultSpacing, - right: defaultSpacing + elementSpacing, - left: defaultSpacing + elementSpacing, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (newHeading || index == 0) - Padding( - padding: const EdgeInsets.only(top: defaultSpacing, bottom: defaultSpacing), - child: Text(formatDay(message.createdAt), style: Get.theme.textTheme.labelMedium), - ), - Material( - borderRadius: BorderRadius.circular(defaultSpacing), - color: Get.theme.colorScheme.inverseSurface, - child: InkWell( - borderRadius: BorderRadius.circular(defaultSpacing), - onTap: () => Get.find().currentProvider.value!.scrollToMessage(message.id), - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: MaterialMessageRenderer( - message: message, - provider: null, - senderAddress: message.senderAddress, - sender: friend, - overwritePadding: 0, - ), - ), - ), - ), - if (index == searchController.results.length - 1) verticalSpacing(elementSpacing) - ], - ), - ); - }, - ), - ); - }), - ) - ], - ), - ); - } -} diff --git a/lib/pages/chat/components/conversations/notification_dot.dart b/lib/pages/chat/components/conversations/notification_dot.dart new file mode 100644 index 00000000..602fc8ac --- /dev/null +++ b/lib/pages/chat/components/conversations/notification_dot.dart @@ -0,0 +1,21 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class NotificationDot extends StatelessWidget { + final int amount; + + const NotificationDot({super.key, required this.amount}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration(shape: BoxShape.circle, color: Get.theme.colorScheme.error), + child: Padding( + padding: const EdgeInsets.only(left: 5, right: 5, top: 5, bottom: 5), + child: Center(child: Text(min(amount, 99).toString(), style: Get.textTheme.labelSmall)), + ), + ); + } +} diff --git a/lib/pages/chat/components/conversations/zap_share_window.dart b/lib/pages/chat/components/conversations/zap_share_window.dart index 40a0671f..dff9e15d 100644 --- a/lib/pages/chat/components/conversations/zap_share_window.dart +++ b/lib/pages/chat/components/conversations/zap_share_window.dart @@ -5,29 +5,19 @@ import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class ZapShareWindow extends StatefulWidget { +class ZapShareWindow extends StatelessWidget { final Conversation conversation; final ContextMenuData data; - const ZapShareWindow({ - super.key, - required this.data, - required this.conversation, - }); + const ZapShareWindow({super.key, required this.data, required this.conversation}); - @override - State createState() => _ZapShareWindowState(); -} - -class _ZapShareWindowState extends State { @override Widget build(BuildContext context) { - final controller = Get.find(); - return SlidingWindowBase( title: const [], - position: widget.data, + position: data, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -38,14 +28,19 @@ class _ZapShareWindowState extends State { children: [ Row( children: [ - Obx(() => Text("${controller.step.value} ", style: Get.theme.textTheme.labelLarge)), - Obx(() => Text("(${controller.currentPart.value}/${controller.endPart})", style: Get.theme.textTheme.bodyLarge)), + Watch((ctx) => Text("${ZapShareController.step.value} ", style: Get.theme.textTheme.labelLarge)), + Watch( + (ctx) => Text( + "(${ZapShareController.currentPart.value}/${ZapShareController.endPart})", + style: Get.theme.textTheme.bodyLarge, + ), + ), ], ), verticalSpacing(defaultSpacing), FJElevatedButton( onTap: () { - controller.cancel(); + ZapShareController.cancel(); Get.back(); }, child: Row( @@ -56,14 +51,14 @@ class _ZapShareWindowState extends State { Text("Stop file transfer", style: Get.theme.textTheme.labelLarge), ], ), - ) + ), ], ), const Spacer(), - Obx( - () => CircularProgressIndicator( + Watch( + (ctx) => CircularProgressIndicator( backgroundColor: Get.theme.colorScheme.primary, - value: controller.waiting.value ? null : controller.progress.value, + value: ZapShareController.waiting.value ? null : ZapShareController.progress.value, valueColor: AlwaysStoppedAnimation(Get.theme.colorScheme.onPrimary), ), ), diff --git a/lib/pages/chat/components/emojis/emoji_window.dart b/lib/pages/chat/components/emojis/emoji_window.dart index db33d4d3..c2e3cfeb 100644 --- a/lib/pages/chat/components/emojis/emoji_window.dart +++ b/lib/pages/chat/components/emojis/emoji_window.dart @@ -3,9 +3,10 @@ import 'dart:async'; import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:unicode_emojis/unicode_emojis.dart'; class EmojiWindow extends StatefulWidget { @@ -20,23 +21,19 @@ class EmojiWindow extends StatefulWidget { class _EmojiWindowState extends State { final _scrollController = ScrollController(); final _controller = TextEditingController(); - final _currentSearch = "".obs; - int currentIndex = 0; - final emojis = [].obs; - bool gone = false; - Timer? currentTimer; + late final _emojis = listSignal(UnicodeEmojis.allEmojis.sublist(0, 100)); + Timer? _currentTimer; @override void initState() { super.initState(); - emojis.value = UnicodeEmojis.allEmojis; } @override void dispose() { - gone = true; _scrollController.dispose(); _controller.dispose(); + _emojis.dispose(); super.dispose(); } @@ -47,7 +44,7 @@ class _EmojiWindowState extends State { return SlidingWindowBase( title: const [], position: widget.data, - maxSize: 500, + maxSize: 400, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -58,54 +55,57 @@ class _EmojiWindowState extends State { autofocus: true, hintText: "Search emojis", onChange: (value) { - _currentSearch.value = value; - if (value == "") { - emojis.value = UnicodeEmojis.allEmojis; - } else { - final search = UnicodeEmojis.search(value); - emojis.value = search; - } + _currentTimer?.cancel(); + _currentTimer = Timer(const Duration(milliseconds: 300), () { + if (value == "") { + _emojis.value = UnicodeEmojis.allEmojis.sublist(0, 100); + } else { + final search = UnicodeEmojis.search(value); + _emojis.value = search; + } + }); }, ), ConstrainedBox( - constraints: BoxConstraints(maxHeight: Get.height * 0.5), + constraints: BoxConstraints(maxHeight: 300), child: Padding( padding: const EdgeInsets.only(top: defaultSpacing), child: Material( color: Colors.transparent, - child: Obx( - () => GridView.builder( - key: const ValueKey("the grid"), - controller: _scrollController, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 30 * 1.5, - crossAxisSpacing: elementSpacing, - ), - itemCount: emojis.length, - itemBuilder: (context, index) { - final emoji = emojis[index]; - return RepaintBoundary( - child: Tooltip( - key: ValueKey(emoji.shortName), - waitDuration: 500.ms, - exitDuration: 0.ms, + child: Watch( + (ctx) => FadingEdgeScrollView.fromScrollView( + child: GridView.builder( + primary: false, + key: const Key("emojiScrollView"), + scrollDirection: Axis.vertical, + controller: _scrollController, + padding: EdgeInsets.all(elementSpacing), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 1, + crossAxisCount: 6, + mainAxisSpacing: elementSpacing, + crossAxisSpacing: elementSpacing, + ), + itemCount: _emojis.length, + itemBuilder: (context, index) { + final emoji = _emojis[index]; + return Tooltip( + waitDuration: const Duration(milliseconds: 500), + exitDuration: const Duration(milliseconds: 0), message: ":${emoji.shortName}:", child: Center( child: InkWell( borderRadius: BorderRadius.circular(1000), onTap: () { Get.back(result: emoji.emoji); - currentTimer?.cancel(); + _currentTimer?.cancel(); }, - child: Text( - emoji.emoji, - style: emojiTextStyle, - ), + child: Text(emoji.emoji, style: emojiTextStyle), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/chat/components/library/library_entry_renderer.dart b/lib/pages/chat/components/library/library_entry_renderer.dart index 358fad60..da20b84a 100644 --- a/lib/pages/chat/components/library/library_entry_renderer.dart +++ b/lib/pages/chat/components/library/library_entry_renderer.dart @@ -4,10 +4,7 @@ import 'package:flutter/widgets.dart'; class LibraryEntryRenderer extends StatefulWidget { final LibraryEntryData data; - const LibraryEntryRenderer({ - super.key, - required this.data, - }); + const LibraryEntryRenderer({super.key, required this.data}); @override State createState() => _LibraryEntryRendererState(); diff --git a/lib/pages/chat/components/library/library_favorite_button.dart b/lib/pages/chat/components/library/library_favorite_button.dart index 79403c69..99394784 100644 --- a/lib/pages/chat/components/library/library_favorite_button.dart +++ b/lib/pages/chat/components/library/library_favorite_button.dart @@ -1,10 +1,12 @@ import 'package:chat_interface/controller/conversation/attachment_controller.dart'; import 'package:chat_interface/database/database.dart'; -import 'package:chat_interface/pages/chat/components/library/library_manager.dart'; +import 'package:chat_interface/services/chat/library_manager.dart'; +import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class LibraryFavoriteButton extends StatefulWidget { final AttachmentContainer container; @@ -26,26 +28,28 @@ class LibraryFavoriteButton extends StatefulWidget { State createState() => _LibraryFavoriteButtonState(); } -class _LibraryFavoriteButtonState extends State { - final visible = false.obs; - final bookmarked = false.obs; - LibraryEntry? entry; +class _LibraryFavoriteButtonState extends State with SignalsMixin { + late final _visible = createSignal(false); + late final _bookmarked = createSignal(false); + LibraryEntry? _entry; /// Fetches the bookmark state from the local database Future fetchBookmarkState() async { - if (widget.container.attachmentType == AttachmentContainerType.remoteImage) { - final dbEntry = await (db.libraryEntry.select()..where((tbl) => tbl.data.equals(widget.container.url))).getSingleOrNull(); - bookmarked.value = dbEntry != null; - if (bookmarked.value) { - entry = LibraryEntry.fromData(dbEntry!); - } - } else { - final dbEntry = await (db.libraryEntry.select()..where((tbl) => tbl.data.contains(widget.container.id))).getSingleOrNull(); - bookmarked.value = dbEntry != null; - if (bookmarked.value) { - entry = LibraryEntry.fromData(dbEntry!); + // Find identifier for the entry + final identifier = LibraryEntry.entryIdentifier(widget.container); + + // Check if there is an entry with this identifier + final dbEntry = await (db.libraryEntry.select()..where((tbl) => tbl.identifierHash.equals(identifier))).get(); + if (dbEntry.length > 1) { + sendLog("WARNING: hash collision with identifier of library entry, deleting all entries other than index 0"); + for (var entry in dbEntry.sublist(1)) { + await LibraryManager.removeEntryFromLibrary(await LibraryEntry.fromData(entry)); } } + _bookmarked.value = dbEntry.isNotEmpty; + if (_bookmarked.value) { + _entry = await LibraryEntry.fromData(dbEntry[0]); + } // Just so you can await this function return true; @@ -56,11 +60,11 @@ class _LibraryFavoriteButtonState extends State { return MouseRegion( onEnter: (e) async { await fetchBookmarkState(); - visible.value = true; + _visible.value = true; widget.onEnter?.call(); }, onExit: (e) { - visible.value = false; + _visible.value = false; widget.onExit?.call(); }, child: Stack( @@ -69,9 +73,9 @@ class _LibraryFavoriteButtonState extends State { Positioned( top: elementSpacing, right: elementSpacing, - child: Obx( - () => Visibility( - visible: visible.value, + child: Watch( + (ctx) => Visibility( + visible: _visible.value, child: Material( color: Get.theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(elementSpacing), @@ -82,24 +86,24 @@ class _LibraryFavoriteButtonState extends State { splashColor: Colors.transparent, overlayColor: const WidgetStatePropertyAll(Colors.transparent), onTap: () async { - if (bookmarked.value) { - final success = await LibraryManager.removeEntryFromLibrary(entry!); + if (_bookmarked.value) { + final success = await LibraryManager.removeEntryFromLibrary(_entry!); if (success) { - bookmarked.value = false; + _bookmarked.value = false; } } else { final success = await LibraryManager.addContainerToLibrary(widget.container); if (success) { - bookmarked.value = true; + _bookmarked.value = true; } } }, child: Padding( padding: const EdgeInsets.all(elementSpacing), - child: Obx( - () => Icon( - bookmarked.value ? Icons.bookmark : Icons.bookmark_outline, - color: bookmarked.value ? Get.theme.colorScheme.onPrimary : Get.theme.colorScheme.onSurface, + child: Watch( + (ctx) => Icon( + _bookmarked.value ? Icons.bookmark : Icons.bookmark_outline, + color: _bookmarked.value ? Get.theme.colorScheme.onPrimary : Get.theme.colorScheme.onSurface, ), ), ), @@ -107,7 +111,7 @@ class _LibraryFavoriteButtonState extends State { ), ), ), - ) + ), ], ), ); diff --git a/lib/pages/chat/components/library/library_tab.dart b/lib/pages/chat/components/library/library_tab.dart index 594e77da..fb3234f0 100644 --- a/lib/pages/chat/components/library/library_tab.dart +++ b/lib/pages/chat/components/library/library_tab.dart @@ -1,85 +1,83 @@ import 'dart:async'; -import 'dart:convert'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; -import 'package:chat_interface/database/database_entities.dart'; +import 'package:chat_interface/database/database_entities.dart' as model; import 'package:chat_interface/database/database.dart'; import 'package:chat_interface/pages/chat/components/library/library_favorite_button.dart'; import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/chat/library_manager.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:liphium_bridge/liphium_bridge.dart'; +import 'package:signals/signals_flutter.dart'; class LibraryTab extends StatefulWidget { - final LibraryEntryType? filter; + final model.LibraryEntryType? filter; final MessageProvider provider; - const LibraryTab({ - super.key, - this.filter, - required this.provider, - }); + const LibraryTab({super.key, this.filter, required this.provider}); @override State createState() => _LibraryTabState(); } class _LibraryTabState extends State { - LibraryEntryType? _lastFilter; - final _containerList = [].obs; - BigInt lastDate = BigInt.from(0); - final _show = false.obs; + model.LibraryEntryType? _lastFilter; + final _entryList = listSignal([]); + BigInt _lastDate = BigInt.from(0); + final _show = signal(false); + + @override + void dispose() { + _show.dispose(); + _entryList.dispose(); + super.dispose(); + } Future loadMoreItems() async { // Make sure to start from the top again when a new filter is set if (widget.filter != _lastFilter) { - lastDate = BigInt.from(0); + _lastDate = BigInt.from(0); } // Get all the library entries that match the current filter List entries; if (widget.filter != null) { - entries = await (db.libraryEntry.select() - ..orderBy([(tbl) => OrderingTerm.asc(tbl.createdAt)]) - ..where((tbl) => tbl.createdAt.isBiggerThan(Variable(lastDate))) - ..where((tbl) => tbl.type.equals(widget.filter!.index)) - ..limit(30)) - .get(); + entries = + await (db.libraryEntry.select() + ..orderBy([(tbl) => OrderingTerm.asc(tbl.createdAt)]) + ..where((tbl) => tbl.createdAt.isBiggerThan(Variable(_lastDate))) + ..where((tbl) => tbl.type.equals(widget.filter!.index)) + ..limit(30)) + .get(); } else { - entries = await (db.libraryEntry.select() - ..orderBy([(tbl) => OrderingTerm.asc(tbl.createdAt)]) - ..where((tbl) => tbl.createdAt.isBiggerThan(Variable(lastDate))) - ..limit(30)) - .get(); + entries = + await (db.libraryEntry.select() + ..orderBy([(tbl) => OrderingTerm.asc(tbl.createdAt)]) + ..where((tbl) => tbl.createdAt.isBiggerThan(Variable(_lastDate))) + ..limit(30)) + .get(); } if (entries.isNotEmpty) { - lastDate = entries.last.createdAt; + _lastDate = entries.last.createdAt; } // Get all the attachment containers from the library entries for displaying them - final controller = Get.find(); - final newContainerList = []; - for (var entry in entries) { - if (entry.data.isURL) { - newContainerList.add(AttachmentContainer.remoteImage(entry.data)); - } else { - final json = jsonDecode(entry.data); - final type = await AttachmentController.getStorageTypeFor(json["i"]); - if (type == null) { - continue; - } - newContainerList.add(controller.fromJson(type, json)); - } + final newEntryList = []; + for (var dbEntry in entries) { + final entry = await LibraryEntry.fromData(dbEntry); + await entry.initForUI(); + newEntryList.add(entry); } // Add the containers to the list of entries if (_lastFilter != widget.filter) { - _containerList.value = newContainerList; + _entryList.value = newEntryList; } else { - _containerList.addAll(newContainerList); + _entryList.addAll(newEntryList); } // Set what's nessecary for the next iteration @@ -91,51 +89,42 @@ class _LibraryTabState extends State { Widget build(BuildContext context) { unawaited(loadMoreItems()); - return Obx(() { + return Watch((ctx) { if (!_show.value) { return const SizedBox(); } - if (_containerList.isEmpty) { - return InfoContainer( - message: "library.empty".tr, - expand: true, - ); + if (_entryList.isEmpty) { + return InfoContainer(message: "library.empty".tr, expand: true); } return GridView.builder( shrinkWrap: true, - itemCount: _containerList.length, + itemCount: _entryList.length, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200, mainAxisSpacing: defaultSpacing, crossAxisSpacing: defaultSpacing, ), itemBuilder: (context, index) { - final container = _containerList[index]; - container.downloaded.value = true; + final entry = _entryList[index]; + entry.container!.downloaded.value = true; // Render attachment container Widget image; - if (container.attachmentType == AttachmentContainerType.remoteImage) { - image = Image.network( - container.url, - fit: BoxFit.cover, - ); + if (entry.container!.attachmentType == AttachmentContainerType.remoteImage) { + image = Image.network(entry.container!.url, fit: BoxFit.cover); } else { - image = XImage( - file: container.file!, - fit: BoxFit.cover, - ); + image = XImage(file: entry.container!.file!, fit: BoxFit.cover); } return Material( - key: ValueKey(container.id), + key: ValueKey(entry.container!.id), borderRadius: BorderRadius.circular(defaultSpacing), child: InkWell( borderRadius: BorderRadius.circular(defaultSpacing), onTap: () { //* Send message with the library element - widget.provider.sendMessage(false.obs, MessageType.text, [container.toAttachment()], "", ""); + widget.provider.sendMessage(signal(false), MessageType.text, [entry.container!.toAttachment()], "", ""); Get.back(); }, child: ClipRRect( @@ -143,8 +132,8 @@ class _LibraryTabState extends State { child: LayoutBuilder( builder: (context, constraints) { return LibraryFavoriteButton( - callback: () => _containerList.removeAt(index), - container: container, + callback: () => _entryList.removeAt(index), + container: entry.container!, child: SizedBox( width: constraints.biggest.width, height: constraints.biggest.height, diff --git a/lib/pages/chat/components/library/library_window.dart b/lib/pages/chat/components/library/library_window.dart index ee7722fa..972408b8 100644 --- a/lib/pages/chat/components/library/library_window.dart +++ b/lib/pages/chat/components/library/library_window.dart @@ -6,23 +6,20 @@ import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class LibraryWindow extends StatefulWidget { final ContextMenuData data; final MessageProvider provider; - const LibraryWindow({ - super.key, - required this.data, - required this.provider, - }); + const LibraryWindow({super.key, required this.data, required this.provider}); @override State createState() => _LibraryWindowState(); } class _LibraryWindowState extends State { - final _selected = "library.all".tr.obs; + final _selected = signal("library.all".tr); var _tabs = {}; @@ -46,6 +43,12 @@ class _LibraryWindowState extends State { super.initState(); } + @override + void dispose() { + _selected.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return SlidingWindowBase( @@ -58,11 +61,7 @@ class _LibraryWindowState extends State { children: [ //* Tabs LPHTabElement( - tabs: [ - "library.all".tr, - "library.images".tr, - "library.gifs".tr, - ], + tabs: ["library.all".tr, "library.images".tr, "library.gifs".tr], onTabSwitch: (newTab) { _selected.value = newTab; }, @@ -73,7 +72,7 @@ class _LibraryWindowState extends State { child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only(top: defaultSpacing), - child: Obx(() { + child: Watch((ctx) { return _tabs[_selected.value]!; }), ), diff --git a/lib/pages/chat/components/message/message_feed.dart b/lib/pages/chat/components/message/message_feed.dart index 97f8a615..e5741ef5 100644 --- a/lib/pages/chat/components/message/message_feed.dart +++ b/lib/pages/chat/components/message/message_feed.dart @@ -1,23 +1,20 @@ import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/pages/chat/components/conversations/conversation_members.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; import 'package:chat_interface/pages/chat/components/message/message_list.dart'; import 'package:chat_interface/pages/settings/appearance/chat_settings.dart'; import 'package:chat_interface/pages/settings/data/settings_controller.dart'; import 'package:chat_interface/pages/chat/messages/message_input.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class MessageFeed extends StatefulWidget { final double? overwritePadding; final bool rectInput; - const MessageFeed({ - super.key, - this.overwritePadding, - this.rectInput = false, - }); + const MessageFeed({super.key, this.overwritePadding, this.rectInput = false}); @override State createState() => _MessageFeedState(); @@ -25,12 +22,6 @@ class MessageFeed extends StatefulWidget { class _MessageFeedState extends State { final TextEditingController _message = TextEditingController(); - final loading = false.obs; - - @override - void initState() { - super.initState(); - } @override void dispose() { @@ -40,27 +31,18 @@ class _MessageFeedState extends State { @override Widget build(BuildContext context) { - MessageController controller = Get.find(); - SettingController settingController = Get.find(); - - return Obx(() { - if (controller.currentProvider.value!.conversation.error.value != null) { + return Watch((ctx) { + final provider = (SidebarController.currentOpenTab.value as ConversationSidebarTab).provider; + if (provider.conversation.error.value != null) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - "conversation.error".tr, - style: Get.textTheme.titleMedium, - ), + Text("conversation.error".tr, style: Get.textTheme.titleMedium), verticalSpacing(defaultSpacing), - Text( - controller.currentProvider.value!.conversation.error.value!, - style: Get.textTheme.bodyMedium, - textAlign: TextAlign.center, - ), + Text(provider.conversation.error.value!, style: Get.textTheme.bodyMedium, textAlign: TextAlign.center), ], ), ), @@ -74,44 +56,39 @@ class _MessageFeedState extends State { alignment: Alignment.bottomCenter, child: Column( children: [ - //* Message list + // Message list Expanded( child: Stack( children: [ - //* Messages + // Messages Center( child: ConstrainedBox( constraints: BoxConstraints( - maxWidth: (ChatSettings.chatThemeSetting.value.value ?? 1) == 0 ? double.infinity : 1200, + maxWidth: + (SettingController.settings[ChatSettings.chatTheme]!.value.value ?? 1) == 0 + ? double.infinity + : 1200, ), - child: Obx( - () { - if (!controller.loaded.value) { - return const SizedBox(); - } + child: Watch((ctx) { + if (!MessageController.loaded.value) { + return const SizedBox(); + } - return MessageList( - key: ValueKey(controller.currentProvider.value!.conversation.id), - provider: controller.currentProvider.value!, - overwritePadding: isMobileMode() ? defaultSpacing : sectionSpacing, - ); - }, - ), + return MessageList( + key: ValueKey(provider.getKey("ml")), + provider: provider, + overwritePadding: isMobileMode() ? defaultSpacing : sectionSpacing, + ); + }), ), ), - //* Animated loading indicator + // Animated loading indicator Align( alignment: Alignment.topCenter, - child: Obx( - () => Animate( - effects: [ - FadeEffect( - curve: Curves.ease, - duration: 250.ms, - ), - ], - target: controller.currentProvider.value!.newMessagesLoading.value ? 1 : 0, + child: Watch(key: ValueKey(provider.getKey("loading")), (ctx) { + return Visibility( + visible: provider.newMessagesLoading.value, child: Padding( padding: const EdgeInsets.all(defaultSpacing), child: Material( @@ -138,8 +115,8 @@ class _MessageFeedState extends State { ), ), ), - ), - ), + ); + }), ), ], ), @@ -147,31 +124,19 @@ class _MessageFeedState extends State { //* Message input SelectionContainer.disabled( - child: controller.currentProvider.value!.conversation.borked - ? const SizedBox.shrink() - : MessageInput( - rectangle: widget.rectInput, - draft: controller.currentProvider.value!.conversation.id.encode(), - provider: controller.currentProvider.value!, - ), - ) + child: + provider.conversation.borked + ? const SizedBox.shrink() + : MessageInput( + rectangle: widget.rectInput, + draft: provider.conversation.id.encode(), + provider: provider, + ), + ), ], ), ), ), - Obx(() { - final visible = settingController.settings[AppSettings.showGroupMembers]!.value.value; - return Visibility( - visible: controller.currentProvider.value!.conversation.isGroup && visible, - child: Container( - color: Get.theme.colorScheme.onInverseSurface, - width: 300, - child: ConversationMembers( - conversation: controller.currentProvider.value!.conversation, - ), - ), - ); - }) ], ); }); diff --git a/lib/pages/chat/components/message/message_list.dart b/lib/pages/chat/components/message/message_list.dart index f47ccfd1..32ae396c 100644 --- a/lib/pages/chat/components/message/message_list.dart +++ b/lib/pages/chat/components/message/message_list.dart @@ -1,21 +1,18 @@ import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/pages/chat/components/message/renderer/bubbles/bubbles_renderer.dart'; +import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:lorien_chat_list/lorien_chat_list.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:signals/signals_flutter.dart'; class MessageList extends StatefulWidget { final MessageProvider provider; final double? overwritePadding; final double heightMultiplier; - const MessageList({ - super.key, - required this.provider, - this.overwritePadding, - this.heightMultiplier = 1, - }); + const MessageList({super.key, required this.provider, this.overwritePadding, this.heightMultiplier = 1}); @override State createState() => _MessageListState(); @@ -26,7 +23,7 @@ class _MessageListState extends State { @override void initState() { - widget.provider.newScrollController(_scrollController); + widget.provider.newControllers(_scrollController); super.initState(); } @@ -36,36 +33,48 @@ class _MessageListState extends State { widget.provider.checkCurrentScrollHeight(); }); - return LayoutBuilder( - builder: (context, constraints) { - return Obx(() { - return ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: widget.overwritePadding ?? (constraints.maxWidth <= 800 ? defaultSpacing : sectionSpacing), - ), - child: ListView.builder( - itemCount: widget.provider.messages.length + 2, - reverse: true, - controller: _scrollController, - addAutomaticKeepAlives: false, - addRepaintBoundaries: false, - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - itemBuilder: (context, index) { + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: widget.overwritePadding ?? (constraints.maxWidth <= 800 ? defaultSpacing : sectionSpacing), + ), + child: ChatList( + scrollController: _scrollController, + controller: widget.provider.listController, + loadingMoreWidget: SizedBox(), + onLoadMoreCallback: () async { + var (topReached, error) = await widget.provider.loadNewMessagesTop(); + sendLog("new top $topReached $error"); + if (!error) { + return !topReached; + } + return true; + }, + scrollPhysics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + itemBuilder: (messageId, itemDetails) { + return Watch((ctx) { + final message = widget.provider.messages[messageId]; + if (message == null) { + return SizedBox(); + } + return BubblesRenderer( - index: index, + message: message, + properties: itemDetails, controller: _scrollController, provider: widget.provider, mobileLayout: constraints.maxWidth <= 800, heightMultiplier: widget.heightMultiplier, ); - }, - ), + }); + }, ), ); - }); - }, + }, + ), ); } } diff --git a/lib/pages/chat/components/message/renderer/attachment_renderer.dart b/lib/pages/chat/components/message/renderer/attachment_renderer.dart index 1d673495..42de24f4 100644 --- a/lib/pages/chat/components/message/renderer/attachment_renderer.dart +++ b/lib/pages/chat/components/message/renderer/attachment_renderer.dart @@ -1,5 +1,4 @@ import 'package:chat_interface/controller/conversation/attachment_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/database/trusted_links.dart'; import 'package:chat_interface/pages/chat/components/library/library_favorite_button.dart'; @@ -7,17 +6,18 @@ import 'package:chat_interface/pages/chat/components/message/renderer/audio_atta import 'package:chat_interface/pages/chat/components/message/renderer/bubbles/bubbles_zap_renderer.dart'; import 'package:chat_interface/pages/settings/town/file_settings.dart'; import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; import 'package:chat_interface/theme/components/file_renderer.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/theme/ui/dialogs/image_preview_window.dart'; import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; -import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:open_file/open_file.dart'; import 'package:path/path.dart' as path; +import 'package:signals/signals_flutter.dart'; class AttachmentRenderer extends StatefulWidget { final Message? message; @@ -25,13 +25,7 @@ class AttachmentRenderer extends StatefulWidget { final ConversationMessageProvider? provider; final AttachmentContainer container; - const AttachmentRenderer({ - super.key, - required this.container, - required this.self, - this.message, - this.provider, - }); + const AttachmentRenderer({super.key, required this.container, required this.self, this.message, this.provider}); @override State createState() => _AttachmentRendererState(); @@ -39,50 +33,11 @@ class AttachmentRenderer extends StatefulWidget { class _AttachmentRendererState extends State { Image? _networkImage; - final GlobalKey _heightKey = GlobalKey(); - final loading = true.obs; @override void initState() { super.initState(); - if (widget.container.attachmentType == AttachmentContainerType.remoteImage && - widget.message != null && - (widget.message?.heightCallback ?? false)) { - _networkImage = Image.network( - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null || loadingProgress.expectedTotalBytes == null) { - return const SizedBox( - width: 60, - height: 60, - child: CircularProgressIndicator(), - ); - } - return SizedBox( - width: 60, - height: 60, - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes! / loadingProgress.cumulativeBytesLoaded, - ), - ); - }, - widget.container.url, - fit: BoxFit.cover, - ); - final stream = _networkImage!.image.resolve(const ImageConfiguration()); - final listener = ImageStreamListener((image, synchronousCall) { - if (!loading.value) { - return; - } - loading.value = false; - sendLog("current height ${widget.message!.currentHeight}"); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - sendLog("NEW HEIGHT ${widget.message!.heightKey!.currentContext!.size!.height}"); - final currentHeight = widget.message!.heightKey!.currentContext!.size!.height; - widget.provider!.messageHeightChange(widget.message!, currentHeight - widget.message!.currentHeight!); - }); - }); - stream.addListener(listener); - } else if (widget.container.attachmentType == AttachmentContainerType.remoteImage) { + if (widget.container.attachmentType == AttachmentContainerType.remoteImage) { _networkImage = Image.network( loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null || loadingProgress.expectedTotalBytes == null) { @@ -103,23 +58,18 @@ class _AttachmentRendererState extends State { widget.container.url, fit: BoxFit.cover, ); - loading.value = false; } } @override Widget build(BuildContext context) { if (widget.container.attachmentType == AttachmentContainerType.link) { - return Row( - children: [ - ErrorContainer(message: "under_dev".tr), - ], - ); + return Row(children: [ErrorContainer(message: "under_dev".tr)]); } //* Remote images if (widget.container.attachmentType == AttachmentContainerType.remoteImage) { - return Obx(() { + return Watch((ctx) { if (widget.container.unsafeLocation.value) { final domain = TrustedLinkHelper.extractDomain(widget.container.url); @@ -139,19 +89,19 @@ class _AttachmentRendererState extends State { size: Get.theme.textTheme.bodyMedium!.fontSize! * 1.5, ), horizontalSpacing(elementSpacing), - Flexible( - child: Text("file.unsafe".trParams({"domain": domain})), - ), + Flexible(child: Text("file.unsafe".trParams({"domain": domain}))), horizontalSpacing(elementSpacing), LoadingIconButton( iconSize: 22, extra: 4, padding: 4, onTap: () async { - final result = await showConfirmPopup(ConfirmWindow( - title: "file.images.trust.title".tr, - text: "file.images.trust.description".trParams({"domain": domain}), - )); + final result = await showConfirmPopup( + ConfirmWindow( + title: "file.images.trust.title".tr, + text: "file.images.trust.description".trParams({"domain": domain}), + ), + ); if (result) { await TrustedLinkHelper.addToTrustedLinks(domain); @@ -169,23 +119,14 @@ class _AttachmentRendererState extends State { return Row( mainAxisSize: MainAxisSize.min, children: [ - Align( - key: _heightKey, - heightFactor: loading.value ? 0 : 1, - child: LibraryFavoriteButton( - container: widget.container, - child: InkWell( - onTap: () => Get.dialog(ImagePreviewWindow(url: widget.container.url)), + LibraryFavoriteButton( + container: widget.container, + child: InkWell( + onTap: () => Get.dialog(ImagePreviewWindow(url: widget.container.url)), + borderRadius: BorderRadius.circular(defaultSpacing), + child: ClipRRect( borderRadius: BorderRadius.circular(defaultSpacing), - child: ClipRRect( - borderRadius: BorderRadius.circular(defaultSpacing), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 350, - ), - child: _networkImage, - ), - ), + child: ConstrainedBox(constraints: const BoxConstraints(maxHeight: 350), child: _networkImage), ), ), ), @@ -228,8 +169,8 @@ class _AttachmentRendererState extends State { ), ), Flexible( - child: Obx( - () => Text( + child: Watch( + (ctx) => Text( !widget.container.error.value ? formatFileSize(widget.container.size) : 'file.not_uploaded'.tr, style: Get.theme.textTheme.bodyMedium, ), @@ -241,7 +182,7 @@ class _AttachmentRendererState extends State { horizontalSpacing(defaultSpacing), //* Button - Obx(() { + Watch((ctx) { if (widget.container.downloading.value) { return SizedBox( width: 30, @@ -256,7 +197,7 @@ class _AttachmentRendererState extends State { if (widget.container.error.value) { return IconButton( onPressed: () { - Get.find().downloadAttachment(widget.container, retry: true); + AttachmentController.downloadAttachment(widget.container, retry: true); }, icon: const Icon(Icons.refresh), ); @@ -276,7 +217,7 @@ class _AttachmentRendererState extends State { return IconButton( onPressed: () { - Get.find().downloadAttachment(widget.container); + AttachmentController.downloadAttachment(widget.container); }, icon: const Icon(Icons.download), ); diff --git a/lib/pages/chat/components/message/renderer/audio_attachment_player.dart b/lib/pages/chat/components/message/renderer/audio_attachment_player.dart index f350b4fb..b347273f 100644 --- a/lib/pages/chat/components/message/renderer/audio_attachment_player.dart +++ b/lib/pages/chat/components/message/renderer/audio_attachment_player.dart @@ -7,6 +7,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:signals/signals_flutter.dart'; class AudioAttachmentPlayer extends StatefulWidget { final AttachmentContainer container; @@ -17,13 +18,13 @@ class AudioAttachmentPlayer extends StatefulWidget { State createState() => _AudioAttachmentPlayerState(); } -class _AudioAttachmentPlayerState extends State { - final player = AudioPlayer(); - final playing = false.obs; - final currentMax = Rx(null); - final currentDuration = Rx(null); - bool paused = false; - final hoverPosition = Rx(null); +class _AudioAttachmentPlayerState extends State with SignalsMixin { + final _player = AudioPlayer(); + late final _playing = createSignal(false); + late final _currentMax = createSignal(null); + late final _currentDuration = createSignal(null); + bool _paused = false; + late final _hoverPosition = createSignal(null); @override void initState() { @@ -32,30 +33,24 @@ class _AudioAttachmentPlayerState extends State { } void _init() { - player.positionStream.listen( - (duration) { - currentDuration.value = duration; - }, - ); - player.durationStream.listen( - (max) { - currentMax.value = max; - }, - ); + _player.positionStream.listen((duration) { + _currentDuration.value = duration; + }); + _player.durationStream.listen((max) { + _currentMax.value = max; + }); } @override void dispose() { super.dispose(); - player.dispose(); + _player.dispose(); } @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 350, - ), + constraints: BoxConstraints(maxWidth: 350), child: Container( padding: const EdgeInsets.all(defaultSpacing), decoration: BoxDecoration( @@ -71,26 +66,17 @@ class _AudioAttachmentPlayerState extends State { Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.library_music, - size: sectionSpacing * 2, - color: Get.theme.colorScheme.onPrimary, - ), + Icon(Icons.library_music, size: sectionSpacing * 2, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(defaultSpacing), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + Flexible(child: Text(widget.container.name, style: Get.theme.textTheme.labelMedium)), Flexible( - child: Text( - widget.container.name, - style: Get.theme.textTheme.labelMedium, - ), - ), - Flexible( - child: Obx( - () => Text( + child: Watch( + (ctx) => Text( !widget.container.error.value ? formatFileSize(1000) : 'file.not_uploaded'.tr, style: Get.theme.textTheme.bodyMedium, ), @@ -104,7 +90,7 @@ class _AudioAttachmentPlayerState extends State { ), //* Button - Obx(() { + Watch((ctx) { if (widget.container.downloading.value) { return SizedBox( width: 30, @@ -119,7 +105,7 @@ class _AudioAttachmentPlayerState extends State { if (widget.container.error.value) { return IconButton( onPressed: () { - Get.find().downloadAttachment(widget.container, retry: true); + AttachmentController.downloadAttachment(widget.container, retry: true); }, icon: const Icon(Icons.refresh), ); @@ -128,34 +114,33 @@ class _AudioAttachmentPlayerState extends State { if (!widget.container.downloaded.value) { return IconButton( onPressed: () { - Get.find().downloadAttachment(widget.container); + AttachmentController.downloadAttachment(widget.container); }, icon: const Icon(Icons.download), ); } return LoadingIconButton( - loading: false.obs, onTap: () async { - if (playing.value) { - await player.pause(); - playing.value = false; - paused = true; + if (_playing.value) { + await _player.pause(); + _playing.value = false; + _paused = true; } else { - if (paused) { - unawaited(player.play()); - paused = false; - playing.value = true; + if (_paused) { + unawaited(_player.play()); + _paused = false; + _playing.value = true; return; } - await player.setFilePath(widget.container.file!.path); - await player.setVolume(0.2); - unawaited(player.play()); - playing.value = true; + await _player.setFilePath(widget.container.file!.path); + await _player.setVolume(0.2); + unawaited(_player.play()); + _playing.value = true; } }, - icon: playing.value ? Icons.pause : Icons.play_arrow, + icon: _playing.value ? Icons.pause : Icons.play_arrow, background: true, color: Get.theme.colorScheme.onPrimary, backgroundColor: Get.theme.colorScheme.primary, @@ -164,9 +149,9 @@ class _AudioAttachmentPlayerState extends State { ], ), verticalSpacing(defaultSpacing), - LayoutBuilder(builder: (context, constraints) { - return Obx(() { - if (currentDuration.value == null || currentMax.value == null) { + LayoutBuilder( + builder: (context, constraints) { + if (_currentDuration.value == null || _currentMax.value == null) { return Container( height: 10, decoration: BoxDecoration( @@ -180,13 +165,13 @@ class _AudioAttachmentPlayerState extends State { return MouseRegion( cursor: SystemMouseCursors.click, onHover: (event) { - hoverPosition.value = event.localPosition.dx; + _hoverPosition.value = event.localPosition.dx; }, - onExit: (event) => hoverPosition.value = null, + onExit: (event) => _hoverPosition.value = null, child: GestureDetector( onTap: () { - final percentage = (hoverPosition.value! / constraints.maxWidth); - player.seek(Duration(milliseconds: (currentMax.value!.inMilliseconds * percentage).toInt())); + final percentage = (_hoverPosition.value! / constraints.maxWidth); + _player.seek(Duration(milliseconds: (_currentMax.value!.inMilliseconds * percentage).toInt())); }, child: Stack( children: [ @@ -197,14 +182,15 @@ class _AudioAttachmentPlayerState extends State { color: Get.theme.colorScheme.primary, ), ), - Obx( - () => AnimatedContainer( + Watch( + (ctx) => AnimatedContainer( duration: Duration(milliseconds: 100), height: 10, - width: constraints.maxWidth * - (hoverPosition.value == null - ? (currentDuration.value!.inMilliseconds / currentMax.value!.inMilliseconds) - : (hoverPosition.value! / constraints.maxWidth)) + width: + constraints.maxWidth * + (_hoverPosition.value == null + ? (_currentDuration.value!.inMilliseconds / _currentMax.value!.inMilliseconds) + : (_hoverPosition.value! / constraints.maxWidth)) .clamp(0, 1), decoration: BoxDecoration( borderRadius: BorderRadius.circular(defaultSpacing), @@ -216,8 +202,8 @@ class _AudioAttachmentPlayerState extends State { ), ), ); - }); - }) + }, + ), ], ), ), diff --git a/lib/pages/chat/components/message/renderer/bubbles/bubbles_message_renderer.dart b/lib/pages/chat/components/message/renderer/bubbles/bubbles_message_renderer.dart index c8034110..e1f3db86 100644 --- a/lib/pages/chat/components/message/renderer/bubbles/bubbles_message_renderer.dart +++ b/lib/pages/chat/components/message/renderer/bubbles/bubbles_message_renderer.dart @@ -1,4 +1,4 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/chat/components/message/renderer/attachment_renderer.dart'; @@ -12,6 +12,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class BubblesMessageRenderer extends StatefulWidget { final LPHAddress senderAddress; @@ -59,26 +60,26 @@ class _BubblesMessageRendererState extends State { final menuData = ContextMenuData.fromPosition(Offset(_mouseX, _mouseY)); // Open the context menu - Get.dialog(MessageOptionsWindow( - data: menuData, - self: widget.message.senderAddress == StatusController.ownAddress, - message: widget.message, - provider: widget.provider, - )); + Get.dialog( + MessageOptionsWindow( + data: menuData, + self: widget.message.senderAddress == StatusController.ownAddress, + message: widget.message, + provider: widget.provider, + ), + ); }, child: Padding( - padding: EdgeInsets.symmetric( - vertical: elementSpacing, - ), + padding: EdgeInsets.symmetric(vertical: elementSpacing), child: Row( textDirection: widget.self ? TextDirection.rtl : TextDirection.ltr, crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - //* Avatar + // Avatar with a tooltip to show their name Visibility( - visible: !widget.last, - replacement: const SizedBox(width: 34), //* Show timestamp instead + visible: widget.last, + replacement: const SizedBox(width: 34), child: Tooltip( message: sender.displayName.value, child: InkWell( @@ -97,9 +98,7 @@ class _BubblesMessageRendererState extends State { textDirection: widget.self ? TextDirection.rtl : TextDirection.ltr, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible( - child: renderMessageContent(), - ), + Flexible(child: renderMessageContent()), //* Desktop timestamp horizontalSpacing(defaultSpacing), @@ -107,14 +106,17 @@ class _BubblesMessageRendererState extends State { Padding( padding: const EdgeInsets.only(top: defaultSpacing), child: SelectionContainer.disabled( - child: Text(formatMessageTime(widget.message.createdAt), style: Get.theme.textTheme.bodySmall), + child: Text( + formatMessageTime(widget.message.createdAt), + style: Get.theme.textTheme.bodySmall, + ), ), ), //* Desktop verified indicator horizontalSpacing(defaultSpacing), - Obx(() { + Watch((ctx) { final verified = widget.message.verified.value; return Visibility( visible: !verified, @@ -122,17 +124,14 @@ class _BubblesMessageRendererState extends State { padding: const EdgeInsets.only(top: elementSpacing + elementSpacing / 4), child: Tooltip( message: "chat.not.signed".tr, - child: const Icon( - Icons.warning_rounded, - color: Colors.amber, - ), + child: const Icon(Icons.warning_rounded, color: Colors.amber), ), ), ); - }) + }), ], ), - ) + ), ], ), ), @@ -142,41 +141,40 @@ class _BubblesMessageRendererState extends State { } Widget renderMessageContent() { - return LayoutBuilder(builder: (context, constraints) { - return ConstrainedBox( - constraints: BoxConstraints(maxWidth: widget.mobileLayout ? Get.width * 0.75 : (Get.width - 350) * 0.5), - child: Column( - crossAxisAlignment: widget.self ? CrossAxisAlignment.end : CrossAxisAlignment.start, - children: [ - //* Message content (text) - Visibility( - visible: widget.message.content.isNotEmpty, - child: Container( - padding: const EdgeInsets.symmetric(vertical: defaultSpacing * 0.5, horizontal: defaultSpacing), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(defaultSpacing), - color: widget.self ? Get.theme.colorScheme.primary : Get.theme.colorScheme.primaryContainer, - ), - child: Column( - crossAxisAlignment: widget.self ? CrossAxisAlignment.end : CrossAxisAlignment.start, - children: [ - renderReplyMessage(), + return LayoutBuilder( + builder: (context, constraints) { + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: widget.mobileLayout ? Get.width * 0.75 : (Get.width - 350) * 0.5), + child: Column( + crossAxisAlignment: widget.self ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + //* Message content (text) + Visibility( + visible: widget.message.content.isNotEmpty, + child: Container( + padding: const EdgeInsets.symmetric(vertical: defaultSpacing * 0.5, horizontal: defaultSpacing), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(defaultSpacing), + color: widget.self ? Get.theme.colorScheme.primary : Get.theme.colorScheme.primaryContainer, + ), + child: Column( + crossAxisAlignment: widget.self ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + renderReplyMessage(), - //* Actual message (with formatted renderer) - FormattedText( - text: widget.message.content, - baseStyle: Get.theme.textTheme.labelLarge!, - ), - ], + //* Actual message (with formatted renderer) + FormattedText(text: widget.message.content, baseStyle: Get.theme.textTheme.labelLarge!), + ], + ), ), ), - ), - renderAttachments(), - ], - ), - ); - }); + renderAttachments(), + ], + ), + ); + }, + ); } Widget renderReplyMessage() { @@ -187,7 +185,7 @@ class _BubblesMessageRendererState extends State { padding: const EdgeInsets.only(top: elementSpacing, bottom: elementSpacing), child: Material( borderRadius: BorderRadius.circular(defaultSpacing), - color: widget.self ? Get.theme.colorScheme.onPrimary.withOpacity(0.2) : Get.theme.colorScheme.inverseSurface, + color: widget.self ? Get.theme.colorScheme.onPrimary.withAlpha(50) : Get.theme.colorScheme.inverseSurface, child: InkWell( borderRadius: BorderRadius.circular(defaultSpacing), onTap: () => widget.provider.scrollToMessage(widget.message.answer), @@ -210,7 +208,10 @@ class _BubblesMessageRendererState extends State { Flexible( child: Text( AnswerData.answerContent( - widget.message.answerMessage!.type, widget.message.answerMessage!.content, widget.message.answerMessage!.attachments), + widget.message.answerMessage!.type, + widget.message.answerMessage!.content, + widget.message.answerMessage!.attachments, + ), style: Get.theme.textTheme.labelMedium, overflow: TextOverflow.ellipsis, maxLines: 1, @@ -234,24 +235,22 @@ class _BubblesMessageRendererState extends State { padding: EdgeInsets.only(top: widget.message.content.isEmpty ? 0 : elementSpacing), child: Column( mainAxisSize: MainAxisSize.min, - children: List.generate(widget.message.attachmentsRenderer.length, (index) { - final container = widget.message.attachmentsRenderer[index]; + children: + List.generate(widget.message.attachmentsRenderer.length, (index) { + final container = widget.message.attachmentsRenderer[index]; - if (container.width != null && container.height != null) { - return Padding( - padding: EdgeInsets.only(top: widget.message.content.isEmpty && index == 0 ? 0 : elementSpacing), - child: ImageAttachmentRenderer( - image: container, - hoverCheck: true, - ), - ); - } + if (container.width != null && container.height != null) { + return Padding( + padding: EdgeInsets.only(top: widget.message.content.isEmpty && index == 0 ? 0 : elementSpacing), + child: ImageAttachmentRenderer(image: container, hoverCheck: true), + ); + } - return Padding( - padding: EdgeInsets.only(top: widget.message.content.isEmpty && index == 0 ? 0 : elementSpacing), - child: AttachmentRenderer(container: container, message: widget.message, self: widget.self), - ); - }).toList(), + return Padding( + padding: EdgeInsets.only(top: widget.message.content.isEmpty && index == 0 ? 0 : elementSpacing), + child: AttachmentRenderer(container: container, message: widget.message, self: widget.self), + ); + }).toList(), ), ), ), diff --git a/lib/pages/chat/components/message/renderer/bubbles/bubbles_renderer.dart b/lib/pages/chat/components/message/renderer/bubbles/bubbles_renderer.dart index cfdc0b10..5ec7c587 100644 --- a/lib/pages/chat/components/message/renderer/bubbles/bubbles_renderer.dart +++ b/lib/pages/chat/components/message/renderer/bubbles/bubbles_renderer.dart @@ -1,4 +1,4 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/message_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; @@ -6,32 +6,34 @@ import 'package:chat_interface/pages/chat/components/message/renderer/bubbles/bu import 'package:chat_interface/pages/chat/components/message/renderer/bubbles/bubbles_message_renderer.dart'; import 'package:chat_interface/pages/chat/components/message/renderer/bubbles/bubbles_space_renderer.dart'; import 'package:chat_interface/pages/chat/components/message/renderer/bubbles/bubbles_system_renderer.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/theme/ui/dialogs/message_options_window.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; -import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:lorien_chat_list/lorien_chat_list.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:signals/signals_flutter.dart'; class BubblesRenderer extends StatefulWidget { - final int index; - final Message? message; + final Message message; final MessageProvider provider; final AutoScrollController controller; final double heightMultiplier; + final ChatListItemProperties properties; // Design of the bubbles final bool mobileLayout; const BubblesRenderer({ super.key, - required this.index, required this.controller, + required this.properties, required this.provider, - this.message, + required this.message, this.mobileLayout = false, this.heightMultiplier = 1.0, }); @@ -40,10 +42,9 @@ class BubblesRenderer extends StatefulWidget { State createState() => _BubblesRendererState(); } -class _BubblesRendererState extends State with TickerProviderStateMixin { - final GlobalKey _heightKey = GlobalKey(); +class _BubblesRendererState extends State with TickerProviderStateMixin, SignalsMixin { final GlobalKey contextMenuKey = GlobalKey(); - final hovering = false.obs; + final hovering = signal(false); Message? _message; @override @@ -53,89 +54,64 @@ class _BubblesRendererState extends State with TickerProviderSt super.dispose(); } - /// Called when the height should be reported back to the message controller - void heightCallback(Message message, Duration timeStamp) { - if (_heightKey.currentContext == null) { - sendLog("couldn't find height, this message has been disposed"); - return; - } - - // Report the actual height to the controller to scroll up the viewport - message.heightReported = true; - message.heightKey = _heightKey; - widget.provider.messageHeightCallback(message, _heightKey.currentContext!.size!.height); - } - @override Widget build(BuildContext context) { - final friendController = Get.find(); + final message = widget.message; - // This is needed for jump to message - if (widget.index == widget.provider.messages.length + 1) { - return Obx(() { - final loading = widget.provider.newMessagesLoading.value && widget.provider.messages.isEmpty; - return SizedBox( - height: Get.height * widget.heightMultiplier, - child: Visibility( - visible: !loading, - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: sectionSpacing), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "chat.welcome.title".tr, - style: Get.theme.textTheme.headlineMedium, - textAlign: TextAlign.center, - ), - verticalSpacing(sectionSpacing), - Text( - "chat.welcome.desc".tr, - style: Get.theme.textTheme.bodyMedium, - textAlign: TextAlign.center, - ) - ], - ), - ), - ), - ), - ), - ); - }); - } + // Evaluate whether we need a heading + bool lastMessage = false; + bool readHeading = false; + bool last = widget.properties.isAtTopEdge; + bool newHeading = false; + final nextMessageId = widget.provider.getNextMessageId(widget.properties.index); + if (nextMessageId != null && widget.provider.messages[nextMessageId] != null) { + final nextMessage = widget.provider.messages[nextMessageId]!; - // Just for spacing above the input and a loading indicator - if (widget.index == 0 && widget.message == null) { - return verticalSpacing(defaultSpacing); - } + // Check if the last message was a day before the current one + if (nextMessage.createdAt.day != message.createdAt.day) { + newHeading = true; + lastMessage = true; + } - //* Chat bubbles - final message = widget.message ?? widget.provider.messages[widget.index - 1]; + // Check if we should render the profile picture + if (nextMessage.senderAddress != message.senderAddress) { + lastMessage = true; + } - // Call the height callback (in case requested, for keeping the viewport up to date with the scroll) - if (message.heightCallback && !message.heightReported) { - WidgetsBinding.instance.addPostFrameCallback((timestamp) => heightCallback(message, timestamp)); + // See if we need a heading to indicate messages below it are not read + if (widget.provider is ConversationMessageProvider) { + final provider = widget.provider as ConversationMessageProvider; + final readTime = provider.conversation.reads.get(provider.extra); + if (readTime < message.createdAt.millisecondsSinceEpoch && + readTime > nextMessage.createdAt.millisecondsSinceEpoch) { + readHeading = true; + } + } + } else { + lastMessage = true; } - if (message.type == MessageType.system) { - return BubblesSystemMessageRenderer(message: message, provider: widget.provider); + // Make sure to also show it when there are no messages before the current one + if (nextMessageId == null && widget.provider is ConversationMessageProvider) { + final provider = widget.provider as ConversationMessageProvider; + final readTime = provider.conversation.reads.get(provider.extra); + if (readTime < message.createdAt.millisecondsSinceEpoch) { + readHeading = true; + } } - final sender = friendController.friends[message.senderAddress]; - final self = message.senderAddress == StatusController.ownAddress; - - bool last = false; - bool newHeading = false; - if (widget.index != widget.provider.messages.length) { - final lastMessage = widget.provider.messages[widget.index]; - // Check if the last message was a day before the current one - if (lastMessage.createdAt.day != message.createdAt.day) { - newHeading = true; - } + if (message.type == MessageType.system) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (newHeading || last) renderHeightTag(message), + if (readHeading) renderRead(message, newHeading || last), + BubblesSystemMessageRenderer(message: message, provider: widget.provider), + ], + ); } + final sender = FriendController.friends[message.senderAddress]; + final self = message.senderAddress == StatusController.ownAddress; final Widget renderer; switch (message.type) { @@ -146,7 +122,7 @@ class _BubblesRendererState extends State with TickerProviderSt provider: widget.provider, senderAddress: message.senderAddress, self: self, - last: last, + last: lastMessage, sender: self ? Friend.me() : sender, mobileLayout: widget.mobileLayout, ); @@ -157,7 +133,7 @@ class _BubblesRendererState extends State with TickerProviderSt message: message, provider: widget.provider, self: self, - last: last, + last: lastMessage, sender: self ? Friend.me() : sender, mobileLayout: widget.mobileLayout, ); @@ -185,100 +161,84 @@ class _BubblesRendererState extends State with TickerProviderSt message.highlightAnimation ??= AnimationController(vsync: this); message.highlightCallback?.call(); message.highlightCallback = null; - final messageWidget = AutoScrollTag( - index: widget.index, + + return AutoScrollTag( + index: widget.properties.index, key: ValueKey("${message.id}-tag"), controller: widget.controller, - child: SizedBox( - key: _heightKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (newHeading || widget.index == widget.provider.messages.length) - Padding( - padding: const EdgeInsets.only(top: sectionSpacing, bottom: defaultSpacing), - child: Text(formatDay(message.createdAt), style: Get.theme.textTheme.bodyMedium), - ), - MouseRegion( - onEnter: (event) { - hovering.value = true; - Get.find().hoveredMessage = message; - }, - onHover: (event) { - if (hovering.value) { - return; - } - hovering.value = true; - }, - onExit: (event) { - hovering.value = false; - Get.find().hoveredMessage = null; - }, - child: Row( - textDirection: self ? TextDirection.rtl : TextDirection.ltr, - children: [ - Flexible( - child: Animate( - controller: message.highlightAnimation, - effects: [ - ShimmerEffect( - duration: 1000.ms, - curve: Curves.ease, - ), - ], - target: 0, - child: renderer, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (newHeading || last) renderHeightTag(message), + if (readHeading) renderRead(message, newHeading || last), + MouseRegion( + onEnter: (event) { + hovering.value = true; + MessageController.hoveredMessage = message; + }, + onHover: (event) { + if (hovering.value) { + return; + } + hovering.value = true; + }, + onExit: (event) { + hovering.value = false; + MessageController.hoveredMessage = null; + }, + child: Row( + textDirection: self ? TextDirection.rtl : TextDirection.ltr, + children: [ + Flexible( + child: Animate( + controller: message.highlightAnimation, + effects: [ + ScaleEffect( + begin: Offset(1, 1), + end: Offset(1.15, 1.15), + curve: Curves.ease, + alignment: self ? Alignment.centerRight : Alignment.centerLeft, + ), + ], + target: 0, + child: renderer, ), - if (!widget.mobileLayout) renderOverlay(self, message), - if (widget.mobileLayout && !GetPlatform.isMobile) renderOverlay(self, message) - ], - ), + ), + if (!widget.mobileLayout) renderOverlay(self, message), + if (widget.mobileLayout && !GetPlatform.isMobile) renderOverlay(self, message), + ], ), - ], - ), - ), - ); - - if (message.playAnimation) { - message.initAnimation(this); - return Animate( - effects: [ - ExpandEffect( - alignment: Alignment.center, - duration: 250.ms, - curve: Curves.ease, - axis: Axis.vertical, - ), - FadeEffect( - begin: 0, - end: 1, - duration: 500.ms, ), ], - autoPlay: false, - controller: message.controller!, - onComplete: (controller) => message.playAnimation = false, - child: messageWidget, - ); - } + ), + ); + } - if (message.heightCallback) { - return Obx(() { - return Align( - alignment: Alignment.topCenter, - heightFactor: message.canScroll.value ? 1 : 0, - child: messageWidget, - ); - }); - } + Widget renderHeightTag(Message message) { + return Padding( + padding: const EdgeInsets.only(top: sectionSpacing, bottom: defaultSpacing), + child: Text(formatDay(message.createdAt), style: Get.theme.textTheme.bodyMedium), + ); + } - return messageWidget; + Widget renderRead(Message message, bool heading) { + return Padding( + padding: EdgeInsets.only(top: heading ? 0 : defaultSpacing, bottom: elementSpacing), + child: Row( + children: [ + Expanded(child: Container(height: 2, color: Get.theme.colorScheme.error)), + horizontalSpacing(defaultSpacing), + Text("unread.messages".tr, style: Get.textTheme.labelMedium), + horizontalSpacing(defaultSpacing), + Expanded(child: Container(height: 2, color: Get.theme.colorScheme.error)), + ], + ), + ); } Widget renderOverlay(bool self, Message message) { - return Obx( - () => SizedBox( + return Watch( + (ctx) => SizedBox( height: 34, child: Visibility( visible: hovering.value, @@ -312,7 +272,7 @@ class _BubblesRendererState extends State with TickerProviderSt MessageSendHelper.addReplyToCurrentDraft(message); }, icon: Icons.reply, - ) + ), ], ), ), diff --git a/lib/pages/chat/components/message/renderer/bubbles/bubbles_space_renderer.dart b/lib/pages/chat/components/message/renderer/bubbles/bubbles_space_renderer.dart index 80f0d5f5..8e7518ae 100644 --- a/lib/pages/chat/components/message/renderer/bubbles/bubbles_space_renderer.dart +++ b/lib/pages/chat/components/message/renderer/bubbles/bubbles_space_renderer.dart @@ -1,16 +1,18 @@ import 'dart:convert'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; -import 'package:chat_interface/controller/spaces/space_container.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/chat/components/message/renderer/space_renderer.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; import 'package:chat_interface/theme/ui/dialogs/message_options_window.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/theme/ui/profile/profile.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class BubblesSpaceMessageRenderer extends StatefulWidget { final Message message; @@ -35,9 +37,15 @@ class BubblesSpaceMessageRenderer extends StatefulWidget { } class _CallMessageRendererState extends State { - final loading = false.obs; + final _loading = signal(false); double _mouseX = 0, _mouseY = 0; + @override + void dispose() { + _loading.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { Friend sender = widget.sender ?? Friend.system(); @@ -59,26 +67,34 @@ class _CallMessageRendererState extends State { final menuData = ContextMenuData.fromPosition(Offset(_mouseX, _mouseY)); // Open the context menu - Get.dialog(MessageOptionsWindow( - data: menuData, - self: widget.message.senderAddress == StatusController.ownAddress, - message: widget.message, - provider: widget.provider, - )); + Get.dialog( + MessageOptionsWindow( + data: menuData, + self: widget.message.senderAddress == StatusController.ownAddress, + message: widget.message, + provider: widget.provider, + ), + ); }, child: Padding( - padding: EdgeInsets.symmetric( - vertical: elementSpacing, - ), + padding: EdgeInsets.symmetric(vertical: elementSpacing), child: Row( textDirection: widget.self ? TextDirection.rtl : TextDirection.ltr, crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Avatar of the message sender with a tooltip to know their name - Tooltip( - message: sender.displayName.value, - child: UserAvatar(id: sender.id, size: 34), + Visibility( + visible: widget.last, + replacement: const SizedBox(width: 34), //* Show timestamp instead + child: Tooltip( + message: sender.displayName.value, + child: InkWell( + borderRadius: BorderRadius.circular(100), + onTap: () => showModal(Profile(friend: sender)), + child: UserAvatar(id: sender.id, size: 34), + ), + ), ), horizontalSpacing(defaultSpacing), @@ -97,7 +113,10 @@ class _CallMessageRendererState extends State { padding: const EdgeInsets.all(defaultSpacing), decoration: BoxDecoration( borderRadius: BorderRadius.circular(defaultSpacing), - color: widget.self ? Get.theme.colorScheme.primary : Get.theme.colorScheme.primaryContainer, + color: + widget.self + ? Get.theme.colorScheme.primary + : Get.theme.colorScheme.primaryContainer, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -108,10 +127,7 @@ class _CallMessageRendererState extends State { Icon(Icons.public, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(elementSpacing), Flexible( - child: Text( - "chat.space_invite".tr, - style: Get.theme.textTheme.labelLarge, - ), + child: Text("chat.space_invite".tr, style: Get.theme.textTheme.labelLarge), ), ], ), @@ -119,15 +135,15 @@ class _CallMessageRendererState extends State { // Render the member preview for Spaces verticalSpacing(defaultSpacing), ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 350, - ), + constraints: const BoxConstraints(maxWidth: 350), child: SpaceRenderer( container: container, clickable: true, pollNewData: true, background: - widget.self ? Get.theme.colorScheme.onPrimary.withOpacity(0.13) : Get.theme.colorScheme.inverseSurface, + widget.self + ? Get.theme.colorScheme.onPrimary.withOpacity(0.13) + : Get.theme.colorScheme.inverseSurface, ), ), ], @@ -140,13 +156,16 @@ class _CallMessageRendererState extends State { Padding( padding: const EdgeInsets.only(top: defaultSpacing), child: SelectionContainer.disabled( - child: Text(formatMessageTime(widget.message.createdAt), style: Get.theme.textTheme.bodySmall), + child: Text( + formatMessageTime(widget.message.createdAt), + style: Get.theme.textTheme.bodySmall, + ), ), ), // Show a warning in case the message couldn't be verified horizontalSpacing(defaultSpacing), - Obx(() { + Watch((ctx) { final verified = widget.message.verified.value; return Visibility( visible: !verified, @@ -154,14 +173,11 @@ class _CallMessageRendererState extends State { padding: const EdgeInsets.only(top: elementSpacing + elementSpacing / 4), child: Tooltip( message: "chat.not.signed".tr, - child: const Icon( - Icons.warning_rounded, - color: Colors.amber, - ), + child: const Icon(Icons.warning_rounded, color: Colors.amber), ), ), ); - }) + }), ], ), ], diff --git a/lib/pages/chat/components/message/renderer/bubbles/bubbles_system_renderer.dart b/lib/pages/chat/components/message/renderer/bubbles/bubbles_system_renderer.dart index 4776a5c3..1de94b0e 100644 --- a/lib/pages/chat/components/message/renderer/bubbles/bubbles_system_renderer.dart +++ b/lib/pages/chat/components/message/renderer/bubbles/bubbles_system_renderer.dart @@ -32,9 +32,7 @@ class _MessageRendererState extends State { return Padding( padding: const EdgeInsets.only(top: defaultSpacing), child: Padding( - padding: const EdgeInsets.symmetric( - vertical: elementSpacing, - ), + padding: const EdgeInsets.symmetric(vertical: elementSpacing), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/pages/chat/components/message/renderer/bubbles/bubbles_zap_renderer.dart b/lib/pages/chat/components/message/renderer/bubbles/bubbles_zap_renderer.dart index 37f67381..a8f3994b 100644 --- a/lib/pages/chat/components/message/renderer/bubbles/bubbles_zap_renderer.dart +++ b/lib/pages/chat/components/message/renderer/bubbles/bubbles_zap_renderer.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/conversation/zap_share_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; import 'package:chat_interface/theme/components/file_renderer.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; import 'package:chat_interface/theme/ui/dialogs/message_options_window.dart'; @@ -15,6 +15,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class BubblesLiveshareMessageRenderer extends StatefulWidget { final MessageProvider provider; @@ -37,14 +38,14 @@ class BubblesLiveshareMessageRenderer extends StatefulWidget { } class _BubblesLiveshareMessageRendererState extends State { - final loading = true.obs; - final available = false.obs; - LiveshareInviteContainer? container; - int unavailableCount = 0; - final size = 0.obs; - String transactionBegin = ""; + final _loading = signal(true); + final _available = signal(false); + LiveshareInviteContainer? _container; + int _unavailableCount = 0; + final _size = signal(0); + String _transactionBegin = ""; - Timer? timer; + Timer? _timer; // For the context menu double _mouseX = 0, _mouseY = 0; @@ -52,50 +53,53 @@ class _BubblesLiveshareMessageRendererState extends State updateInfo()); - transactionBegin = container!.id; + _container = LiveshareInviteContainer.fromJson(widget.message.content); + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 3), (_) => updateInfo()); + _transactionBegin = _container!.id; updateInfo(); } Future updateInfo() async { - if (transactionBegin != container!.id) { - sendLog("WTF Flutter is actually weird $transactionBegin ${container!.id}"); + if (_transactionBegin != _container!.id) { + sendLog("WTF Flutter is actually weird $_transactionBegin ${_container!.id}"); return; } - final json = await postAny("${nodeProtocol()}${container!.url}/liveshare/info", { - "id": container!.id, - "token": container!.token, + final json = await postAny("${nodeProtocol()}${_container!.url}/liveshare/info", { + "id": _container!.id, + "token": _container!.token, }); - loading.value = false; + _loading.value = false; if (!json["success"]) { - unavailableCount++; - sendLog(unavailableCount); - if (unavailableCount > 5) { - available.value = false; - timer?.cancel(); + _unavailableCount++; + sendLog(_unavailableCount); + if (_unavailableCount > 5) { + _available.value = false; + _timer?.cancel(); } - available.value = false; + _available.value = false; return; } - available.value = true; - size.value = json["size"]; + _available.value = true; + _size.value = json["size"]; } @override void dispose() { + _loading.dispose(); + _available.dispose(); + _size.dispose(); + _timer?.cancel(); super.dispose(); - timer?.cancel(); } @override Widget build(BuildContext context) { Friend sender = widget.sender ?? Friend.system(); - container = LiveshareInviteContainer.fromJson(widget.message.content); + _container = LiveshareInviteContainer.fromJson(widget.message.content); return RepaintBoundary( child: MouseRegion( @@ -113,17 +117,17 @@ class _BubblesLiveshareMessageRendererState extends State(); - return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(defaultSpacing), - color: widget.self ? Get.theme.colorScheme.onPrimary.withOpacity(0.13) : Get.theme.colorScheme.inverseSurface, + color: widget.self ? Get.theme.colorScheme.onPrimary.withAlpha(40) : Get.theme.colorScheme.inverseSurface, ), padding: const EdgeInsets.symmetric(vertical: defaultSpacing, horizontal: defaultSpacing), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( - getIconForFileName(container!.fileName), + getIconForFileName(_container!.fileName), size: sectionSpacing * 2, color: Get.theme.colorScheme.onPrimary, ), @@ -247,15 +249,15 @@ class _BubblesLiveshareMessageRendererState extends State Text( - available.value ? formatFileSize(size.value) : 'chat.zapshare.not_found'.tr, + child: Watch( + (ctx) => Text( + _available.value ? formatFileSize(_size.value) : 'chat.zapshare.not_found'.tr, style: Get.theme.textTheme.bodyMedium, overflow: TextOverflow.ellipsis, ), @@ -267,38 +269,37 @@ class _BubblesLiveshareMessageRendererState extends State Get.find().joinTransaction( - convProvider.conversation.id, - widget.message.senderAddress, - container!, - ), + onPressed: + () => ZapShareController.joinTransaction( + convProvider.conversation.id, + widget.message.senderAddress, + _container!, + ), icon: const Icon(Icons.check), ), ); diff --git a/lib/pages/chat/components/message/renderer/file_info_window.dart b/lib/pages/chat/components/message/renderer/file_info_window.dart index 11e506c2..f509fe04 100644 --- a/lib/pages/chat/components/message/renderer/file_info_window.dart +++ b/lib/pages/chat/components/message/renderer/file_info_window.dart @@ -5,6 +5,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class FileInfoWindow extends StatefulWidget { final AttachmentContainer container; @@ -16,8 +17,8 @@ class FileInfoWindow extends StatefulWidget { } class _ConversationAddWindowState extends State { - final _errorText = "".obs; - final _loading = true.obs; + final _errorText = signal(""); + final _loading = signal(true); double _size = 0.0; @override @@ -26,10 +27,15 @@ class _ConversationAddWindowState extends State { super.initState(); } + @override + void dispose() { + _errorText.dispose(); + _loading.dispose(); + super.dispose(); + } + Future grabFileInfo() async { - final json = await postAuthorizedJSON("/account/files/info", { - "id": widget.container.id, - }); + final json = await postAuthorizedJSON("/account/files/info", {"id": widget.container.id}); if (!json["success"]) { _errorText.value = json["error"]; @@ -43,22 +49,14 @@ class _ConversationAddWindowState extends State { @override Widget build(BuildContext context) { return DialogBase( - child: Obx(() { + child: Watch((ctx) { // Show loading spinner if (_loading.value) { - return Center( - heightFactor: 1, - child: CircularProgressIndicator( - color: Get.theme.colorScheme.onPrimary, - ), - ); + return Center(heightFactor: 1, child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary)); } if (_errorText.value.isNotEmpty) { - return Center( - heightFactor: 1, - child: Text(_errorText.value, style: Get.textTheme.bodyMedium), - ); + return Center(heightFactor: 1, child: Text(_errorText.value, style: Get.textTheme.bodyMedium)); } return Column( @@ -66,10 +64,7 @@ class _ConversationAddWindowState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "file.dialog".trParams({ - "name": widget.container.name, - "size": _size.toStringAsFixed(2), - }), + "file.dialog".trParams({"name": widget.container.name, "size": _size.toStringAsFixed(2)}), style: Get.textTheme.bodyMedium, ), verticalSpacing(defaultSpacing), @@ -79,7 +74,6 @@ class _ConversationAddWindowState extends State { onTap: () { Get.back(); }, - loading: false.obs, ), verticalSpacing(elementSpacing), ProfileButton( @@ -88,7 +82,6 @@ class _ConversationAddWindowState extends State { onTap: () { Get.back(); }, - loading: false.obs, ), ], ); diff --git a/lib/pages/chat/components/message/renderer/image_attachment_renderer.dart b/lib/pages/chat/components/message/renderer/image_attachment_renderer.dart index b8fd42f9..bd7ff1ea 100644 --- a/lib/pages/chat/components/message/renderer/image_attachment_renderer.dart +++ b/lib/pages/chat/components/message/renderer/image_attachment_renderer.dart @@ -6,16 +6,13 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:liphium_bridge/liphium_bridge.dart'; +import 'package:signals/signals_flutter.dart'; class ImageAttachmentRenderer extends StatefulWidget { final bool hoverCheck; final AttachmentContainer image; - const ImageAttachmentRenderer({ - super.key, - required this.image, - this.hoverCheck = false, - }); + const ImageAttachmentRenderer({super.key, required this.image, this.hoverCheck = false}); @override State createState() => _ImageAttachmentRendererState(); @@ -28,14 +25,12 @@ class _ImageAttachmentRendererState extends State { final height = widget.image.height!.toDouble(); return ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 350, - ), + constraints: const BoxConstraints(maxHeight: 350), child: ClipRRect( borderRadius: BorderRadius.circular(defaultSpacing), child: AspectRatio( aspectRatio: width / height, - child: Obx(() { + child: Watch((ctx) { if (widget.image.downloading.value) { return Container( width: width, @@ -63,7 +58,7 @@ class _ImageAttachmentRendererState extends State { child: Center( child: IconButton( onPressed: () { - Get.find().downloadAttachment(widget.image, retry: true); + AttachmentController.downloadAttachment(widget.image, retry: true); }, icon: const Icon(Icons.refresh, size: 40), ), @@ -79,7 +74,7 @@ class _ImageAttachmentRendererState extends State { child: Center( child: IconButton( onPressed: () { - Get.find().downloadAttachment(widget.image); + AttachmentController.downloadAttachment(widget.image); }, icon: const Icon(Icons.download, size: 40), ), @@ -94,11 +89,11 @@ class _ImageAttachmentRendererState extends State { container: widget.image, onEnter: () { if (widget.hoverCheck) { - Get.find().hoveredAttachment = widget.image; + MessageController.hoveredAttachment = widget.image; } }, onExit: () { - Get.find().hoveredAttachment = widget.image; + MessageController.hoveredAttachment = widget.image; }, child: MouseRegion( cursor: SystemMouseCursors.click, diff --git a/lib/pages/chat/components/message/renderer/material/material_message_renderer.dart b/lib/pages/chat/components/message/renderer/material/material_message_renderer.dart index c17481bb..6726ede3 100644 --- a/lib/pages/chat/components/message/renderer/material/material_message_renderer.dart +++ b/lib/pages/chat/components/message/renderer/material/material_message_renderer.dart @@ -1,4 +1,4 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/chat/components/message/renderer/attachment_renderer.dart'; @@ -11,6 +11,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class MaterialMessageRenderer extends StatefulWidget { final LPHAddress senderAddress; @@ -60,12 +61,14 @@ class _MaterialMessageRendererState extends State { final menuData = ContextMenuData.fromPosition(Offset(_mouseX, _mouseY)); // Open the context menu - Get.dialog(MessageOptionsWindow( - data: menuData, - self: widget.message.senderAddress == StatusController.ownAddress, - message: widget.message, - provider: widget.provider, - )); + Get.dialog( + MessageOptionsWindow( + data: menuData, + self: widget.message.senderAddress == StatusController.ownAddress, + message: widget.message, + provider: widget.provider, + ), + ); }, child: Padding( padding: EdgeInsets.symmetric( @@ -83,12 +86,7 @@ class _MaterialMessageRendererState extends State { horizontalSpacing(defaultSpacing), // Render the display name of the user - Obx( - () => Text( - sender.displayName.value, - style: Get.textTheme.labelLarge, - ), - ), + Watch((ctx) => Text(sender.displayName.value, style: Get.textTheme.labelLarge)), const Spacer(), // Add some spacing @@ -100,7 +98,7 @@ class _MaterialMessageRendererState extends State { ), // Render the verified indicator of the message - Obx(() { + Watch((ctx) { final verified = widget.message.verified.value; return Visibility( visible: !verified, @@ -108,14 +106,11 @@ class _MaterialMessageRendererState extends State { padding: const EdgeInsets.only(top: elementSpacing + elementSpacing / 4), child: Tooltip( message: "chat.not.signed".tr, - child: const Icon( - Icons.warning_rounded, - color: Colors.amber, - ), + child: const Icon(Icons.warning_rounded, color: Colors.amber), ), ), ); - }) + }), ], ), verticalSpacing(elementSpacing), @@ -126,17 +121,11 @@ class _MaterialMessageRendererState extends State { mainAxisSize: MainAxisSize.min, textDirection: widget.self ? TextDirection.rtl : TextDirection.ltr, crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: renderMessageContent(), - ), - ], + children: [Flexible(child: renderMessageContent())], ), ), - Flexible( - child: renderAttachments(), - ) + Flexible(child: renderAttachments()), ], ), ), @@ -150,10 +139,7 @@ class _MaterialMessageRendererState extends State { visible: widget.message.content.isNotEmpty, child: Padding( padding: const EdgeInsets.only(left: elementSpacing), - child: FormattedText( - text: widget.message.content, - baseStyle: Get.theme.textTheme.bodyLarge!, - ), + child: FormattedText(text: widget.message.content, baseStyle: Get.theme.textTheme.bodyLarge!), ), ); } @@ -189,7 +175,10 @@ class _MaterialMessageRendererState extends State { Flexible( child: Text( AnswerData.answerContent( - widget.message.answerMessage!.type, widget.message.answerMessage!.content, widget.message.answerMessage!.attachments), + widget.message.answerMessage!.type, + widget.message.answerMessage!.content, + widget.message.answerMessage!.attachments, + ), style: Get.theme.textTheme.labelMedium, overflow: TextOverflow.ellipsis, maxLines: 1, @@ -211,24 +200,22 @@ class _MaterialMessageRendererState extends State { visible: widget.message.attachmentsRenderer.isNotEmpty, child: Column( mainAxisSize: MainAxisSize.min, - children: List.generate(widget.message.attachmentsRenderer.length, (index) { - final container = widget.message.attachmentsRenderer[index]; - - if (container.width != null && container.height != null) { - return Padding( - padding: EdgeInsets.only(top: elementSpacing), - child: ImageAttachmentRenderer( - image: container, - hoverCheck: true, - ), - ); - } - - return Padding( - padding: EdgeInsets.only(top: elementSpacing), - child: AttachmentRenderer(container: container, message: widget.message, self: widget.self), - ); - }).toList(), + children: + List.generate(widget.message.attachmentsRenderer.length, (index) { + final container = widget.message.attachmentsRenderer[index]; + + if (container.width != null && container.height != null) { + return Padding( + padding: EdgeInsets.only(top: elementSpacing), + child: ImageAttachmentRenderer(image: container, hoverCheck: true), + ); + } + + return Padding( + padding: EdgeInsets.only(top: elementSpacing), + child: AttachmentRenderer(container: container, message: widget.message, self: widget.self), + ); + }).toList(), ), ), ); diff --git a/lib/pages/chat/components/message/renderer/space_renderer.dart b/lib/pages/chat/components/message/renderer/space_renderer.dart index 846ae422..c03242ba 100644 --- a/lib/pages/chat/components/message/renderer/space_renderer.dart +++ b/lib/pages/chat/components/message/renderer/space_renderer.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:math'; -import 'package:chat_interface/controller/spaces/space_container.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/theme/components/duration_renderer.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; @@ -10,6 +10,7 @@ import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SpaceRenderer extends StatefulWidget { final bool requestOnInit; @@ -35,10 +36,10 @@ class SpaceRenderer extends StatefulWidget { State createState() => _SpaceRendererState(); } -class _SpaceRendererState extends State { - final _loading = true.obs; - final _info = Rx(null); - StreamSubscription? _sub; +class _SpaceRendererState extends State with SignalsMixin { + late final _loading = createSignal(true); + late final _info = createSignal(null); + Function()? _disposeInfoSub; @override void initState() { @@ -56,29 +57,34 @@ class _SpaceRendererState extends State { } Future loadState() async { - _info.value = await widget.container.getInfo(timer: widget.pollNewData); - _sub = widget.container.info.listen((info) { + final info = await widget.container.getInfo(timer: widget.pollNewData); + batch(() { + _info.value = info; + if (_info.value!.exists || _info.value!.error || !widget.pollNewData) { + _loading.value = false; + } + }); + + // Subscribe for future changes + _disposeInfoSub = widget.container.info.subscribe((info) { if (widget.container.cancelled) { _loading.value = false; } _info.value = info; }); - if (_info.value!.exists || _info.value!.error || !widget.pollNewData) { - _loading.value = false; - } } @override void dispose() { widget.container.onDrop(); - _sub?.cancel(); + _disposeInfoSub?.call(); super.dispose(); } @override Widget build(BuildContext context) { return RepaintBoundary( - child: Obx(() { + child: Watch((ctx) { if (_loading.value || _info.value == null) { return Container( decoration: BoxDecoration( @@ -96,9 +102,7 @@ class _SpaceRendererState extends State { child: SizedBox( width: 30, height: 30, - child: CircularProgressIndicator( - color: Get.theme.colorScheme.onPrimary, - ), + child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary), ), ), horizontalSpacing(defaultSpacing), @@ -174,81 +178,83 @@ class _SpaceRendererState extends State { final renderAmount = min(info.friends.length, 3); return Material( - color: widget.background ?? + color: + widget.background ?? (widget.sidebar ? Get.theme.colorScheme.primary.withAlpha(100) : widget.clickable - ? Get.theme.colorScheme.primaryContainer - : Colors.transparent), + ? Get.theme.colorScheme.primaryContainer + : Colors.transparent), borderRadius: BorderRadius.circular(defaultSpacing), child: InkWell( borderRadius: BorderRadius.circular(defaultSpacing), - onTap: widget.clickable - ? () { - showConfirmPopup( - ConfirmWindow( - title: "join.space".tr, - text: "join.space.popup".tr, - onConfirm: () { - Get.find().join(widget.container); - }, - ), - ); - } - : null, + onTap: + widget.clickable + ? () { + showConfirmPopup( + ConfirmWindow( + title: "join.space".tr, + text: "join.space.popup".tr, + onConfirm: () { + SpaceController.join(widget.container); + }, + ), + ); + } + : null, child: Padding( padding: widget.clickable ? const EdgeInsets.all(defaultSpacing) : const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Visibility( - visible: renderAmount > 0, - child: Flexible( - child: SizedBox( - width: 44 + 25 * (renderAmount - 1), - height: 44, - child: Stack( - children: List.generate(renderAmount, (index) { - return Positioned( - left: index * 25, - child: Tooltip( - message: info.friends[index].displayName.value, - child: SizedBox( - width: 44, - height: 44, - child: UserAvatar( - id: info.friends[index].id, - size: 44, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Visibility( + visible: renderAmount > 0, + child: Flexible( + child: SizedBox( + width: 44 + 25 * (renderAmount - 1), + height: 44, + child: Stack( + children: List.generate(renderAmount, (index) { + return Positioned( + left: index * 25, + child: Tooltip( + message: info.friends[index].displayName.value, + child: SizedBox( + width: 44, + height: 44, + child: UserAvatar(id: info.friends[index].id, size: 44), ), ), - ), - ); - }), + ); + }), + ), ), ), ), - ), - Visibility( - visible: partyAmount >= renderAmount && renderAmount > 0 && partyAmount != renderAmount, - child: Padding( - padding: const EdgeInsets.only(left: defaultSpacing), - child: Text("+${partyAmount - renderAmount}", style: Get.theme.textTheme.bodyLarge), + Visibility( + visible: partyAmount >= renderAmount && renderAmount > 0 && partyAmount != renderAmount, + child: Padding( + padding: const EdgeInsets.only(left: defaultSpacing), + child: Text("+${partyAmount - renderAmount}", style: Get.theme.textTheme.bodyLarge), + ), ), - ), - Visibility( - visible: renderAmount == 0, - child: Text("$partyAmount members", style: Get.theme.textTheme.bodyLarge), - ) - ], - ) - ]), + Visibility( + visible: renderAmount == 0, + child: Text("$partyAmount members", style: Get.theme.textTheme.bodyLarge), + ), + ], + ), + ], + ), ), - DurationRenderer(info.start, style: Get.theme.textTheme.bodyLarge) + DurationRenderer(info.start, style: Get.theme.textTheme.bodyLarge), ], ), ), diff --git a/lib/pages/chat/components/squares/shared_space_add_window.dart b/lib/pages/chat/components/squares/shared_space_add_window.dart new file mode 100644 index 00000000..d8c4877f --- /dev/null +++ b/lib/pages/chat/components/squares/shared_space_add_window.dart @@ -0,0 +1,225 @@ +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/services/squares/square_service.dart'; +import 'package:chat_interface/services/squares/square_shared_space.dart'; +import 'package:chat_interface/theme/components/forms/fj_button.dart'; +import 'package:chat_interface/theme/components/forms/fj_switch.dart'; +import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/constants.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class SharedSpaceAddWindow extends StatefulWidget { + final Square square; + final String? action; + final SharedSpace? space; + final PinnedSharedSpace? pinned; + final bool onlyEdit; + + const SharedSpaceAddWindow({ + super.key, + required this.square, + this.action = "create", + this.space, + this.pinned, + this.onlyEdit = false, + }); + + @override + State createState() => _SharedSpaceAddWindowState(); +} + +class _SharedSpaceAddWindowState extends State { + // Text controllers + final _nameController = TextEditingController(); + + // The current pin state + late final _pinned = signal(widget.pinned != null); + + // State + final _errorText = signal(''); + final _loading = signal(false); + + @override + void dispose() { + _errorText.dispose(); + _loading.dispose(); + super.dispose(); + } + + /// Perform the action + Future save() async { + if (_loading.value) return; + _loading.value = true; + _errorText.value = ""; + + // Make sure the name fits within the requirements + final name = _nameController.text; + if (name.isEmpty || name == "") { + _errorText.value = "squares.space.name_needed".tr; + _loading.value = false; + return; + } + if (name.length > specialConstants[Constants.specialConstantMaxConversationNameLength]!) { + _errorText.value = "squares.space.name.length".trParams({ + "length": specialConstants[Constants.specialConstantMaxConversationNameLength].toString(), + }); + _loading.value = false; + return; + } + + // Edit the space in case in edit mode + if (widget.onlyEdit) { + await editSpace(name); + return; + } + + // Create a pinned space in case desired + if (_pinned.peek()) { + // Pin the space + final pinnedSpace = SquareService.newPinnedSharedSpace(widget.square, name); + var error = await SquareService.pinPinnedSpace(widget.square, pinnedSpace); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + + // Create a new shared space with the pinned space passed in + error = await SquareService.createSharedSpace(widget.square, name, underlyingId: pinnedSpace.id, rejoin: true); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + + _loading.value = false; + Get.back(); + return; + } + + // Create the Space + final error = await SquareService.createSharedSpace(widget.square, name, rejoin: true); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + + _loading.value = false; + Get.back(); + } + + /// Edit the space + Future editSpace(String name) async { + // If the space shouldn't be pinned anymore, unpin it + if (widget.pinned != null && !_pinned.peek()) { + sendLog("unpin shared space"); + // If it shouldn't be pinned anymore, remove it + final error = await SquareService.unpinSharedSpace(widget.square, widget.pinned!.id, space: widget.space); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + } + + // If the space should be pinned, pin it + if (widget.pinned == null && widget.space != null && _pinned.peek()) { + sendLog("pin shared space"); + final error = await SquareService.pinSharedSpace(widget.square, widget.space!); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + } + + // If the name changed and there is a pinned spxace, change the name of it + if (name != (widget.pinned?.name ?? "") && widget.pinned != null && _pinned.peek()) { + sendLog("change name square"); + final error = await SquareService.changePinnedName(widget.square, widget.pinned!, name); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + } + + // If there is a shared space, also change the name on the server + if (name != (widget.space?.name ?? "") && widget.space != null) { + sendLog("change name shared-spaces"); + final error = await SquareService.renameSharedSpace(widget.square, widget.space!.id, name); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + } + + _loading.value = false; + Get.back(); + } + + @override + void initState() { + // Set the name in case provided + if (widget.pinned != null) { + _nameController.text = widget.pinned!.name; + } + if (widget.space != null) { + _nameController.text = widget.space!.name; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return DialogBase( + title: [Text("squares.spaces.${widget.action}".tr, style: Get.textTheme.labelLarge)], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FJTextField( + hintText: 'squares.spaces.name.placeholder'.tr, + controller: _nameController, + maxLength: specialConstants[Constants.specialConstantMaxConversationNameLength], + autofocus: true, + onSubmitted: (t) => save(), + ), + verticalSpacing(defaultSpacing), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("pinned".tr, style: Get.theme.textTheme.bodyMedium), + Watch( + (ctx) => FJSwitch( + value: _pinned.value, + onChanged: (p0) { + _pinned.value = p0; + }, + ), + ), + ], + ), + verticalSpacing(defaultSpacing), + AnimatedErrorContainer( + message: _errorText, + padding: const EdgeInsets.only(bottom: defaultSpacing), + expand: true, + ), + FJElevatedLoadingButtonCustom( + loading: _loading, + onTap: () => save(), + child: Center(child: Text("${widget.action}".tr, style: Get.theme.textTheme.labelLarge)), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat/components/squares/square_add_window.dart b/lib/pages/chat/components/squares/square_add_window.dart new file mode 100644 index 00000000..6a172c83 --- /dev/null +++ b/lib/pages/chat/components/squares/square_add_window.dart @@ -0,0 +1,176 @@ +import 'dart:async'; + +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/squares/square_service.dart'; +import 'package:chat_interface/theme/components/forms/fj_button.dart'; +import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; +import 'package:chat_interface/theme/ui/dialogs/conversation_add_window.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/constants.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class SquareAddWindow extends StatefulWidget { + final String title; + final String action; + final bool nameField; + final List? initial; + final ContextMenuData? position; + + /// Called when clicking the action button (returns error text or closed on null) + final Future Function(List, String?)? onDone; + + const SquareAddWindow({ + super.key, + required this.position, + this.title = "squares.create", + this.action = "create", + this.nameField = true, + this.initial, + this.onDone, + }); + + @override + State createState() => _SquareAddWindowState(); + + /// Create a square using a list of friends. + static Future createSquareAction(List friends, String name) async { + // Make sure the selection is valid + if (friends.length > specialConstants[Constants.specialConstantMaxConversationMembers]!) { + return "squares.too_many_members".trParams({ + "amount": specialConstants[Constants.specialConstantMaxConversationMembers]!.toString(), + }); + } + if (name.isEmpty) { + return "squares.name_needed".tr; + } + if (name.length > specialConstants[Constants.specialConstantMaxConversationNameLength]!) { + return "squares.name.length".trParams({"length": specialConstants["max_conversation_name_length"].toString()}); + } + + // Create the square + return SquareService.openSquare(friends, name); + } +} + +class _SquareAddWindowState extends State { + // State + final _members = listSignal([]); + final _conversationLoading = signal(false); + final _errorText = signal(""); + final _search = signal(""); + + // Input + final _searchFocusNode = FocusNode(); + final _searchController = TextEditingController(); + final _controller = TextEditingController(); + + @override + void initState() { + if (widget.initial != null) { + for (var friend in widget.initial!) { + _members.add(friend); + } + } + if (!isMobileMode()) { + _searchFocusNode.requestFocus(); + } + super.initState(); + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _searchController.dispose(); + _controller.dispose(); + _members.dispose(); + _conversationLoading.dispose(); + _errorText.dispose(); + _search.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + + // Only show creation menu when the user has friends + if (FriendController.friends.length == 1) { + return SlidingWindowBase( + title: [Text(widget.title.tr, style: theme.textTheme.labelLarge)], + position: widget.position, + child: NoFriendsMessage(), + ); + } + + return SlidingWindowBase( + position: widget.position, + title: [ + Text(widget.title.tr, style: theme.textTheme.labelLarge), + const Spacer(), + Watch((ctx) => Text("${_members.length}/100", style: theme.textTheme.bodyLarge)), + ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Selector for the friends in the square + FriendSelector(signal: _members, initial: widget.initial), + + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // The input for naming the square + Visibility( + visible: widget.nameField, + child: Padding( + padding: const EdgeInsets.only(bottom: defaultSpacing), + child: FJTextField( + controller: _controller, + maxLength: specialConstants[Constants.specialConstantMaxConversationNameLength], + hintText: "squares.name.placeholder".tr, + ), + ), + ), + + // Where an error is displayed in case one happens + AnimatedErrorContainer( + expand: true, + padding: const EdgeInsets.only(bottom: defaultSpacing), + message: _errorText, + ), + + // The button for creating the square + FJElevatedLoadingButton( + onTap: () async { + _conversationLoading.value = true; + if (widget.onDone != null) { + final error = await widget.onDone!(_members, _controller.text); + if (error != null) { + _errorText.value = error; + } else { + Get.back(); + } + _conversationLoading.value = false; + return; + } + final error = await SquareAddWindow.createSquareAction(_members, _controller.text); + if (error != null) { + _errorText.value = error; + } else { + Get.back(); + } + _conversationLoading.value = false; + }, + label: widget.action.tr, + loading: _conversationLoading, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat/components/squares/square_shared_spaces.dart b/lib/pages/chat/components/squares/square_shared_spaces.dart new file mode 100644 index 00000000..b43a9494 --- /dev/null +++ b/lib/pages/chat/components/squares/square_shared_spaces.dart @@ -0,0 +1,339 @@ +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/square/shared_space_controller.dart'; +import 'package:chat_interface/pages/chat/components/squares/shared_space_add_window.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/services/squares/square_service.dart'; +import 'package:chat_interface/services/squares/square_shared_space.dart'; +import 'package:chat_interface/theme/components/forms/fj_button.dart'; +import 'package:chat_interface/theme/components/forms/icon_button.dart'; +import 'package:chat_interface/theme/components/user_renderer.dart'; +import 'package:chat_interface/util/dispose_hook.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class SquareSharedSpaces extends StatelessWidget { + final Square square; + + const SquareSharedSpaces({super.key, required this.square}); + + /// Connect to a shared space (with a loading signal) + Future joinSpaceAction(SharedSpace space) async { + // Make sure we're not connecting to the same space (or already connecting) + if (SpaceController.id.peek() == space.container.roomId || SpaceController.spaceLoading.peek()) { + return; + } + + // Leave the space in case currently in one + if (SpaceController.connected.peek()) { + await SpaceController.leaveSpace(); + } + + // Connect to the shared space + SpaceController.shouldSwitchToPage = false; + await SpaceController.join(space.container); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Render the pinned shared spaces + Watch((ctx) { + final container = square.containerSub.value as SquareContainer; + + // Render a reordable list so the pinned spaces can be dragged around + return ReorderableListView.builder( + dragStartBehavior: DragStartBehavior.start, + buildDefaultDragHandles: false, + shrinkWrap: true, + onReorder: (oldIndex, newIndex) async { + // Create a new container with the order changed + final copied = SquareContainer.copy(container); + + // Add at the new index + final removed = container.spaces.removeAt(oldIndex); + container.spaces.insert(oldIndex > newIndex ? newIndex : newIndex - 1, removed); + + // Change on the server, reset in case didn't work + final error = await SquareService.refreshContainer(square, copied); + if (error != null) { + square.containerSub.value = square.container; + showErrorPopup("error", error); + } else { + square.containerSub.value = container; + } + }, + itemCount: container.spaces.length, + itemBuilder: (context, index) { + final pinnedSpace = container.spaces[index]; + + return Padding( + key: ValueKey("psl-${pinnedSpace.id}"), + padding: const EdgeInsets.only(bottom: defaultSpacing), + child: ReorderableDelayedDragStartListener( + index: index, + child: Watch((ctx) { + final space = + SharedSpaceController.sharedSpaceMap[square.id]?.entries + .firstWhereOrNull((entry) => entry.value.underlyingId == pinnedSpace.id) + ?.value; + + return SignalHook( + value: false, + builder: (loading) { + // Create the onTap function for the item + Future onTap() async { + if (loading.value) { + return; + } + loading.value = true; + + // Connect to the space in case there is already one + if (space != null) { + await joinSpaceAction(space); + loading.value = false; + return; + } + + // Create a new shared space + final error = await SquareService.createSharedSpace( + square, + pinnedSpace.name, + underlyingId: pinnedSpace.id, + rejoin: true, + ); + loading.value = false; + if (error != null) { + showErrorPopup("error", error); + } + } + + // Render the pinned space as empty when there isn't a shared one + if (space == null) { + return renderSpaceItem( + pinnedSpace.name, + [], + onTap: onTap, + pinnedSpace: pinnedSpace, + loading: loading, + ); + } + + // Render the space as shared when it's actually there + return renderSpaceItem( + pinnedSpace.name, + space.members, + onTap: onTap, + pinnedSpace: pinnedSpace, + space: space, + loading: loading, + ); + }, + ); + }), + ), + ); + }, + ); + }), + + // Render dynamic shared spaces + Watch((ctx) { + // Only render when there actually are dynamically shared spaces + final sharedSpaces = SharedSpaceController.sharedSpaceMap[square.id]; + if (sharedSpaces == null) { + return SizedBox(); + } + final spaces = sharedSpaces.entries.where((entry) => entry.value.underlyingId == "-").toList(); + + // Render the dynamically shared space as an item + return ListView.builder( + shrinkWrap: true, + itemCount: spaces.length, + itemBuilder: (context, index) { + final space = spaces[index].value; + return Padding( + key: ValueKey("ssl-${space.id}"), + padding: const EdgeInsets.only(bottom: defaultSpacing), + child: SignalHook( + value: false, + builder: + (loading) => renderSpaceItem( + space.name, + space.members, + + // Join the space when the item is clicked + onTap: () async { + if (loading.value) { + return; + } + loading.value = true; + await joinSpaceAction(space); + loading.value = false; + }, + loading: loading, + space: space, + ), + ), + ); + }, + ); + }), + + // Create a button for creating shared spaces + Watch( + (ctx) => Visibility( + visible: SpaceController.connected.value, + child: Padding( + padding: const EdgeInsets.only(right: defaultSpacing, left: defaultSpacing, bottom: defaultSpacing), + child: FJElevatedButton( + onTap: () => showModal(SharedSpaceAddWindow(square: square, action: "add")), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.launch, color: Get.theme.colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Text("squares.spaces.add".tr, style: Get.theme.textTheme.labelMedium), + ], + ), + ), + ), + ), + ), + + // Create a button for creating shared spaces + Padding( + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), + child: FJElevatedButton( + onTap: () => showModal(SharedSpaceAddWindow(square: square)), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.rocket_launch, color: Get.theme.colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Text("squares.spaces.create".tr, style: Get.theme.textTheme.labelMedium), + ], + ), + ), + ), + ], + ); + } + + /// Render a shared space + Widget renderSpaceItem( + String name, + List members, { + SharedSpace? space, + PinnedSharedSpace? pinnedSpace, + Function()? onTap, + Signal? loading, + }) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), + child: SignalHook( + value: false, + builder: + (hovered) => Material( + color: Get.theme.colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(sectionSpacing), + child: InkWell( + borderRadius: BorderRadius.circular(sectionSpacing), + onTap: onTap, + onHover: (value) { + hovered.value = value; + }, + child: Container( + color: Colors.transparent, + width: double.infinity, + padding: EdgeInsets.all(defaultSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: elementSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + verticalSpacing(elementSpacing), + // Display name and icon + Row( + children: [ + Icon( + pinnedSpace != null ? Icons.volume_up : Icons.rocket_launch, + size: Get.textTheme.labelMedium!.fontSize! * 1.5, + ), + horizontalSpacing(defaultSpacing), + Flexible( + child: Text(name, overflow: TextOverflow.ellipsis, style: Get.textTheme.labelLarge), + ), + ], + ), + + // Render all of the members + for (var member in members) + Builder( + builder: (context) { + final friend = FriendController.getFriend(LPHAddress.from(member)); + return Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: Row( + children: [ + UserAvatar(id: friend.id, size: 28), + horizontalSpacing(defaultSpacing), + // Not watching here, should be fine (hover = update) + Flexible( + child: Text(friend.displayName.peek(), style: Get.textTheme.bodyMedium), + ), + ], + ), + ); + }, + ), + verticalSpacing(elementSpacing), + ], + ), + ), + ), + horizontalSpacing(defaultSpacing), + Visibility( + visible: hovered.value, + child: LoadingIconButton( + onTap: () { + showModal( + SharedSpaceAddWindow( + square: square, + action: "edit", + onlyEdit: true, + pinned: pinnedSpace, + space: space, + ), + ); + }, + loading: loading, + icon: Icons.edit, + iconSize: Get.textTheme.labelMedium!.fontSize! * 1.5, + extra: elementSpacing2 - 1, + padding: elementSpacing2 - 1, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/chat/components/squares/square_topic_list.dart b/lib/pages/chat/components/squares/square_topic_list.dart new file mode 100644 index 00000000..bba451a9 --- /dev/null +++ b/lib/pages/chat/components/squares/square_topic_list.dart @@ -0,0 +1,147 @@ +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/conversation/message_controller.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/pages/chat/components/conversations/notification_dot.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/services/squares/square_service.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class SquareTopicList extends StatefulWidget { + final Square square; + + const SquareTopicList({super.key, required this.square}); + + @override + State createState() => _SquareTopicListState(); +} + +class _SquareTopicListState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: sectionSpacing), + child: Watch((ctx) { + final container = widget.square.containerSub.value as SquareContainer; + + // Only render when the topics are shown + if (!widget.square.topicsShown.value) { + return SizedBox(); + } + + // Render the actual topic list + return ReorderableListView.builder( + shrinkWrap: true, + onReorder: (oldIndex, newIndex) async { + // Create a new container with the order changed + final copied = SquareContainer.copy(container); + + // Add at the new index + final removed = container.topics.removeAt(oldIndex); + container.topics.insert(oldIndex > newIndex ? newIndex : newIndex - 1, removed); + + // Change on the server, reset in case didn't work + final error = await SquareService.refreshContainer(widget.square, container); + if (error != null) { + widget.square.containerSub.value = copied; + showErrorPopup("error", error); + } else { + widget.square.containerSub.value = container; + } + }, + buildDefaultDragHandles: false, + itemCount: container.topics.length, + itemBuilder: (context, index) { + final topic = container.topics[index]; + return ReorderableDelayedDragStartListener( + key: ValueKey("topic-${widget.square.id.encode()}-${topic.id}"), + index: index, + child: Container( + color: Get.theme.colorScheme.onInverseSurface, + padding: const EdgeInsets.only(top: elementSpacing), + child: Watch( + (ctx) => Material( + borderRadius: BorderRadius.circular(defaultSpacing), + color: + (SidebarController.getCurrentProviderReactive()?.extra ?? "") == topic.id + ? Get.theme.colorScheme.onSurface.withAlpha(20) + : Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(defaultSpacing), + hoverColor: Get.theme.hoverColor, + splashColor: Get.theme.hoverColor, + + // When topic is tapped (open topic) + onTap: () { + // Make sure to not open when already open on desktop + if ((SidebarController.getCurrentProvider()?.extra ?? "") == topic.id && !isMobileMode()) { + return; + } + MessageController.openConversation(widget.square, extra: topic.id); + }, + onSecondaryTapDown: (details) { + ConversationMessageProvider( + widget.square, + extra: topic.id, + ).openDialogForConversation(ContextMenuData.fromPosition(details.globalPosition)); + }, + + // Topic item content + child: Padding( + padding: const EdgeInsets.all(elementSpacing2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + const Icon(Icons.numbers), + horizontalSpacing(elementSpacing2), + Flexible( + child: Text( + topic.name, + style: + (SidebarController.getCurrentProviderReactive()?.extra ?? "") == topic.id + ? Get.theme.textTheme.labelMedium + : Get.theme.textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + horizontalSpacing(elementSpacing2), + Watch((ctx) { + final notifications = + ConversationController.notificationMap[ConversationService.withExtra( + widget.square.id.encode(), + topic.id, + )] ?? + 0; + + return Visibility( + visible: notifications > 0, + child: NotificationDot(amount: notifications), + ); + }), + ], + ), + ), + ), + ), + ), + ), + ); + }, + ); + }, dependencies: [widget.square.topicsShown, widget.square.containerSub]), + ); + } +} diff --git a/lib/pages/chat/components/squares/topic_manage_window.dart b/lib/pages/chat/components/squares/topic_manage_window.dart new file mode 100644 index 00000000..9640e70f --- /dev/null +++ b/lib/pages/chat/components/squares/topic_manage_window.dart @@ -0,0 +1,119 @@ +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/services/squares/square_service.dart'; +import 'package:chat_interface/theme/components/forms/fj_button.dart'; +import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/constants.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class TopicManageWindow extends StatefulWidget { + final Square square; + final Topic? toEdit; + + const TopicManageWindow({super.key, required this.square, this.toEdit}); + + @override + State createState() => _TopicManageWindowState(); +} + +class _TopicManageWindowState extends State { + // Text controllers + final _nameController = TextEditingController(); + + // State + final _errorText = signal(''); + final _loading = signal(false); + + @override + void initState() { + // Set name of current topic in case there + _nameController.text = widget.toEdit?.name ?? ""; + super.initState(); + } + + @override + void dispose() { + _errorText.dispose(); + _loading.dispose(); + super.dispose(); + } + + /// Save the conversation title + Future save() async { + if (_loading.value) return; + _loading.value = true; + _errorText.value = ""; + + // Make sure the name fits within the requirements + final name = _nameController.text; + if (name.isEmpty || name == "") { + _errorText.value = "topic.name_needed".tr; + _loading.value = false; + return; + } + if (name.length > specialConstants[Constants.specialConstantMaxConversationNameLength]!) { + _errorText.value = "topic.name.length".trParams({ + "length": specialConstants[Constants.specialConstantMaxConversationNameLength].toString(), + }); + _loading.value = false; + return; + } + + // Add the topic or edit it + if (widget.toEdit != null) { + final error = await SquareService.renameTopic(widget.square, widget.toEdit!.id, _nameController.text); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + } else { + final error = await SquareService.createTopic(widget.square, _nameController.text); + if (error != null) { + _errorText.value = error; + _loading.value = false; + return; + } + } + + _loading.value = false; + Get.back(); + } + + @override + Widget build(BuildContext context) { + return DialogBase( + title: [Text("squares.topics.${widget.toEdit != null ? "edit" : "create"}".tr, style: Get.textTheme.labelLarge)], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FJTextField( + hintText: 'squares.topics.name.placeholder'.tr, + controller: _nameController, + maxLength: specialConstants[Constants.specialConstantMaxConversationNameLength], + autofocus: true, + onSubmitted: (t) => save(), + ), + verticalSpacing(defaultSpacing), + AnimatedErrorContainer( + message: _errorText, + padding: const EdgeInsets.only(bottom: defaultSpacing), + expand: true, + ), + FJElevatedLoadingButtonCustom( + loading: _loading, + onTap: () => save(), + child: Center( + child: Text((widget.toEdit != null ? "edit" : "create").tr, style: Get.theme.textTheme.labelLarge), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat/components/townsquare/townsquare_bar.dart b/lib/pages/chat/components/townsquare/townsquare_bar.dart deleted file mode 100644 index 76145942..00000000 --- a/lib/pages/chat/components/townsquare/townsquare_bar.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -class TownsquareBar extends StatefulWidget { - const TownsquareBar({super.key}); - - @override - State createState() => _TownsquareBarState(); -} - -class _TownsquareBarState extends State { - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.onInverseSurface, - borderRadius: BorderRadius.circular(sectionSpacing), - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: sectionSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - verticalSpacing(sectionSpacing), - - // The title of the page - Text("Townsquare", style: Theme.of(context).textTheme.titleLarge), - verticalSpacing(defaultSpacing * 0.5), - - // All of the actions on townsquare - TownsquareBarButton( - icon: Icons.edit, - label: "Create", - ), - TownsquareBarButton( - icon: Icons.dashboard, - label: "Posts", - ), - TownsquareBarButton( - icon: Icons.account_circle, - label: "Profile", - ), - - verticalSpacing(sectionSpacing), - Text("Friends", style: Theme.of(context).textTheme.titleLarge), - verticalSpacing(defaultSpacing * 0.5), - TownsquareBarButton( - icon: Icons.account_circle, - label: "Friend 1", - ), - verticalSpacing(sectionSpacing), - ], - ), - ), - ), - ), - ), - ); - } -} - -class TownsquareBarButton extends StatelessWidget { - final IconData icon; - final String label; - - const TownsquareBarButton({super.key, required this.icon, required this.label}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: defaultSpacing), - child: Material( - color: Get.theme.colorScheme.inverseSurface, - borderRadius: BorderRadius.circular(defaultSpacing), - child: InkWell( - onTap: () {}, - borderRadius: BorderRadius.circular(defaultSpacing), - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Row( - children: [ - Icon( - icon, - color: Theme.of(context).colorScheme.onPrimary, - size: Get.theme.textTheme.titleLarge!.fontSize! * 1.5, - ), - horizontalSpacing(defaultSpacing), - Expanded( - child: Text( - label, - style: Theme.of(context).textTheme.labelLarge!, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/chat/components/townsquare/townsquare_page.dart b/lib/pages/chat/components/townsquare/townsquare_page.dart deleted file mode 100644 index f0ff41a5..00000000 --- a/lib/pages/chat/components/townsquare/townsquare_page.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:chat_interface/pages/chat/components/townsquare/townsquare_bar.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; - -class TownsquarePage extends StatefulWidget { - const TownsquarePage({super.key}); - - @override - State createState() => _TownsquarePageState(); -} - -class _TownsquarePageState extends State { - @override - Widget build(BuildContext context) { - return Center( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const TownsquareBar(), - horizontalSpacing(defaultSpacing), - const SizedBox( - width: 700, - child: Placeholder(), - ) - ], - ), - ); - } -} diff --git a/lib/pages/chat/conversation_list_mobile.dart b/lib/pages/chat/conversation_list_mobile.dart index 957e2103..1f3ddf3f 100644 --- a/lib/pages/chat/conversation_list_mobile.dart +++ b/lib/pages/chat/conversation_list_mobile.dart @@ -18,9 +18,7 @@ class _SidebarState extends State { Widget build(BuildContext context) { //* Sidebar return Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.onInverseSurface, - ), + decoration: BoxDecoration(color: Get.theme.colorScheme.onInverseSurface), //* Sidebar content child: Stack( @@ -38,10 +36,7 @@ class _SidebarState extends State { padding: const EdgeInsets.all(defaultSpacing * 1.5), child: Row( children: [ - SizedBox( - height: 32, - child: Image.asset("assets/tray/icon_linux.png"), - ), + SizedBox(height: 32, child: Image.asset("assets/tray/icon_linux.png")), horizontalSpacing(defaultSpacing * 1.5), Text("Liphium", style: Get.textTheme.labelLarge), ], @@ -51,16 +46,7 @@ class _SidebarState extends State { ), // Conversation list and the profile - Expanded( - child: SafeArea( - top: false, - bottom: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), - child: SidebarConversationList(query: "".obs), - ), - ), - ), + Expanded(child: SafeArea(top: false, bottom: false, child: SidebarConversationList())), ], ), Align( @@ -77,7 +63,7 @@ class _SidebarState extends State { color: Get.theme.colorScheme.onPrimary, ), ), - ) + ), ], ), ); diff --git a/lib/pages/chat/messages/message_automaton.dart b/lib/pages/chat/messages/message_automaton.dart new file mode 100644 index 00000000..4a3d68f4 --- /dev/null +++ b/lib/pages/chat/messages/message_automaton.dart @@ -0,0 +1,273 @@ +import 'dart:math'; + +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +enum TextFormattingType { + bold, + italic, + lineThrough, + underline, + pattern; + + TextStyle apply(TextStyle base, {TextStyle? pattern}) { + switch (this) { + case TextFormattingType.bold: + return base.copyWith(fontWeight: FontWeight.bold); + case TextFormattingType.italic: + return base.copyWith(fontStyle: FontStyle.italic); + case TextFormattingType.underline: + return base.copyWith( + decoration: TextDecoration.combine([base.decoration ?? TextDecoration.none, TextDecoration.underline]), + ); + case TextFormattingType.lineThrough: + return base.copyWith( + decoration: TextDecoration.combine([base.decoration ?? TextDecoration.none, TextDecoration.lineThrough]), + ); + case TextFormattingType.pattern: + return pattern ?? base; + } + } +} + +abstract class PatternAutomaton { + bool logging = false; + int _count = 0; + int _currentStart = 0; + bool _incremented = false; + List<(int, int, List)> _currentState = []; + + void run(int index, String prevChar, String char) { + // Evaluate the result of the automaton + var (pattern, valid, invalid, formatting) = evaluate(prevChar, char); + if (valid) { + _currentStart = _count; + } + if (invalid) { + _currentState.removeRange(_currentStart, _currentState.length); + _count = max(_currentState.length - 1, 0); + } + + // If the pattern is currently being scanned, set the formatting type to pattern + if (pattern) { + formatting = [TextFormattingType.pattern]; + } + + if (formatting.isEmpty) { + // Reset the current state to make sure it will start rendering again from the beginning + if (!_incremented && _currentState.isNotEmpty) { + _incremented = true; + _count++; + _currentStart = _count + 1; + } + if (logging) { + sendLog("$char | skip valid=$valid invalid=$invalid count=$_count"); + } + return; + } + _incremented = false; + + // Apply the current formatting + if (_currentState.length == _count) { + if (logging) { + sendLog("$char | start valid=$valid invalid=$invalid count=$_count"); + } + _currentState.add((index, index + 1, formatting)); + } else { + if (logging) { + sendLog(_count); + } + final (currStart, currEnd, currFmt) = _currentState[_count]; + + // If there is no new formatting, leave it be and add the current thing on top + if (listEquals(currFmt, formatting)) { + if (logging) { + sendLog("$char | add existing $valid $invalid $_count"); + } + + _currentState[_count] = (currStart, currEnd + 1, currFmt); + } else { + if (logging) { + sendLog("$char | add new $valid $invalid $_count"); + } + + // If there is new formatting, start a new range + _currentState.add((currEnd, currEnd + 1, formatting)); + _count += 1; + } + } + } + + List<(int, int, List)> getResult() { + return _currentState; + } + + /// Reset all of the state of the automaton. + void resetState() { + _currentState = []; + _count = 0; + _currentStart = 0; + _incremented = false; + } + + /// Evaluate an automaton for one char and the previous one. + /// + /// The first element is whether the element was matched by the automaton. + /// The second element is whether or not the entire pattern that was just matched is invalid. + /// The third element is whether a snapshot should be saved and the pattern was valid. + /// The fourth element are the currently active types of formatting. + (bool, bool, bool, List) evaluate(String prevChar, String char); +} + +class BoldItalicAutomaton extends PatternAutomaton { + int _stars = 0; + bool _inPattern = false; + List _current = []; + + @override + void resetState() { + _stars = 0; + _inPattern = false; + _current = []; + super.resetState(); + } + + @override + (bool, bool, bool, List) evaluate(String prevChar, String char) { + // Close as invalid in case of termination symbol + if (char == '' && _inPattern) { + return (false, false, true, []); + } + + // Check for star characters + if (char == '*') { + // If the previous char wasn't a star, we're changing modes + if (prevChar != "*") { + _inPattern = !_inPattern; + } + + // When we're inside the pattern, adjust the outputted formatting + if (_inPattern) { + _stars = min(_stars + 1, 3); + if (_stars == 1) { + _current = [TextFormattingType.italic]; + } else if (_stars == 2) { + _current = [TextFormattingType.bold]; + } else if (_stars == 3) { + _current = [TextFormattingType.bold, TextFormattingType.italic]; + } + } else { + _stars--; + _current = []; + } + + return (true, false, false, _current); + } else { + // The pattern is invalid we're outside and the right amount of stars weren't escaped + if (!_inPattern) { + final invalid = _stars != 0; + _stars = 0; + return (false, !invalid, invalid, _current); // Only return valid when there are no stars left + } + + // The pattern is valid as long as nothing happens + return (false, false, false, _current); + } + } +} + +class StrikethroughAutomaton extends PatternAutomaton { + int _squiggles = 0; + bool _inPattern = false; + + @override + void resetState() { + _squiggles = 0; + _inPattern = false; + super.resetState(); + } + + @override + (bool, bool, bool, List) evaluate(String prevChar, String char) { + // Close as invalid in case of termination symbol + if (char == '' && _inPattern) { + return (false, false, true, []); + } + + // Check for squiggle characters + if (char == '~') { + // If the previous char wasn't a squiggle, we're changing modes + if (prevChar != "~") { + _inPattern = !_inPattern; + } + + // When we're inside the pattern, adjust the outputted formatting + if (_inPattern) { + _squiggles = min(_squiggles + 1, 2); + return (true, false, false, [TextFormattingType.lineThrough]); + } else { + _squiggles--; + } + + return (true, false, false, [TextFormattingType.lineThrough]); + } else { + // The pattern is invalid we're outside and the right amount of squiggles weren't escaped + if (!_inPattern) { + final invalid = _squiggles != 0; + _squiggles = 0; + return (false, !invalid, invalid, []); // Only return valid when there are no squiggles left + } + + // The pattern is valid as long as nothing happens + return (false, false, false, [TextFormattingType.lineThrough]); + } + } +} + +class UnderlineAutomaton extends PatternAutomaton { + int _underscores = 0; + bool _inPattern = false; + + @override + void resetState() { + _underscores = 0; + _inPattern = false; + super.resetState(); + } + + @override + (bool, bool, bool, List) evaluate(String prevChar, String char) { + // Close as invalid in case of termination symbol + if (char == '' && _inPattern) { + return (false, false, true, []); + } + + // Check for underscore characters + if (char == "_") { + // If the previous char wasn't an underscore, we're changing modes + if (prevChar != "_") { + _inPattern = !_inPattern; + } + + if (_inPattern) { + _underscores = min(_underscores + 1, 2); + return (true, false, false, [TextFormattingType.underline]); + } else { + _underscores--; + } + + return (true, false, false, [TextFormattingType.underline]); + } else { + // The pattern is invalid we're outside and the right amount of underscores weren't escaped + if (!_inPattern) { + final invalid = _underscores != 0; + _underscores = 0; + return (false, !invalid, invalid, []); // Only return valid when there are no underscores left + } + + // The pattern is valid as long as nothing happens + return (false, false, false, [TextFormattingType.underline]); + } + } +} diff --git a/lib/pages/chat/messages/message_formatter.dart b/lib/pages/chat/messages/message_formatter.dart index e84818dc..1a670ad4 100644 --- a/lib/pages/chat/messages/message_formatter.dart +++ b/lib/pages/chat/messages/message_formatter.dart @@ -1,152 +1,171 @@ -import 'package:flutter/gestures.dart'; +import 'dart:math'; + +import 'package:chat_interface/pages/chat/messages/message_automaton.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -class MessageFormatter { - final TextStyle normalStyle; - final TextStyle? formattedStyle; +class TextEvaluator { + final automatons = [BoldItalicAutomaton(), StrikethroughAutomaton(), UnderlineAutomaton()]; + + /// Evaluate a text with a start style of [startStyle]. + /// Optionally provide a text style for the formatting patterns. + /// + /// Returns a list of text spans that are formatted properly. + List evaluate(String text, TextStyle startStyle, {TextStyle? pattern, bool skipPatterns = false}) { + // Reset the state of the automatons + for (var automaton in automatons) { + /* + // Add logging to any automaton like this (in case tests fail or sth) + if (automaton is BoldItalicAutomaton) { + automaton.logging = true; + } + */ - MessageFormatter(this.normalStyle, this.formattedStyle); + automaton.resetState(); + } - final RegExp emojiRegex = RegExp(r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])'); - - List _parseWithEmojis(String text, TextStyle style) { - final emojiStyle = style.copyWith( - /* fontFamily: "Emoji", */ - ); - final linkStyle = style.copyWith(color: Get.theme.colorScheme.onPrimary); - final linkRegex = RegExp(r'(https?://[^\s]+)'); - - var current = TextSpan(style: style); - var textSpans = []; - var lastIndex = 0; - - for (var match in linkRegex.allMatches(text)) { - // Handle text before the link - for (var char in text.substring(lastIndex, match.start).characters) { - if (emojiRegex.hasMatch(char)) { - textSpans.add(current); - textSpans.add(TextSpan(text: char, style: emojiStyle)); - current = TextSpan(text: "", style: style); - } else { - current = TextSpan(text: (current.text ?? "") + char, style: current.style); - } + // Run all the automatons + var prevChar = ""; + for (int i = 0; i < text.characters.length; i++) { + final char = text.characters.elementAt(i); + for (var automaton in automatons) { + automaton.run(i, prevChar, char); } + prevChar = char; + } + for (var automaton in automatons) { + automaton.run(text.length, prevChar, ""); + } - // Add the link with linkStyle - if (current.text != null && current.text!.isNotEmpty) { - textSpans.add(current); + // Collect all ranges from automatons + List<(int, int, List)> ranges = []; + for (var automaton in automatons) { + if (ranges.isEmpty) { + // If nothing is there yet, add all the ranges from the automaton (shouldn't have overlaps) + for (var (start, end, formatting) in automaton.getResult()) { + // Only add if it's a valid range + if (end >= start) { + ranges.add((start, end, formatting)); + } + } + } else { + // Merge all of the ranges into it + for (var range in automaton.getResult()) { + ranges = mergeRanges(range, ranges); + } } - textSpans.add(TextSpan( - text: match.group(0), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrlString(match.group(0)!); - }, - style: linkStyle, - )); - current = TextSpan(text: "", style: style); - - lastIndex = match.end; } - // Handle the rest of the text after the last link - for (var char in text.substring(lastIndex).characters) { - if (emojiRegex.hasMatch(char)) { - textSpans.add(current); - textSpans.add(TextSpan(text: char, style: emojiStyle)); - current = TextSpan(text: "", style: style); - } else { - current = TextSpan(text: (current.text ?? "") + char, style: current.style); + // Create the text spans from the ranges + List spans = []; + int lastEnd = 0; + for (var (start, end, formattings) in ranges) { + // Add everything before the range in case necessary + if (start != lastEnd) { + spans.add(TextSpan(text: text.substring(lastEnd, start), style: startStyle)); + } + + // Build the formatting for the range + bool skip = false; + TextStyle style = startStyle; + for (var format in formattings) { + if (format == TextFormattingType.pattern && skipPatterns) { + skip = true; + break; + } + style = format.apply(style, pattern: pattern); } + + // Add the range itself + if (!skip) { + spans.add(TextSpan(text: text.substring(start, min(end, text.length)), style: style)); + } + lastEnd = end; } - if (current.text != null && current.text!.isNotEmpty) { - textSpans.add(current); + // Add the rest of the text (in case necessary) + if (lastEnd < text.length) { + spans.add(TextSpan(text: text.substring(lastEnd, text.length), style: startStyle)); } - return textSpans; + return spans; } - TextSpan build(String text) { - // Parse the text into smaller spans that include the respective text styles - final parsedText = []; - - final pattern = RegExp("\\*\\*\\*(.*?)\\*\\*\\*|\\*\\*(.*?)\\*\\*|\\*(.*?)\\*|~~(.*?)~~"); - var currentStart = 0; - for (var match in pattern.allMatches(text)) { - if (match.start > currentStart) { - parsedText.addAll(_parseWithEmojis(text.substring(currentStart, match.start), normalStyle)); + /// Merge a range with text formatting ([toAdd]) into a non-overlapping set of base ([ranges]) ranges. + /// + /// Returns the merged ranges (also non-overlapping). + List<(int, int, List)> mergeRanges( + (int, int, List) toAdd, + List<(int, int, List)> ranges, + ) { + List<(int, int, List)> merged = []; + + var (start, end, formatting) = toAdd; + for (var (mStart, mEnd, mFormatting) in ranges) { + // Add the rest if current range is already past it + if (start < end && mStart > end && start < end) { + merged.add((start, end, formatting)); + start = end; } - final matchedString = text.substring(match.start, match.end); - currentStart = match.end; - - // Check for the respective patterns and apply text styles - if (matchedString.startsWith("***") && matchedString.length != 3) { - // Bold and italic - if (formattedStyle != null) { - parsedText.add(TextSpan(text: text.substring(match.start, match.start + 3), style: formattedStyle)); - } - parsedText.addAll(_parseWithEmojis( - text.substring(match.start + 3, match.end - 3), - normalStyle.copyWith( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.italic, - ), - )); - if (formattedStyle != null) { - parsedText.add(TextSpan(text: text.substring(match.end - 3, match.end), style: formattedStyle)); - } - } else if (matchedString.startsWith("**") && matchedString.length != 2) { - // Bold - if (formattedStyle != null) { - parsedText.add(TextSpan(text: text.substring(match.start, match.start + 2), style: formattedStyle)); - } - parsedText.addAll(_parseWithEmojis( - text.substring(match.start + 2, match.end - 2), - normalStyle.copyWith( - fontWeight: FontWeight.bold, - ), - )); - if (formattedStyle != null) { - parsedText.add(TextSpan(text: text.substring(match.end - 2, match.end), style: formattedStyle)); - } - } else if (matchedString.startsWith("*")) { - // Italic - if (formattedStyle != null) { - parsedText.add(TextSpan(text: text.substring(match.start, match.start + 1), style: formattedStyle)); - } - parsedText.addAll(_parseWithEmojis( - text.substring(match.start + 1, match.end - 1), - normalStyle.copyWith( - fontStyle: FontStyle.italic, - ), - )); - if (formattedStyle != null) { - parsedText.add(TextSpan(text: text.substring(match.end - 1, match.end), style: formattedStyle)); + + // Check if they are overlapping + if (mEnd < start || mStart > end || start >= end) { + merged.add((mStart, mEnd, mFormatting)); + continue; + } + + if (start <= mStart) { + if (start != mStart) { + merged.add((start, mStart, formatting)); } - } else if (matchedString.startsWith("~~")) { - // Stroke - if (formattedStyle != null) { - parsedText.add(TextSpan(text: text.substring(match.start, match.start + 2), style: formattedStyle)); + if (end <= mEnd) { + if (mStart != end) { + merged.add((mStart, end, [...mFormatting, ...formatting])); + } + if (end != mEnd) { + merged.add((end, mEnd, mFormatting)); + } + } else if (end > mEnd) { + merged.add((mStart, mEnd, [...mFormatting, ...formatting])); + } else { + merged.add((mStart, mEnd, [...mFormatting, ...formatting])); } - parsedText.addAll(_parseWithEmojis( - text.substring(match.start + 2, match.end - 2), - normalStyle.copyWith( - decoration: TextDecoration.lineThrough, - ), - )); - if (formattedStyle != null) { - parsedText.add(TextSpan(text: text.substring(match.end - 2, match.end), style: formattedStyle)); + } else { + // start > mStart (already enforced cause if) + merged.add((mStart, start, mFormatting)); + + if (end < mEnd) { + merged.add((start, end, [...mFormatting, ...formatting])); + merged.add((end, mEnd, mFormatting)); + } else if (end >= mEnd && start != mEnd) { + merged.add((start, mEnd, [...mFormatting, ...formatting])); } } + + start = mEnd; } - parsedText.addAll(_parseWithEmojis(text.substring(currentStart, text.length), normalStyle)); - // Parse links + // Add rest, if not added by merge operation + if (start < end) { + merged.add((start, end, formatting)); + } - return TextSpan(children: parsedText, style: normalStyle); + return merged; + } +} + +class MessageFormatter { + final TextStyle normalStyle; + final TextStyle? formattedStyle; + final evaluator = TextEvaluator(); + + MessageFormatter(this.normalStyle, this.formattedStyle); + + /// Build a text span from a text by parsing the formatting patterns in it. + TextSpan build(String text) { + return TextSpan( + children: evaluator.evaluate(text, normalStyle, pattern: formattedStyle, skipPatterns: formattedStyle == null), + style: normalStyle, + ); } } @@ -165,22 +184,34 @@ class FormattedTextEditingController extends TextEditingController { } /// Widget to display formatting directives -class FormattedText extends StatelessWidget { +class FormattedText extends StatefulWidget { final String text; final TextStyle baseStyle; final TextStyle? formatStyle; - const FormattedText({ - super.key, - required this.text, - required this.baseStyle, - this.formatStyle, - }); + const FormattedText({super.key, required this.text, required this.baseStyle, this.formatStyle}); @override - Widget build(BuildContext context) { - final MessageFormatter formatter = MessageFormatter(baseStyle, formatStyle); + State createState() => _FormattedTextState(); +} - return Text.rich(formatter.build(text)); +class _FormattedTextState extends State { + late TextSpan formatted; + + @override + void initState() { + formatted = MessageFormatter(widget.baseStyle, widget.formatStyle).build(widget.text); + super.initState(); + } + + @override + void didUpdateWidget(covariant FormattedText oldWidget) { + formatted = MessageFormatter(widget.baseStyle, widget.formatStyle).build(widget.text); + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return Text.rich(formatted); } } diff --git a/lib/pages/chat/messages/message_input.dart b/lib/pages/chat/messages/message_input.dart index f1800a35..3268d2ac 100644 --- a/lib/pages/chat/messages/message_input.dart +++ b/lib/pages/chat/messages/message_input.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; @@ -21,6 +21,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'package:pasteboard/pasteboard.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:unicode_emojis/unicode_emojis.dart'; import '../../../theme/components/forms/icon_button.dart'; @@ -47,13 +48,17 @@ class MessageInput extends StatefulWidget { } class _MessageInputState extends State { - final FormattedTextEditingController _message = FormattedTextEditingController(Get.theme.textTheme.labelLarge!, Get.theme.textTheme.bodyLarge!); - final loading = false.obs; + final FormattedTextEditingController _message = FormattedTextEditingController( + Get.theme.textTheme.labelLarge!, + Get.theme.textTheme.bodyLarge!, + ); + final _loading = signal(false); final FocusNode _inputFocus = FocusNode(); StreamSubscription? _sub; final GlobalKey _libraryKey = GlobalKey(); // final GlobalKey _emojiKey = GlobalKey(); - final _emojiSuggestions = [].obs; + final _emojiSuggestions = listSignal([]); + final _emojiRegex = RegExp(":(.*?)\\s|:(.*\$)|"); // For a little hack to prevent the answers from disappearing instantly LPHAddress? _previousAccount; @@ -62,7 +67,9 @@ class _MessageInputState extends State { void dispose() { _message.dispose(); _sub?.cancel(); + _loading.dispose(); _inputFocus.dispose(); + _emojiSuggestions.dispose(); super.dispose(); } @@ -86,15 +93,14 @@ class _MessageInputState extends State { _emojiSuggestions.clear(); // Search for emojis - final regex = RegExp(":(.*?)\\s|:(.*\$)|"); final cursorPos = _message.selection.start; - for (var match in regex.allMatches(_message.text)) { + for (var match in _emojiRegex.allMatches(_message.text)) { // Check if the cursor is inside of the current emoji if (match.start < cursorPos && match.end >= cursorPos) { final query = _message.text.substring(match.start + 1, cursorPos); if (query.length >= 2) { sendLog("current emoji query: $query"); - _emojiSuggestions.value = UnicodeEmojis.search(query, limit: 20); + _emojiSuggestions.value = UnicodeEmojis.search(query, limit: 10); } } } @@ -114,51 +120,51 @@ class _MessageInputState extends State { void resetCurrentDraft() { if (MessageSendHelper.currentDraft.value != null) { - MessageSendHelper.drafts[MessageSendHelper.currentDraft.value!.target] = MessageDraft(MessageSendHelper.currentDraft.value!.target, ""); + MessageSendHelper.drafts[MessageSendHelper.currentDraft.value!.target] = MessageDraft( + MessageSendHelper.currentDraft.value!.target, + "", + ); MessageSendHelper.currentDraft.value = MessageDraft(MessageSendHelper.currentDraft.value!.target, ""); _message.clear(); } - loading.value = false; + _loading.value = false; } /// Replace the current selection with a new text void replaceSelection(String replacer) { // Compute the new offset before the text is changed final beforeLeft = - _message.selection.baseOffset > _message.selection.extentOffset ? _message.selection.baseOffset : _message.selection.extentOffset; + _message.selection.baseOffset > _message.selection.extentOffset + ? _message.selection.baseOffset + : _message.selection.extentOffset; final newOffset = beforeLeft - (_message.selection.end - _message.selection.start) + replacer.length; // Change the text in the field to include the pasted text _message.text = - _message.text.substring(0, _message.selection.start) + replacer + _message.text.substring(_message.selection.end, _message.text.length); + _message.text.substring(0, _message.selection.start) + + replacer + + _message.text.substring(_message.selection.end, _message.text.length); // Change the selection to the calculated offset - _message.selection = _message.selection.copyWith( - baseOffset: newOffset, - extentOffset: newOffset, - ); + _message.selection = _message.selection.copyWith(baseOffset: newOffset, extentOffset: newOffset); } /// Replace the emoji selector in the input with an emoji void doEmojiSuggestion(String emoji) { // Search for emojis - final regex = RegExp(":(.*?)\\s|:(.*\$)|"); final cursorPos = _message.selection.start; - for (var match in regex.allMatches(_message.text)) { + for (var match in _emojiRegex.allMatches(_message.text)) { // Check if the cursor is inside of the current emoji if (match.start < cursorPos && match.end >= cursorPos) { final query = _message.text.substring(match.start + 1, cursorPos); if (query.length >= 2) { - _emojiSuggestions.value = UnicodeEmojis.search(query, limit: 20); - _message.text = "${_message.text.substring(0, match.start)}$emoji ${_message.text.substring(cursorPos, _message.text.length)}"; + _message.text = + "${_message.text.substring(0, match.start)}$emoji ${_message.text.substring(cursorPos, _message.text.length)}"; _emojiSuggestions.clear(); _inputFocus.requestFocus(); // Change the selection to the calculated offset - final newOffset = cursorPos - query.length + 3; - _message.selection = _message.selection.copyWith( - baseOffset: newOffset, - extentOffset: newOffset, - ); + final newOffset = cursorPos - query.length + 2; + _message.selection = _message.selection.copyWith(baseOffset: newOffset, extentOffset: newOffset); } } } @@ -173,7 +179,7 @@ class _MessageInputState extends State { SendIntent: CallbackAction( onInvoke: (SendIntent intent) async { // Check if there is a connection before doing this - if (!Get.find().connected.value) { + if (!ConnectionController.connected.value) { showErrorPopup("error", "error.no_connection".tr); return; } @@ -187,7 +193,7 @@ class _MessageInputState extends State { // Send a regular text message if there are no files to attach if (MessageSendHelper.currentDraft.value!.files.isEmpty) { final error = await widget.provider.sendMessage( - loading, + _loading, MessageType.text, [], _message.text, @@ -207,7 +213,7 @@ class _MessageInputState extends State { // Send a regular text message with files final error = await widget.provider.sendTextMessageWithFiles( - loading, + _loading, _message.text, MessageSendHelper.currentDraft.value!.files, MessageSendHelper.currentDraft.value!.answer.value?.id ?? "", @@ -261,9 +267,10 @@ class _MessageInputState extends State { }; // Build actual widget - final double padding = widget.rectangle - ? 0 - : isMobileMode() + final double padding = + widget.rectangle + ? 0 + : isMobileMode() ? defaultSpacing : sectionSpacing; return Padding( @@ -278,142 +285,129 @@ class _MessageInputState extends State { color: widget.secondary ? theme.colorScheme.inverseSurface : theme.colorScheme.onInverseSurface, borderRadius: BorderRadius.circular(defaultSpacing * (widget.rectangle ? 0 : 1.5)), child: Padding( - padding: EdgeInsets.symmetric( - horizontal: defaultSpacing, - vertical: elementSpacing, - ), + padding: EdgeInsets.symmetric(horizontal: defaultSpacing, vertical: elementSpacing), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ //* Reply preview - Obx( - () { - final answer = MessageSendHelper.currentDraft.value?.answer.value; - if (answer != null) { - _previousAccount = answer.senderAddress; - } - - return Animate( - effects: [ - ExpandEffect( - duration: 300.ms, - curve: Curves.easeInOut, - axis: Axis.vertical, - alignment: Alignment.center, - ), - FadeEffect( - duration: 300.ms, - ) - ], - target: MessageSendHelper.currentDraft.value == null || answer == null ? 0 : 1, - child: Padding( - padding: const EdgeInsets.all(elementSpacing), - child: Row( - children: [ - Icon(Icons.reply, color: theme.colorScheme.tertiary), - horizontalSpacing(defaultSpacing), - Expanded( - child: Text( - "message.reply.text".trParams({ - "name": _previousAccount == null - ? "tf" - : Get.find().friends[_previousAccount]?.name ?? Friend.unknown(_previousAccount!).name, - }), - style: theme.textTheme.labelMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + Watch((ctx) { + final answer = MessageSendHelper.currentDraft.value?.answer.value; + if (answer != null) { + _previousAccount = answer.senderAddress; + } + + return Animate( + effects: [ + ExpandEffect( + duration: 300.ms, + curve: Curves.easeInOut, + axis: Axis.vertical, + alignment: Alignment.center, + ), + FadeEffect(duration: 300.ms), + ], + target: MessageSendHelper.currentDraft.value == null || answer == null ? 0 : 1, + child: Padding( + padding: const EdgeInsets.all(elementSpacing), + child: Row( + children: [ + Icon(Icons.reply, color: theme.colorScheme.tertiary), + horizontalSpacing(defaultSpacing), + Expanded( + child: Text( + "message.reply.text".trParams({ + "name": + _previousAccount == null + ? "tf" + : FriendController.friends[_previousAccount]?.name ?? + Friend.unknown(_previousAccount!).name, + }), + style: theme.textTheme.labelMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - LoadingIconButton( - iconSize: 22, - extra: 4, - padding: 4, - onTap: () { - MessageSendHelper.currentDraft.value!.answer.value = null; - }, - icon: Icons.close, - ) - ], - ), + ), + LoadingIconButton( + iconSize: 22, + extra: 4, + padding: 4, + onTap: () { + MessageSendHelper.currentDraft.value!.answer.value = null; + }, + icon: Icons.close, + ), + ], ), - ); - }, - ), + ), + ); + }), //* Emoji suggestions - Obx( - () { - if (_emojiSuggestions.isEmpty) { - return const SizedBox(); - } - return Padding( - padding: const EdgeInsets.all(elementSpacing), - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (var emoji in _emojiSuggestions) - Padding( - padding: const EdgeInsets.only(right: elementSpacing), - child: Tooltip( - key: ValueKey(emoji.shortName), - exitDuration: 0.ms, - message: ":${emoji.shortName}:", - child: Center( - child: InkWell( - borderRadius: BorderRadius.circular(1000), - onTap: () { - doEmojiSuggestion(emoji.emoji); - }, - child: Text( - emoji.emoji, - style: Get.theme.textTheme.titleLarge!.copyWith(/* fontFamily: "Emoji", */ fontSize: 30), + Watch((ctx) { + if (_emojiSuggestions.isEmpty) { + return const SizedBox(); + } + return Padding( + padding: const EdgeInsets.all(elementSpacing), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (var emoji in _emojiSuggestions) + Padding( + padding: const EdgeInsets.only(right: elementSpacing), + child: Tooltip( + key: ValueKey(emoji.shortName), + exitDuration: 0.ms, + message: ":${emoji.shortName}:", + child: Center( + child: InkWell( + borderRadius: BorderRadius.circular(1000), + onTap: () { + doEmojiSuggestion(emoji.emoji); + }, + child: Text( + emoji.emoji, + style: Get.theme.textTheme.titleLarge!.copyWith( + /* fontFamily: "Emoji", */ fontSize: 30, ), ), ), ), ), - ], - ), + ), + ], ), ), - ); - }, - ), + ), + ); + }), //* File preview - Obx( - () { - if (MessageSendHelper.currentDraft.value == null) { - return const SizedBox(); - } - return Animate( - effects: [ - ExpandEffect( - duration: 250.ms, - curve: Curves.easeInOut, - axis: Axis.vertical, - ) - ], - target: MessageSendHelper.currentDraft.value!.files.isEmpty ? 0 : 1, - child: Padding( - padding: const EdgeInsets.only(bottom: defaultSpacing * 0.5), - child: Row( - children: [ - const SizedBox(height: 200 + defaultSpacing), - for (final file in MessageSendHelper.currentDraft.value!.files) - SquareFileRenderer( - file: file, - onRemove: () => MessageSendHelper.currentDraft.value!.files.remove(file), - ), - ], - ), + Watch((ctx) { + if (MessageSendHelper.currentDraft.value == null) { + return const SizedBox(); + } + return Animate( + effects: [ExpandEffect(duration: 250.ms, curve: Curves.easeInOut, axis: Axis.vertical)], + target: MessageSendHelper.currentDraft.value!.files.isEmpty ? 0 : 1, + child: Padding( + padding: const EdgeInsets.only(bottom: defaultSpacing * 0.5), + child: Row( + children: [ + const SizedBox(height: 200 + defaultSpacing), + for (final file in MessageSendHelper.currentDraft.value!.files) + SquareFileRenderer( + file: file, + onRemove: () => MessageSendHelper.currentDraft.value!.files.remove(file), + ), + ], ), - ); - }, - ), + ), + ); + }), //* Input Row( @@ -459,9 +453,7 @@ class _MessageInputState extends State { hintText: 'chat.message'.tr, hintStyle: theme.textTheme.bodyLarge, ), - inputFormatters: [ - LengthLimitingTextInputFormatter(1000), - ], + inputFormatters: [LengthLimitingTextInputFormatter(1000)], focusNode: _inputFocus, onChanged: (value) { MessageSendHelper.currentDraft.value!.message = value; @@ -483,12 +475,13 @@ class _MessageInputState extends State { ), IconButton( key: _libraryKey, - onPressed: () => showModal( - LibraryWindow( - data: ContextMenuData.fromKey(_libraryKey, above: true, right: true), - provider: widget.provider, - ), - ), + onPressed: + () => showModal( + LibraryWindow( + data: ContextMenuData.fromKey(_libraryKey, above: true, right: true), + provider: widget.provider, + ), + ), icon: const Icon(Icons.folder), color: theme.colorScheme.tertiary, ), @@ -503,7 +496,7 @@ class _MessageInputState extends State { }, icon: Icons.send, color: theme.colorScheme.tertiary, - loading: loading, + loading: _loading, ), ), ], diff --git a/lib/pages/chat/messages_page.dart b/lib/pages/chat/messages_page.dart index 559339aa..78f2cbef 100644 --- a/lib/pages/chat/messages_page.dart +++ b/lib/pages/chat/messages_page.dart @@ -2,6 +2,7 @@ import 'package:chat_interface/controller/conversation/message_controller.dart'; import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; import 'package:chat_interface/pages/chat/components/conversations/message_bar_mobile.dart'; import 'package:chat_interface/pages/chat/components/message/message_feed.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; import 'package:chat_interface/util/platform_callback.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; @@ -25,7 +26,7 @@ class _MessagesPageMobileState extends State { desktop: () { Get.back(); Get.off(const ChatPageDesktop()); - Get.find().selectConversation(widget.provider.conversation); + MessageController.openConversation(widget.provider.conversation); }, child: Scaffold( backgroundColor: theme.colorScheme.inverseSurface, @@ -35,15 +36,11 @@ class _MessagesPageMobileState extends State { DevicePadding( top: true, padding: const EdgeInsets.all(0), - child: MobileMessageBar(conversation: Get.find().currentProvider.value!.conversation), + child: MobileMessageBar(conversation: widget.provider.conversation), ), // Render the actual message feed - Expanded( - child: MessageFeed( - rectInput: true, - ), - ), + Expanded(child: MessageFeed(rectInput: true)), ], ), ), diff --git a/lib/pages/chat/sidebar/friends/friend_add_window.dart b/lib/pages/chat/sidebar/friends/friend_add_window.dart index d95f050c..4bdb2e60 100644 --- a/lib/pages/chat/sidebar/friends/friend_add_window.dart +++ b/lib/pages/chat/sidebar/friends/friend_add_window.dart @@ -1,11 +1,12 @@ -import 'package:chat_interface/controller/account/friends/requests_controller.dart'; -import 'package:chat_interface/controller/current/tasks/friend_sync_task.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/account/requests_controller.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class FriendAddWindow extends StatefulWidget { const FriendAddWindow({super.key}); @@ -24,7 +25,7 @@ class _FriendAddWindowState extends State { } void sendRequest() { - if (requestsLoading.value || friendsVaultRefreshing.value) { + if (RequestController.requestsLoading.value || FriendsVault.friendsVaultRefreshing.value) { return; } newFriendRequest(_name.text, (message) { @@ -35,18 +36,12 @@ class _FriendAddWindowState extends State { @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text("friends.add".tr, style: Get.textTheme.labelLarge), - ], + title: [Text("friends.add".tr, style: Get.textTheme.labelLarge)], child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - "friends.add.desc".tr, - style: Get.theme.textTheme.bodyMedium, - textAlign: TextAlign.start, - ), + Text("friends.add.desc".tr, style: Get.theme.textTheme.bodyMedium, textAlign: TextAlign.start), verticalSpacing(defaultSpacing), FJTextField( controller: _name, @@ -55,11 +50,13 @@ class _FriendAddWindowState extends State { onSubmitted: (t) => sendRequest(), ), verticalSpacing(defaultSpacing), - Obx( - () => FJElevatedLoadingButton( + Watch( + (ctx) => FJElevatedLoadingButton( onTap: () => sendRequest(), label: 'friends.add.button'.tr, - loading: (requestsLoading.value || friendsVaultRefreshing.value).obs, + loading: computed( + () => RequestController.requestsLoading.value || FriendsVault.friendsVaultRefreshing.value, + ), ), ), ], diff --git a/lib/pages/chat/sidebar/friends/friend_button.dart b/lib/pages/chat/sidebar/friends/friend_button.dart index 364c2199..a3d2e49d 100644 --- a/lib/pages/chat/sidebar/friends/friend_button.dart +++ b/lib/pages/chat/sidebar/friends/friend_button.dart @@ -1,16 +1,18 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/profile.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class FriendButton extends StatefulWidget { - final Rx position; + final Signal position; final Friend friend; const FriendButton({super.key, required this.friend, required this.position}); @@ -31,74 +33,76 @@ class _FriendButtonState extends State { borderRadius: BorderRadius.circular(10), //* Show profile - onTap: () => showModal(Profile(position: widget.position.value, friend: widget.friend)), + onTap: + () => + showModal(Profile(data: ContextMenuData.fromPosition(widget.position.value), friend: widget.friend)), //* Friend info child: Padding( padding: const EdgeInsets.symmetric(horizontal: defaultSpacing, vertical: defaultSpacing * 0.5), - child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Icon(Icons.person, size: 30, color: Theme.of(context).colorScheme.onPrimary), - horizontalSpacing(defaultSpacing), - Text( - widget.friend.displayName.value, - style: Get.theme.textTheme.labelMedium, - ), - if (widget.friend.id.server != basePath) - Padding( - padding: const EdgeInsets.only(left: defaultSpacing), - child: Tooltip( - message: "friends.different_town".trParams({ - "town": widget.friend.id.server, - }), - child: Icon(Icons.sensors, color: Get.theme.colorScheme.onPrimary), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(Icons.person, size: 30, color: Theme.of(context).colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Text(widget.friend.displayName.value, style: Get.theme.textTheme.labelMedium), + if (widget.friend.id.server != basePath) + Padding( + padding: const EdgeInsets.only(left: defaultSpacing), + child: Tooltip( + message: "friends.different_town".trParams({"town": widget.friend.id.server}), + child: Icon(Icons.sensors, color: Get.theme.colorScheme.onPrimary), + ), ), - ), - ], - ), + ], + ), - //* Friend actions - Builder(builder: (context) { - if (Get.find().inSpace.value) { + // All the things that can be done with the friend + Watch((context) { + // If connected to a Space, add a button to send them an invite + if (SpaceController.connected.value) { + return IconButton( + icon: Icon(Icons.forward_to_inbox, color: Theme.of(context).colorScheme.onPrimary), + onPressed: () { + // Check if there even is a conversation with the guy + final conversation = ConversationController.conversations.values.toList().firstWhereOrNull( + (c) => c.members.values.any((m) => m.address == widget.friend.id), + ); + if (conversation == null) { + showErrorPopup("error", "profile.conversation_not_found".tr); + return; + } + + // Invite the user to the current space + SpaceController.inviteToCall(ConversationMessageProvider(conversation)); + Get.back(); + }, + ); + } + + // Otherwise just add a call button return IconButton( - icon: Icon(Icons.forward_to_inbox, color: Theme.of(context).colorScheme.onPrimary), + icon: Icon(Icons.rocket_launch, color: Theme.of(context).colorScheme.onPrimary), onPressed: () { // Check if there even is a conversation with the guy - final conversation = Get.find().conversations.values.toList().firstWhereOrNull( - (c) => c.members.values.any((m) => m.address == widget.friend.id), - ); + final conversation = ConversationController.conversations.values.toList().firstWhereOrNull( + (c) => c.members.values.any((m) => m.address == widget.friend.id), + ); if (conversation == null) { showErrorPopup("error", "profile.conversation_not_found".tr); return; } // Invite the user to the current space - Get.find().inviteToCall(ConversationMessageProvider(conversation)); + SpaceController.createAndConnect(ConversationMessageProvider(conversation)); Get.back(); }, ); - } - - return IconButton( - icon: Icon(Icons.call, color: Theme.of(context).colorScheme.onPrimary), - onPressed: () { - // Check if there even is a conversation with the guy - final conversation = Get.find().conversations.values.toList().firstWhereOrNull( - (c) => c.members.values.any((m) => m.address == widget.friend.id), - ); - if (conversation == null) { - showErrorPopup("error", "profile.conversation_not_found".tr); - return; - } - - // Invite the user to the current space - Get.find().createAndConnect(ConversationMessageProvider(conversation)); - Get.back(); - }, - ); - }), - ]), + }), + ], + ), ), ), ), diff --git a/lib/pages/chat/sidebar/friends/friends_page.dart b/lib/pages/chat/sidebar/friends/friends_page.dart index 338abb12..01bcd06e 100644 --- a/lib/pages/chat/sidebar/friends/friends_page.dart +++ b/lib/pages/chat/sidebar/friends/friends_page.dart @@ -1,10 +1,9 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/account/friends/requests_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/account/requests_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/chat/sidebar/friends/friend_add_window.dart'; import 'package:chat_interface/pages/chat/sidebar/friends/friend_button.dart'; import 'package:chat_interface/pages/chat/sidebar/friends/request_button.dart'; -import 'package:chat_interface/controller/current/tasks/friend_sync_task.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/theme/ui/containers/success_container.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; @@ -12,6 +11,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class FriendsPage extends StatefulWidget { const FriendsPage({super.key}); @@ -21,28 +21,28 @@ class FriendsPage extends StatefulWidget { } class _FriendsPageState extends State { - final position = const Offset(0, 0).obs; - final query = "".obs; - final loading = false.obs; - final revealSuccess = false.obs; + final _position = signal(Offset(0, 0)); + final _query = signal(""); + final _revealSuccess = signal(false); + + @override + void dispose() { + _revealSuccess.dispose(); + _position.dispose(); + _query.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text( - "friends".tr, - style: Get.theme.textTheme.labelLarge, - ), - ], + title: [Text("friends".tr, style: Get.theme.textTheme.labelLarge)], showTitleDesktop: false, maxWidth: 500, mobileSheet: false, mobileFlat: true, child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 800, - ), + constraints: const BoxConstraints(maxHeight: 800), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -72,17 +72,17 @@ class _FriendsPageState extends State { hintText: "friends.placeholder".tr, ), onChanged: (value) { - query.value = value; + _query.value = value; }, onSubmitted: (value) => {}, // TODO: Think about what do with this cursorColor: Get.theme.colorScheme.onPrimary, ), ), LoadingIconButton( - loading: friendsVaultRefreshing, + loading: FriendsVault.friendsVaultRefreshing, onTap: () => showModal(const FriendAddWindow()), icon: Icons.person_add_alt_1, - ) + ), ], ), ), @@ -92,83 +92,58 @@ class _FriendsPageState extends State { //* Friends list Flexible( child: RepaintBoundary( - child: Obx(() { - final friendController = Get.find(); - final requestController = Get.find(); - + child: Watch((ctx) { //* Friends, requests, sent requests list return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Obx( - () => Animate( + Watch( + (ctx) => Animate( effects: [ - ExpandEffect( - curve: Curves.easeInOut, - duration: 250.ms, - axis: Axis.vertical, - ), - FadeEffect( - end: 0, - begin: 1, - duration: 250.ms, - ), + ExpandEffect(curve: Curves.easeInOut, duration: 250.ms, axis: Axis.vertical), + FadeEffect(end: 0, begin: 1, duration: 250.ms), ], - target: revealSuccess.value ? 1.0 : 0.0, + target: _revealSuccess.value ? 1.0 : 0.0, child: SuccessContainer(text: "request.sent".tr), ), ), - Obx(() { - final found = friendController.friends.values.any((friend) => - (friend.displayName.value.toLowerCase().contains(query.value.toLowerCase()) || - friend.name.toLowerCase().contains(query.value.toLowerCase())) && - friend.id != StatusController.ownAddress); + Watch((ctx) { + final found = FriendController.friends.values.any( + (friend) => + (friend.displayName.value.toLowerCase().contains(_query.value.toLowerCase()) || + friend.name.toLowerCase().contains(_query.value.toLowerCase())) && + friend.id != StatusController.ownAddress, + ); return Animate( - effects: [ - ExpandEffect( - curve: Curves.easeInOut, - duration: 250.ms, - axis: Axis.vertical, - ), - FadeEffect( - end: 1, - begin: 0, - duration: 250.ms, - ), - ], - target: found ? 0.0 : 1.0, - child: Padding( - padding: const EdgeInsets.only(top: defaultSpacing, left: defaultSpacing, right: defaultSpacing), - child: Center( - child: Text( - "friends.empty".tr, - style: Get.theme.textTheme.bodyMedium, - ), - ), - )); + effects: [ + ExpandEffect(curve: Curves.easeInOut, duration: 250.ms, axis: Axis.vertical), + FadeEffect(end: 1, begin: 0, duration: 250.ms), + ], + target: found ? 0.0 : 1.0, + child: Padding( + padding: const EdgeInsets.only( + top: defaultSpacing, + left: defaultSpacing, + right: defaultSpacing, + ), + child: Center(child: Text("friends.empty".tr, style: Get.theme.textTheme.bodyMedium)), + ), + ); }), //* Requests - Obx( - () => Animate( + Watch( + (ctx) => Animate( effects: [ - ReverseExpandEffect( - curve: Curves.easeInOut, - duration: 250.ms, - axis: Axis.vertical, - ), - FadeEffect( - end: 0, - begin: 1, - duration: 250.ms, - ), + ReverseExpandEffect(curve: Curves.easeInOut, duration: 250.ms, axis: Axis.vertical), + FadeEffect(end: 0, begin: 1, duration: 250.ms), ], - target: query.value.isEmpty ? 0.0 : 1.0, + target: _query.value.isEmpty ? 0.0 : 1.0, child: Visibility( - visible: requestController.requests.isNotEmpty, + visible: RequestController.requests.isNotEmpty, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, @@ -179,22 +154,24 @@ class _FriendsPageState extends State { verticalSpacing(elementSpacing), Builder( builder: (context) { - if (requestController.requests.isEmpty) { + if (RequestController.requests.isEmpty) { return const SizedBox.shrink(); } return Column( mainAxisSize: MainAxisSize.min, - children: List.generate(requestController.requests.length, (index) { - final request = requestController.requests.values.elementAt(index); + children: List.generate(RequestController.requests.length, (index) { + final request = RequestController.requests.values.elementAt(index); return RequestButton(request: request, self: false); }), ); }, ), Visibility( - visible: friendController.friends.length > 1 || requestController.requestsSent.isNotEmpty, + visible: + FriendController.friends.length > 1 || + RequestController.requestsSent.isNotEmpty, child: verticalSpacing(sectionSpacing - elementSpacing), - ) + ), ], ), ), @@ -202,23 +179,15 @@ class _FriendsPageState extends State { ), //* Sent requests - Obx( - () => Animate( + Watch( + (ctx) => Animate( effects: [ - ReverseExpandEffect( - curve: Curves.easeInOut, - duration: 250.ms, - axis: Axis.vertical, - ), - FadeEffect( - end: 0, - begin: 1, - duration: 250.ms, - ), + ReverseExpandEffect(curve: Curves.easeInOut, duration: 250.ms, axis: Axis.vertical), + FadeEffect(end: 0, begin: 1, duration: 250.ms), ], - target: query.value.isEmpty ? 0.0 : 1.0, + target: _query.value.isEmpty ? 0.0 : 1.0, child: Visibility( - visible: requestController.requestsSent.isNotEmpty, + visible: RequestController.requestsSent.isNotEmpty, child: Padding( padding: const EdgeInsets.only(top: sectionSpacing), child: Column( @@ -230,13 +199,13 @@ class _FriendsPageState extends State { verticalSpacing(elementSpacing), Builder( builder: (context) { - if (requestController.requestsSent.isEmpty) { + if (RequestController.requestsSent.isEmpty) { return const SizedBox.shrink(); } return Column( mainAxisSize: MainAxisSize.min, - children: List.generate(requestController.requestsSent.length, (index) { - final request = requestController.requestsSent.values.elementAt(index); + children: List.generate(RequestController.requestsSent.length, (index) { + final request = RequestController.requestsSent.values.elementAt(index); return Padding( padding: const EdgeInsets.only(bottom: elementSpacing), child: RequestButton(request: request, self: true), @@ -246,9 +215,9 @@ class _FriendsPageState extends State { }, ), Visibility( - visible: friendController.friends.length > 1, + visible: FriendController.friends.length > 1, child: verticalSpacing(sectionSpacing - elementSpacing), - ) + ), ], ), ), @@ -258,7 +227,7 @@ class _FriendsPageState extends State { //* Friends Visibility( - visible: friendController.friends.length > 1, + visible: FriendController.friends.length > 1, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, @@ -266,46 +235,43 @@ class _FriendsPageState extends State { children: [ Builder( builder: (context) { - if (friendController.friends.length <= 1) { + if (FriendController.friends.length <= 1) { return const SizedBox.shrink(); } return ListView.builder( shrinkWrap: true, - itemCount: friendController.friends.length, + itemCount: FriendController.friends.length, itemBuilder: (context, index) { - final friend = friendController.friends.values.elementAt(index); + final friend = FriendController.friends.values.elementAt(index); if (friend.unknown || friend.id == StatusController.ownAddress) { return const SizedBox(); } - return Obx( - () { - final visible = query.value.isEmpty || - friend.displayName.value.toLowerCase().contains(query.value.toLowerCase()) || - friend.name.toLowerCase().contains(query.value.toLowerCase()); + return Watch((ctx) { + final visible = + _query.value.isEmpty || + friend.displayName.value.toLowerCase().contains( + _query.value.toLowerCase(), + ) || + friend.name.toLowerCase().contains(_query.value.toLowerCase()); - return Animate( - effects: [ - ReverseExpandEffect( - curve: Curves.easeInOut, - duration: 250.ms, - alignment: Alignment.bottomCenter, - axis: Axis.vertical, - ), - FadeEffect( - end: 0, - begin: 1, - duration: 250.ms, - ), - ], - target: visible ? 0.0 : 1.0, - child: Padding( - padding: EdgeInsets.only(top: index == 0 ? defaultSpacing : elementSpacing), - child: FriendButton(friend: friend, position: position), + return Animate( + effects: [ + ReverseExpandEffect( + curve: Curves.easeInOut, + duration: 250.ms, + alignment: Alignment.bottomCenter, + axis: Axis.vertical, ), - ); - }, - ); + FadeEffect(end: 0, begin: 1, duration: 250.ms), + ], + target: visible ? 0.0 : 1.0, + child: Padding( + padding: EdgeInsets.only(top: index == 0 ? defaultSpacing : elementSpacing), + child: FriendButton(friend: friend, position: _position), + ), + ); + }); }, ); }, diff --git a/lib/pages/chat/sidebar/friends/request_button.dart b/lib/pages/chat/sidebar/friends/request_button.dart index d41b6f29..caa8a7e7 100644 --- a/lib/pages/chat/sidebar/friends/request_button.dart +++ b/lib/pages/chat/sidebar/friends/request_button.dart @@ -1,10 +1,12 @@ -import 'package:chat_interface/controller/account/friends/requests_controller.dart'; +import 'package:chat_interface/controller/account/requests_controller.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class RequestButton extends StatefulWidget { final bool self; // If the request was sent by the user @@ -16,8 +18,8 @@ class RequestButton extends StatefulWidget { State createState() => _RequestButtonState(); } -class _RequestButtonState extends State { - final requestLoading = false.obs; +class _RequestButtonState extends State with SignalsMixin { + late final requestLoading = createSignal(false); @override Widget build(BuildContext context) { @@ -25,8 +27,8 @@ class _RequestButtonState extends State { final children = [ IconButton( icon: Icon(Icons.close, color: Theme.of(context).colorScheme.onPrimary), - onPressed: () => widget.self ? widget.request.cancel() : widget.request.ignore(), - ) + onPressed: () => widget.request.delete(), + ), ]; // Add accept button if request is for self @@ -38,12 +40,15 @@ class _RequestButtonState extends State { loading: requestLoading, icon: Icons.check, color: Get.theme.colorScheme.onPrimary, - onTap: () { + onTap: () async { requestLoading.value = true; - widget.request.accept((p0) { - sendLog("Request accepted"); - requestLoading.value = false; - }); + final (error, result) = await widget.request.accept(); + if (error != null) { + showErrorPopup("error", error); + return; + } + sendLog(result); + requestLoading.value = false; }, ), ); @@ -70,9 +75,7 @@ class _RequestButtonState extends State { Padding( padding: const EdgeInsets.only(left: defaultSpacing), child: Tooltip( - message: "friends.different_town".trParams({ - "town": widget.request.id.server, - }), + message: "friends.different_town".trParams({"town": widget.request.id.server}), child: Icon(Icons.sensors, color: Get.theme.colorScheme.onPrimary), ), ), @@ -80,24 +83,20 @@ class _RequestButtonState extends State { ), //* Request actions - Obx( - () => widget.request.loading.value - ? const SizedBox( - width: 25, - height: 25, - child: Padding( - padding: EdgeInsets.all(defaultSpacing * 0.25), - child: CircularProgressIndicator( - strokeWidth: 2.0, + Watch( + (ctx) => + widget.request.loading.value + ? const SizedBox( + width: 25, + height: 25, + child: Padding( + padding: EdgeInsets.all(defaultSpacing * 0.25), + child: CircularProgressIndicator(strokeWidth: 2.0), ), - ), - ) - : - - //* Accept/decline - Row( - children: children, - ), + ) + : + //* Accept/decline + Row(children: children), ), ], ), diff --git a/lib/pages/chat/sidebar/sidebar.dart b/lib/pages/chat/sidebar/sidebar.dart index f9cc8fb7..bd23c83e 100644 --- a/lib/pages/chat/sidebar/sidebar.dart +++ b/lib/pages/chat/sidebar/sidebar.dart @@ -1,17 +1,14 @@ -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; -import 'package:chat_interface/main.dart'; import 'package:chat_interface/pages/chat/sidebar/sidebar_conversations.dart'; import 'package:chat_interface/pages/chat/sidebar/sidebar_profile.dart'; +import 'package:chat_interface/pages/chat/sidebar/universal_create_window.dart'; import 'package:chat_interface/pages/status/error/error_container.dart'; import 'package:chat_interface/pages/status/error/offline_hider.dart'; -import 'package:chat_interface/theme/ui/dialogs/conversation_add_window.dart'; -import 'package:chat_interface/theme/ui/dialogs/space_add_window.dart'; -import 'package:chat_interface/theme/ui/dialogs/upgrade_window.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_core.dart'; class Sidebar extends StatefulWidget { const Sidebar({super.key}); @@ -21,16 +18,20 @@ class Sidebar extends StatefulWidget { } class _SidebarState extends State { - final query = "".obs; - final GlobalKey _addConvKey = GlobalKey(), _addSpaceKey = GlobalKey(); + final _query = signal(""); + final _universalKey = GlobalKey(); + + @override + void dispose() { + _query.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - //* Sidebar + final theme = Theme.of(context); return Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.onInverseSurface, - ), + decoration: BoxDecoration(color: Get.theme.colorScheme.onInverseSurface), //* Sidebar content child: DevicePadding( @@ -49,16 +50,12 @@ class _SidebarState extends State { top: false, bottom: false, child: AnimatedErrorContainer( - padding: const EdgeInsets.only( - bottom: defaultSpacing, - right: defaultSpacing, - left: defaultSpacing, - ), - message: Get.find().error, + padding: const EdgeInsets.only(bottom: defaultSpacing, right: defaultSpacing, left: defaultSpacing), + message: ConnectionController.error, ), ), - //* Search field + // Search field Padding( padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), child: SizedBox( @@ -90,7 +87,7 @@ class _SidebarState extends State { hintStyle: Get.textTheme.bodyMedium, ), onChanged: (value) { - query.value = value; + _query.value = value; }, cursorColor: Get.theme.colorScheme.onPrimary, ), @@ -100,43 +97,16 @@ class _SidebarState extends State { OfflineHider( axis: Axis.horizontal, alignment: Alignment.center, - child: Row( - children: [ - horizontalSpacing(defaultSpacing * 0.5), - Visibility( - visible: areSpacesSupported, - child: IconButton( - key: _addSpaceKey, - onPressed: () { - if (isWeb) { - Get.dialog(UpgradeWindow()); - return; - } - - //* Open space add window - final RenderBox box = _addSpaceKey.currentContext?.findRenderObject() as RenderBox; - showModal(SpaceAddWindow(position: box.localToGlobal(box.size.bottomLeft(const Offset(0, 5))))); - }, - icon: Icon(Icons.rocket_launch, color: Get.theme.colorScheme.onPrimary), - ), - ), - horizontalSpacing(defaultSpacing * 0.5), - IconButton( - key: _addConvKey, - onPressed: () { - final RenderBox box = _addConvKey.currentContext?.findRenderObject() as RenderBox; - - //* Open conversation add window - showModal(ConversationAddWindow( - position: - ContextMenuData(box.localToGlobal(box.size.bottomLeft(const Offset(0, elementSpacing))), true, true), - )); - }, - icon: Icon(Icons.chat_bubble, color: Get.theme.colorScheme.onPrimary), - ), - ], + child: IconButton( + key: _universalKey, + onPressed: () { + showModal( + UniversalCreateWindow(data: ContextMenuData.fromKey(_universalKey, below: true)), + ); + }, + icon: Icon(Icons.add_circle, color: theme.colorScheme.onPrimary), ), - ) + ), ], ), ), @@ -148,17 +118,8 @@ class _SidebarState extends State { ), // Conversation list and the profile - Expanded( - child: SafeArea( - top: false, - bottom: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), - child: SidebarConversationList(query: query), - ), - ), - ), - if (!isMobileMode()) const SidebarProfile() + Expanded(child: SafeArea(top: false, bottom: false, child: SidebarConversationList(query: _query))), + if (!isMobileMode()) const SidebarProfile(), ], ), ), diff --git a/lib/pages/chat/sidebar/sidebar_conversations.dart b/lib/pages/chat/sidebar/sidebar_conversations.dart index e05053bb..54b2eef0 100644 --- a/lib/pages/chat/sidebar/sidebar_conversations.dart +++ b/lib/pages/chat/sidebar/sidebar_conversations.dart @@ -1,32 +1,36 @@ -import 'dart:math'; - -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/member_controller.dart'; import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/spaces/space_container.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/controller/current/connection_controller.dart'; +import 'package:chat_interface/database/database_entities.dart' as model; +import 'package:chat_interface/pages/chat/components/conversations/notification_dot.dart'; +import 'package:chat_interface/pages/chat/components/squares/square_topic_list.dart'; +import 'package:chat_interface/pages/chat/components/squares/topic_manage_window.dart'; +import 'package:chat_interface/services/chat/conversation_member.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:chat_interface/pages/chat/components/message/renderer/space_renderer.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/status_renderer.dart'; +import 'package:chat_interface/util/dispose_hook.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SidebarConversationList extends StatefulWidget { - final RxString query; + final Signal? query; - const SidebarConversationList({ - super.key, - required this.query, - }); + const SidebarConversationList({super.key, this.query}); @override State createState() => _SidebarConversationListState(); @@ -34,78 +38,94 @@ class SidebarConversationList extends StatefulWidget { class _SidebarConversationListState extends State { final ScrollController _controller = ScrollController(); + late Signal _query; + + @override + void initState() { + _query = widget.query ?? signal(""); + super.initState(); + } + + @override + void dispose() { + _query.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - final controller = Get.find(); - final spacesController = Get.find(); - final messageController = Get.find(); - final friendController = Get.find(); - - return Obx( - () { - final statusController = Get.find(); - return FadingEdgeScrollView.fromScrollView( - child: ListView.builder( - controller: _controller, - itemCount: controller.order.length, - addRepaintBoundaries: true, - padding: const EdgeInsets.only(top: defaultSpacing), - itemBuilder: (context, index) { - //* Normal conversation renderer - Conversation conversation = controller.conversations[controller.order.elementAt(index)]!; - - Friend? friend; - if (!conversation.isGroup) { - // Fetch the member that isn't the own account - LPHAddress id = conversation.members.values + return Watch((ctx) { + return ListView.builder( + controller: _controller, + itemCount: ConversationController.order.length, + addRepaintBoundaries: true, + padding: const EdgeInsets.only(top: defaultSpacing, right: defaultSpacing, left: defaultSpacing), + itemBuilder: (context, index) { + // Normal conversation renderer + Conversation conversation = + ConversationController.conversations[ConversationController.order.elementAt(index)]!; + + Friend? friend; + if (!conversation.isGroup) { + // Fetch the member that isn't the own account + LPHAddress id = + conversation.members.values .firstWhere( (element) => element.address != StatusController.ownAddress, orElse: () => Member(LPHAddress.error(), LPHAddress.error(), MemberRole.user), ) .address; - // If not found, just use own friend as a backup plan - if (id.id == "-") { - sendLog("THIS SHOULD NOT HAPPEN, rendering me as member of conversation"); - friend = Friend.me(); - } else { - // If found, use the actual friend of course - friend = friendController.friends[id]; - } + // If not found, just use own friend as a backup plan + if (id.id == "-") { + sendLog("THIS SHOULD NOT HAPPEN, rendering me as member of conversation"); + friend = Friend.me(); + } else { + // If found, use the actual friend of course + friend = FriendController.friends[id]; + } + } + + // Hover menu + return Watch(key: ValueKey("${conversation.id.encode()}-sb"), (ctx) { + // Determine the title of the conversation based on the type + String title; + if (conversation.isGroup || friend == null) { + title = conversation.containerSub.value.name; + } else { + title = conversation.dmName; + } + + // Make sure to mark the conversation as archived in case the friend doesn't exist + if (friend == null && !conversation.isGroup) { + title = ".$title"; + } + + // Handle when hidden by search + if (_query.value != "") { + if (!title.toLowerCase().startsWith(_query.value.toLowerCase())) { + return const SizedBox(); } + } else if (friend == null && !conversation.isGroup) { + return const SizedBox(); + } - // Hover menu - final hover = false.obs; - - return Column( - key: ValueKey(conversation), - mainAxisSize: MainAxisSize.min, - children: [ - //* Conversation item - Obx( - () { - var title = conversation.isGroup || friend == null ? conversation.containerSub.value.name : conversation.dmName; - if (friend == null && !conversation.isGroup) { - title = ".$title"; - } - - if (widget.query.value != "") { - if (!title.toLowerCase().startsWith(widget.query.value.toLowerCase())) { - return const SizedBox.shrink(); - } - } else if (friend == null && !conversation.isGroup) { - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.only(bottom: defaultSpacing * 0.5), - child: Obx( - () => Material( + return SignalHook( + value: false, + builder: + (hover) => Padding( + padding: const EdgeInsets.only(bottom: defaultSpacing * 0.5), + child: Watch((ctx) { + final provider = SidebarController.getCurrentProviderReactive(); + + return Column( + children: [ + Material( borderRadius: BorderRadius.circular(defaultSpacing), - color: messageController.currentProvider.value?.conversation == conversation && !isMobileMode() - ? Get.theme.colorScheme.onSurface.withOpacity(0.075) - : Colors.transparent, + color: + provider?.conversation == conversation && provider?.extra == "" && !isMobileMode() + ? Get.theme.colorScheme.onSurface.withAlpha(20) + : Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(defaultSpacing), hoverColor: Get.theme.hoverColor, @@ -114,233 +134,358 @@ class _SidebarConversationListState extends State { hover.value = value; }, - //* When conversation is tapped (open conversation) + // When conversation is tapped (open conversation) onTap: () { - if (messageController.currentProvider.value?.conversation == conversation && !isMobileMode()) return; - messageController.selectConversation(conversation); + // Make sure to not open the conversation again + if (provider?.conversation == conversation && + provider?.extra == "" && + !isMobileMode()) { + return; + } + MessageController.openConversation(conversation); + }, + onSecondaryTapDown: (details) { + ConversationMessageProvider( + conversation, + ).openDialogForConversation(ContextMenuData.fromPosition(details.globalPosition)); }, - //* Conversation item content - child: Padding( - padding: const EdgeInsets.all(elementSpacing2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - //* Conversation info - Expanded( - child: Row( - children: [ - if (conversation.isGroup || friend == null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: elementSpacing * 0.5), - child: Icon( - conversation.isGroup - ? Icons.group - : friend == null - ? Icons.person_off - : Icons.person, - size: 35, - color: Get.theme.colorScheme.onPrimary, - ), - ) - else - UserAvatar(id: friend.id, size: 40), - horizontalSpacing(defaultSpacing * 0.75), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //* Conversation title - if (conversation.isGroup) - Row( - children: [ - Flexible( - child: Text( - conversation.containerSub.value.name, - style: messageController.currentProvider.value?.conversation == conversation - ? Get.theme.textTheme.labelMedium - : Get.theme.textTheme.bodyMedium, - textHeightBehavior: noTextHeight, - ), - ), - if (conversation.id.server != basePath && friend == null) - Padding( - padding: const EdgeInsets.only(left: defaultSpacing), - child: Tooltip( - waitDuration: const Duration(milliseconds: 500), - message: "conversations.different_town".trParams({ - "town": conversation.id.server, - }), - child: Icon( - Icons.sensors, - color: Get.theme.colorScheme.onPrimary, - size: 21, - ), - ), - ), - ], - ) - else - Obx(() { - return Row( - children: [ - Flexible( - child: Text( - friend != null ? conversation.dmName : conversation.containerSub.value.name, - style: messageController.currentProvider.value?.conversation == conversation - ? Get.theme.textTheme.labelMedium - : Get.theme.textTheme.bodyMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textHeightBehavior: noTextHeight, - ), - ), - if (friend != null && friend.id.server != basePath) - Padding( - padding: const EdgeInsets.only(left: defaultSpacing), - child: Tooltip( - waitDuration: const Duration(milliseconds: 500), - message: "friends.different_town".trParams({ - "town": friend.id.server, - }), - child: Icon( - Icons.sensors, - color: Get.theme.colorScheme.onPrimary, - size: 21, - ), - ), - ), - horizontalSpacing(defaultSpacing), - if (friend != null) StatusRenderer(status: friend.statusType.value), - ], - ); - }), - - friend == null - ? verticalSpacing(elementSpacing * 0.5) - : Visibility( - visible: conversation.isGroup || friend.status.value != "", - child: verticalSpacing(elementSpacing * 0.5), - ), - - // Conversation description - conversation.isGroup - ? Text( - //* Conversation status message - "chat.members".trParams({'count': conversation.members.length.toString()}), - - style: Get.textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - : - - //* Friend status message - friend == null - ? Text( - friend != null ? friend.status.value : "friend.removed".tr, - style: Get.textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textHeightBehavior: noTextHeight, - ) - : Obx( - () => Visibility( - visible: friend!.status.value != "" && friend.statusType.value != statusOffline, - child: Text( - friend.status.value, - style: Get.textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textHeightBehavior: noTextHeight, - ), - ), - ), - ], - ), - ), - ], - ), - ), - Obx( - () { - final notifications = conversation.notificationCount.value; - if (hover.value) { - return IconButton( - onPressed: () => showConfirmPopup(ConfirmWindow( - title: "conversations.leave".tr, - text: "conversations.leave.text".tr, - onConfirm: () => conversation.delete(), - onDecline: () => {}, - )), - icon: const Icon(Icons.close), - ); - } - - return Visibility( - visible: notifications > 0, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Get.theme.colorScheme.error, - ), - child: Padding( - padding: const EdgeInsets.only(left: 5, right: 5, top: 2, bottom: 3), - child: Center(child: Text(min(notifications, 99).toString(), style: Get.textTheme.labelSmall)), - ), - ), - ); - }, - ), - ], - ), - ), + // Conversation item content + child: renderConversationItem(conversation, title, provider, friend, hover), ), ), - ), + + // Render the topic list in case open and square + if (conversation.type == model.ConversationType.square) + SquareTopicList(square: conversation as Square), + ], ); - }, - ), - //* Render shared content - if (friend != null) - Obx(() { - final content = statusController.sharedContent[friend!.id]; - if (content == null) { - return const SizedBox(); - } - switch (content.type) { - case ShareType.space: - final container = content as SpaceConnectionContainer; - return Padding( - padding: const EdgeInsets.only(bottom: elementSpacing), - child: Obx( - () => Animate( - effects: [ - ExpandEffect( - duration: 250.ms, - curve: Curves.easeInOut, - axis: Axis.vertical, - alignment: Alignment.topLeft, - ), - ], - target: spacesController.id.value == container.roomId ? 0.0 : 1.0, - child: SpaceRenderer( - container: container, - pollNewData: true, - clickable: true, - sidebar: true, - ), - ), - ), - ); - } }), - ], + ), + ); + }); + }, + ); + }); + } + + /// Render the item for one conversation in the list + Padding renderConversationItem( + Conversation conversation, + String title, + ConversationMessageProvider? provider, + Friend? friend, + Signal hover, + ) { + return Padding( + padding: const EdgeInsets.all(elementSpacing2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Conversation info + if (conversation.type == model.ConversationType.group) + renderGroup(title, conversation, provider) + else if (conversation.type == model.ConversationType.square) + renderSquare(title, conversation, provider) + else + renderDirectMessage(friend, title, conversation, provider), + + Watch((ctx) { + // Make sure the user is actually connected + if (!ConnectionController.connected.value) { + return const SizedBox(); + } + + final notifications = ConversationController.notificationMap[conversation.id.encode()] ?? 0; + if (hover.value) { + // Show the create topic button in case it's a square + if (conversation.type == model.ConversationType.square) { + return Row( + children: [ + IconButton( + onPressed: () { + showModal(TopicManageWindow(square: conversation as Square)); + }, + icon: Icon(Icons.add), + ), + ], + ); + } + + // Show a remove button to leave the current conversation for all other types + return IconButton( + onPressed: + () => showConfirmPopup( + ConfirmWindow( + title: "conversations.leave".tr, + text: "conversations.leave.text".tr, + onConfirm: () => conversation.delete(), + onDecline: () => {}, + ), + ), + icon: const Icon(Icons.close), ); - }, + } + + return Visibility(visible: notifications > 0, child: NotificationDot(amount: notifications)); + }), + + // Show a collapse/expand topics button when it's a square + if (conversation is Square) + Padding( + padding: EdgeInsets.only(left: elementSpacing), + child: Watch((ctx) { + final squareContainer = conversation.container as SquareContainer; + final toggled = conversation.topicsShown.value; + bool notifications = false; + for (var topic in squareContainer.topics) { + if (ConversationController.notificationMap[ConversationService.withExtra( + conversation.id.encode(), + topic.id, + )] != + 0) { + notifications = true; + break; + } + } + + return Stack( + children: [ + IconButton( + onPressed: () { + conversation.topicsShown.value = !conversation.topicsShown.peek(); + }, + icon: Icon(toggled ? Icons.expand_less : Icons.expand_more), + ), + Visibility( + visible: notifications, + child: Positioned( + right: defaultSpacing, + bottom: defaultSpacing, + child: Container( + decoration: BoxDecoration(shape: BoxShape.circle, color: Get.theme.colorScheme.error), + width: defaultSpacing, + height: defaultSpacing, + ), + ), + ), + ], + ); + }), + ), + ], + ), + ); + } + + /// Render a direct message preview for the sidebar + Widget renderDirectMessage( + Friend? friend, + String title, + Conversation conversation, + ConversationMessageProvider? provider, + ) { + return Expanded( + child: Row( + children: [ + // Render the avatar of the friend or a placeholder (if broken conversation) + if (friend == null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: elementSpacing * 0.5), + child: Icon(Icons.person_off, size: 35, color: Get.theme.colorScheme.onPrimary), + ) + else + UserAvatar(id: friend.id, size: 40), + horizontalSpacing(defaultSpacing * 0.75), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Render the name of the friend + Row( + children: [ + Flexible( + child: Text( + conversation.dmName, + style: + provider?.conversation == conversation + ? Get.theme.textTheme.labelMedium + : Get.theme.textTheme.bodyMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textHeightBehavior: noTextHeight, + ), + ), + + // Render the foreign town indicator (in case needed) + if (friend != null && friend.id.server != basePath) + Padding( + padding: const EdgeInsets.only(left: defaultSpacing), + child: Tooltip( + waitDuration: const Duration(milliseconds: 500), + message: "friends.different_town".trParams({"town": friend.id.server}), + child: Icon(Icons.sensors, color: Get.theme.colorScheme.onPrimary, size: 21), + ), + ), + horizontalSpacing(defaultSpacing), + + // Render status + if (friend != null) StatusRenderer(status: friend.statusType.value), + ], + ), + + friend == null + ? verticalSpacing(elementSpacing * 0.5) + : Visibility( + visible: conversation.isGroup || friend.status.value != "", + child: verticalSpacing(elementSpacing * 0.5), + ), + + // Friend status message (if available) + if (friend == null) + Text( + friend != null ? friend.status.value : "friend.removed".tr, + style: Get.textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textHeightBehavior: noTextHeight, + ) + else + Watch( + (ctx) => Visibility( + visible: friend.status.value != "" && friend.statusType.value != statusOffline, + child: Text( + friend.status.value, + style: Get.textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textHeightBehavior: noTextHeight, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + /// Render a group preview for the sidebar + Widget renderGroup(String title, Conversation conversation, ConversationMessageProvider? provider) { + return Expanded( + child: Row( + children: [ + // Render a group icon + Padding( + padding: const EdgeInsets.symmetric(horizontal: elementSpacing * 0.5), + child: Icon(Icons.group, size: 35, color: Get.theme.colorScheme.onPrimary), + ), + horizontalSpacing(defaultSpacing * 0.75), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Render the title of the group + Row( + children: [ + Flexible( + child: Text( + conversation.containerSub.value.name, + style: + provider?.conversation == conversation + ? Get.theme.textTheme.labelMedium + : Get.theme.textTheme.bodyMedium, + textHeightBehavior: noTextHeight, + ), + ), + + // Render remote indicator (in case needed) + if (conversation.id.server != basePath) + Padding( + padding: const EdgeInsets.only(left: defaultSpacing), + child: Tooltip( + waitDuration: const Duration(milliseconds: 500), + message: "conversations.different_town".trParams({"town": conversation.id.server}), + child: Icon(Icons.sensors, color: Get.theme.colorScheme.onPrimary, size: 21), + ), + ), + ], + ), + + verticalSpacing(elementSpacing * 0.5), + + // Render the amount of members of the conversation + Text( + "chat.members".trParams({"count": conversation.members.length.toString()}), + style: Get.textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } + + /// Render a square preview for the sidebar + Widget renderSquare(String title, Conversation conversation, ConversationMessageProvider? provider) { + return Expanded( + child: Row( + children: [ + // Render a square icon + Padding( + padding: const EdgeInsets.symmetric(horizontal: elementSpacing * 0.5), + child: Icon(Icons.public, size: 35, color: Get.theme.colorScheme.onPrimary), + ), + horizontalSpacing(defaultSpacing * 0.75), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Render the title of the group + Row( + children: [ + Flexible( + child: Text( + conversation.containerSub.value.name, + style: + provider?.conversation == conversation && provider?.extra == "" + ? Get.theme.textTheme.labelMedium + : Get.theme.textTheme.bodyMedium, + textHeightBehavior: noTextHeight, + ), + ), + + // Render remote indicator (in case needed) + if (conversation.id.server != basePath) + Padding( + padding: const EdgeInsets.only(left: defaultSpacing), + child: Tooltip( + waitDuration: const Duration(milliseconds: 500), + message: "conversations.different_town".trParams({"town": conversation.id.server}), + child: Icon(Icons.sensors, color: Get.theme.colorScheme.onPrimary, size: 21), + ), + ), + ], + ), + + verticalSpacing(elementSpacing * 0.5), + + // Render the amount of members of the conversation + Text( + "chat.members".trParams({"count": conversation.members.length.toString()}), + style: Get.textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), - ); - }, + ], + ), ); } } diff --git a/lib/pages/chat/sidebar/sidebar_profile.dart b/lib/pages/chat/sidebar/sidebar_profile.dart index 8a000c55..c11560df 100644 --- a/lib/pages/chat/sidebar/sidebar_profile.dart +++ b/lib/pages/chat/sidebar/sidebar_profile.dart @@ -1,9 +1,10 @@ import 'dart:math'; -import 'package:chat_interface/controller/account/friends/requests_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/spaces/space_container.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/account/requests_controller.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/chat/sidebar/friends/friends_page.dart'; @@ -17,6 +18,7 @@ import 'package:chat_interface/theme/ui/profile/status_renderer.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SidebarProfile extends StatefulWidget { const SidebarProfile({super.key}); @@ -30,8 +32,6 @@ class _SidebarProfileState extends State { @override Widget build(BuildContext context) { - final controller = Get.find(); - final statusController = Get.find(); ThemeData theme = Theme.of(context); return Container( @@ -43,262 +43,267 @@ class _SidebarProfileState extends State { left: true, child: Padding( padding: const EdgeInsets.all(defaultSpacing), - child: LayoutBuilder(builder: (context, constraints) { - return SizedBox( - width: constraints.maxWidth, - child: Column( - children: [ - Obx(() { - if (!controller.inSpace.value) { - // Render an embed letting the user know he's in a call on another device - if (statusController.ownContainer.value != null && statusController.ownContainer.value is SpaceConnectionContainer) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: elementSpacing, horizontal: defaultSpacing), - child: Row( - children: [ - Icon(Icons.public, color: Get.theme.colorScheme.onPrimary), - horizontalSpacing(defaultSpacing), - Text("spaces.sharing_other_device".tr, style: Get.theme.textTheme.bodyMedium), - const Spacer(), - LoadingIconButton( - onTap: () => Get.find().join(statusController.ownContainer.value! as SpaceConnectionContainer), - icon: Icons.login, - extra: defaultSpacing, - iconSize: 25, - color: theme.colorScheme.onSurface, - ), - ], - ), - ); - } - - return const SizedBox.shrink(); - } - final shown = Get.find().currentProvider.value == null; - - return Column( - children: [ - Material( - borderRadius: BorderRadius.circular(defaultSpacing), - color: shown ? theme.colorScheme.inverseSurface : theme.colorScheme.primaryContainer, - child: InkWell( - onTap: () { - final controller = Get.find(); - controller.unselectConversation(); - controller.currentOpenType.value = OpenTabType.space; - }, - splashColor: theme.hoverColor, - hoverColor: shown ? theme.colorScheme.inverseSurface : theme.colorScheme.inverseSurface, - borderRadius: BorderRadius.circular(defaultSpacing), - child: Padding( + child: LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + width: constraints.maxWidth, + child: Column( + children: [ + Watch((context) { + if (!SpaceController.connected.value) { + // Render an embed letting the user know he's in a call on another device + return Watch((ctx) { + if (StatusController.ownContainer.value != null && + StatusController.ownContainer.value is SpaceConnectionContainer) { + return Padding( padding: const EdgeInsets.symmetric(vertical: elementSpacing, horizontal: defaultSpacing), child: Row( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.public, color: Get.theme.colorScheme.onPrimary), - ], - ), + Icon(Icons.public, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(defaultSpacing), + Text("spaces.sharing_other_device".tr, style: Get.theme.textTheme.bodyMedium), const Spacer(), LoadingIconButton( - padding: 0, - extra: 10, + onTap: + () => SpaceController.join( + StatusController.ownContainer.value! as SpaceConnectionContainer, + ), + icon: Icons.login, + extra: defaultSpacing, iconSize: 25, - onTap: () => Get.dialog(const SpaceInfoWindow()), - icon: Icons.info, + color: theme.colorScheme.onSurface, ), ], ), + ); + } + + return const SizedBox(); + }); + } + + return Watch((ctx) { + final shown = SidebarController.currentOpenTab.value is SpaceSidebarTab; + + return Column( + children: [ + Material( + borderRadius: BorderRadius.circular(defaultSpacing), + color: shown ? theme.colorScheme.inverseSurface : theme.colorScheme.primaryContainer, + child: InkWell( + onTap: () { + SidebarController.openTab(SpaceSidebarTab()); + }, + splashColor: theme.hoverColor, + hoverColor: shown ? theme.colorScheme.inverseSurface : theme.colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(defaultSpacing), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: elementSpacing, + horizontal: defaultSpacing, + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Icon(Icons.public, color: Get.theme.colorScheme.onPrimary)], + ), + horizontalSpacing(defaultSpacing), + const Spacer(), + LoadingIconButton( + padding: 0, + extra: 10, + iconSize: 25, + onTap: () => Get.dialog(const SpaceInfoWindow()), + icon: Icons.info, + ), + ], + ), + ), + ), ), - ), - ), - verticalSpacing(defaultSpacing), - ], - ); - }), + verticalSpacing(defaultSpacing), + ], + ); + }); + }), - //* Actual profile - Material( - key: _profileKey, - borderRadius: BorderRadius.circular(defaultSpacing), - color: theme.colorScheme.primaryContainer, - child: InkWell( - onTap: () => showModal(OwnProfile(position: ContextMenuData.fromKey(_profileKey, above: true))), - splashColor: theme.hoverColor.withAlpha(10), + // Actual profile + Material( + key: _profileKey, borderRadius: BorderRadius.circular(defaultSpacing), - hoverColor: theme.colorScheme.inverseSurface, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: elementSpacing, vertical: elementSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx(() { - // Check if the thing is loading - if (Get.find().loading.value) { - return Row( - children: [ - horizontalSpacing(defaultSpacing), - SizedBox( - width: 30, - height: 30, - child: CircularProgressIndicator( - color: Get.theme.colorScheme.onPrimary, + color: theme.colorScheme.primaryContainer, + child: InkWell( + onTap: () => showModal(OwnProfile(position: ContextMenuData.fromKey(_profileKey, above: true))), + splashColor: theme.hoverColor.withAlpha(10), + borderRadius: BorderRadius.circular(defaultSpacing), + hoverColor: theme.colorScheme.inverseSurface, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: elementSpacing, vertical: elementSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Watch((ctx) { + // Check if the thing is loading + if (ConnectionController.loading.value) { + return Row( + children: [ + horizontalSpacing(defaultSpacing), + SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary), ), - ), - horizontalSpacing(defaultSpacing), - Text("loading".tr, style: Get.textTheme.labelLarge), - ], - ); - } + horizontalSpacing(defaultSpacing), + Text("loading".tr, style: Get.textTheme.labelLarge), + ], + ); + } - // Check if the thing is connected - if (!Get.find().connected.value) { - return Row( - children: [ - horizontalSpacing(defaultSpacing), - Icon( - Icons.cloud_off, - color: Get.theme.colorScheme.onPrimary, - ), - horizontalSpacing(defaultSpacing), - Text("offline".tr, style: Get.textTheme.labelLarge), - ], - ); - } + // Check if the thing is connected + if (!ConnectionController.connected.value) { + return Row( + children: [ + horizontalSpacing(defaultSpacing), + Icon(Icons.cloud_off, color: Get.theme.colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Text("offline".tr, style: Get.textTheme.labelLarge), + ], + ); + } - return Expanded( - child: Row( - children: [ - UserAvatar(id: StatusController.ownAddress, size: 40), - horizontalSpacing(defaultSpacing), - Expanded( - child: Obx( - () => Visibility( - visible: !statusController.statusLoading.value, - replacement: Center( - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 3.0, - color: Get.theme.colorScheme.onPrimary, + return Expanded( + child: Row( + children: [ + UserAvatar(id: StatusController.ownAddress, size: 40), + horizontalSpacing(defaultSpacing), + Expanded( + child: Watch( + (ctx) => Visibility( + visible: !StatusController.statusLoading.value, + replacement: Center( + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 3.0, + color: Get.theme.colorScheme.onPrimary, + ), ), ), ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //* Profile name and status type - Row( - children: [ - Flexible( - child: Obx( - () => Text( - statusController.displayName.value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleMedium, - textHeightBehavior: noTextHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // The current user's display name and status type + Row( + children: [ + Flexible( + child: Watch( + (ctx) => Text( + StatusController.displayName.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium, + textHeightBehavior: noTextHeight, + ), ), ), - ), - horizontalSpacing(defaultSpacing), - Obx( - () => StatusRenderer(status: statusController.type.value, text: false), - ) - ], - ), - - //* Status message - Obx( - () => Visibility( - visible: statusController.status.value != "", - child: Column( - children: [ - verticalSpacing(defaultSpacing * 0.25), - - //* Status message - Text( - statusController.status.value, - style: theme.textTheme.bodySmall, - textHeightBehavior: noTextHeight, - overflow: TextOverflow.ellipsis, + horizontalSpacing(defaultSpacing), + Watch( + (ctx) => StatusRenderer( + status: StatusController.type.value, + text: false, ), - ], + ), + ], + ), + + // Render the status message of the curretn user + Watch( + (ctx) => Visibility( + visible: StatusController.status.value != "", + child: Column( + children: [ + verticalSpacing(defaultSpacing * 0.25), + Text( + StatusController.status.value, + style: theme.textTheme.bodySmall, + textHeightBehavior: noTextHeight, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ), - ) - ], + ], + ), ), ), ), - ) - ], - ), - ); - }), - horizontalSpacing(defaultSpacing), - Row( - children: [ - SizedBox( - width: 40, - height: 40, - child: Stack( - children: [ - IconButton( - onPressed: () => showModal(const FriendsPage()), - icon: const Icon(Icons.group, color: Colors.white), - ), - Obx(() { - final controller = Get.find(); - if (controller.requests.isEmpty) { - return const SizedBox(); - } - final amount = controller.requests.length; + ], + ), + ); + }), + horizontalSpacing(defaultSpacing), + Row( + children: [ + SizedBox( + width: 40, + height: 40, + child: Stack( + children: [ + IconButton( + onPressed: () => showModal(const FriendsPage()), + icon: const Icon(Icons.group, color: Colors.white), + ), + Watch((ctx) { + if (RequestController.requests.isEmpty) { + return const SizedBox(); + } + final amount = RequestController.requests.length; - return Align( - alignment: Alignment.bottomRight, - child: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: Get.theme.colorScheme.error, - borderRadius: BorderRadius.circular(100), - ), - padding: const EdgeInsets.only(bottom: elementSpacing), - child: Center( - child: Text( - min(amount, 9).toString(), - style: Get.textTheme.labelSmall!.copyWith(fontSize: 12), + return Align( + alignment: Alignment.bottomRight, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Get.theme.colorScheme.error, + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.only(bottom: elementSpacing), + child: Center( + child: Text( + min(amount, 9).toString(), + style: Get.textTheme.labelSmall!.copyWith(fontSize: 12), + ), ), ), - ), - ); - }), - ], + ); + }), + ], + ), ), - ), - horizontalSpacing(defaultSpacing * 0.5), - IconButton( - onPressed: () => SettingController.openSettingsPage(), - icon: const Icon(Icons.settings, color: Colors.white), - ), - ], - ) - ], + horizontalSpacing(defaultSpacing * 0.5), + IconButton( + onPressed: () => SettingController.openSettingsPage(), + icon: const Icon(Icons.settings, color: Colors.white), + ), + ], + ), + ], + ), ), ), ), - ), - ], - ), - ); - }), + ], + ), + ); + }, + ), ), ), ); diff --git a/lib/pages/chat/sidebar/universal_create_window.dart b/lib/pages/chat/sidebar/universal_create_window.dart new file mode 100644 index 00000000..330aa912 --- /dev/null +++ b/lib/pages/chat/sidebar/universal_create_window.dart @@ -0,0 +1,131 @@ +import 'package:chat_interface/pages/chat/components/squares/square_add_window.dart'; +import 'package:chat_interface/pages/settings/appearance/chat_settings.dart'; +import 'package:chat_interface/pages/status/setup/smooth_dialog.dart'; +import 'package:chat_interface/theme/ui/dialogs/conversation_add_window.dart'; +import 'package:chat_interface/theme/ui/dialogs/space_add_window.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class _CreateData { + final IconData icon; + final String title; + final String description; + final Widget Function() build; + + _CreateData({required this.icon, required this.title, required this.description, required this.build}); +} + +class UniversalCreateWindow extends StatefulWidget { + final ContextMenuData data; + + const UniversalCreateWindow({super.key, required this.data}); + + @override + State createState() => _UniversalCreateWindowState(); +} + +class _UniversalCreateWindowState extends State with TickerProviderStateMixin { + /// All the different types of things that can be created + late final _types = <_CreateData>[ + _CreateData( + icon: Icons.public, + title: "square".tr, + description: "square.desc".tr, + build: () => SquareAddWindow(position: null), + ), + _CreateData( + icon: Icons.chat, + title: "conversation".tr, + description: "conversation.desc".tr, + build: () => ConversationAddWindow(position: null), + ), + _CreateData( + icon: Icons.rocket_launch, + title: "space".tr, + description: "space.desc".tr, + build: () => SpaceAddWindow(), + ), + ]; + + late final SmoothDialogController _controller = SmoothDialogController( + Padding( + padding: const EdgeInsets.symmetric(vertical: dialogPadding), + child: Builder( + builder: (context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${"create".tr}${List.filled(ChatSettings.dotAmount.getValue().toInt(), '.').join('')}", + style: theme.textTheme.labelLarge, + ), + + Column( + children: List.generate(_types.length, (index) { + final data = _types[index]; + + return Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: Material( + borderRadius: BorderRadius.circular(defaultSpacing), + color: theme.colorScheme.inverseSurface, + child: InkWell( + borderRadius: BorderRadius.circular(defaultSpacing), + onTap: () { + _controller.transitionTo( + Padding(padding: const EdgeInsets.symmetric(vertical: dialogPadding), child: data.build()), + ); + }, + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + Icon(data.icon, size: 35, color: theme.colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(data.title, style: theme.textTheme.labelMedium), + verticalSpacing(elementSpacing), + Text(data.description, style: theme.textTheme.bodySmall), + ], + ), + ], + ), + ), + ), + ), + ); + }), + ), + ], + ); + }, + ), + ), + duration: Duration(milliseconds: 500), + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SlidingWindowBase( + padding: 0, + title: const [], + position: widget.data, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: dialogPadding), + child: SmoothBox(controller: _controller), + ), + ); + } +} diff --git a/lib/pages/settings/account/change_display_name_window.dart b/lib/pages/settings/account/change_display_name_window.dart index 209e8e53..e0133260 100644 --- a/lib/pages/settings/account/change_display_name_window.dart +++ b/lib/pages/settings/account/change_display_name_window.dart @@ -7,6 +7,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ChangeDisplayNameWindow extends StatefulWidget { const ChangeDisplayNameWindow({super.key}); @@ -20,18 +21,19 @@ class _ChangeNameWindowState extends State { final _displayNameController = TextEditingController(); // State - final _errorText = ''.obs; - final _loading = false.obs; + final _errorText = signal(''); + final _loading = signal(false); @override void dispose() { + _errorText.dispose(); + _loading.dispose(); _displayNameController.dispose(); super.dispose(); } /// Save the display name Future save() async { - final controller = Get.find(); if (_loading.value) return; _loading.value = true; _errorText.value = ""; @@ -46,20 +48,17 @@ class _ChangeNameWindowState extends State { return; } - controller.displayName.value = _displayNameController.text; + StatusController.displayName.value = _displayNameController.text; _loading.value = false; Get.back(); } @override Widget build(BuildContext context) { - final controller = Get.find(); - _displayNameController.text = controller.displayName.value; + _displayNameController.text = StatusController.displayName.value; return DialogBase( - title: [ - Text("display_name".tr, style: Get.textTheme.labelLarge), - ], + title: [Text("display_name".tr, style: Get.textTheme.labelLarge)], child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -82,7 +81,7 @@ class _ChangeNameWindowState extends State { loading: _loading, onTap: () => save(), child: Center(child: Text("save".tr, style: Get.theme.textTheme.labelLarge)), - ) + ), ], ), ); diff --git a/lib/pages/settings/account/change_name_window.dart b/lib/pages/settings/account/change_name_window.dart index a4fd04f8..f5e72c77 100644 --- a/lib/pages/settings/account/change_name_window.dart +++ b/lib/pages/settings/account/change_name_window.dart @@ -7,6 +7,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ChangeNameWindow extends StatefulWidget { const ChangeNameWindow({super.key}); @@ -15,13 +16,13 @@ class ChangeNameWindow extends StatefulWidget { State createState() => _ChangeNameWindowState(); } -class _ChangeNameWindowState extends State { +class _ChangeNameWindowState extends State with SignalsMixin { // Text controllers final _usernameController = TextEditingController(); // State - final _errorText = ''.obs; - final _loading = false.obs; + late final _errorText = createSignal(''); + late final _loading = createSignal(false); @override void dispose() { @@ -34,9 +35,7 @@ class _ChangeNameWindowState extends State { _loading.value = true; _errorText.value = ""; - final json = await postAuthorizedJSON("/account/settings/change_name", { - "name": _usernameController.text, - }); + final json = await postAuthorizedJSON("/account/settings/change_name", {"name": _usernameController.text}); if (!json["success"]) { _errorText.value = json["error"].toString().tr; @@ -44,20 +43,17 @@ class _ChangeNameWindowState extends State { return; } - Get.find().name.value = _usernameController.text; + StatusController.name.value = _usernameController.text; _loading.value = false; Get.back(); } @override Widget build(BuildContext context) { - final controller = Get.find(); - _usernameController.text = controller.name.value; + _usernameController.text = StatusController.name.value; return DialogBase( - title: [ - Text("username".tr, style: Get.textTheme.labelLarge), - ], + title: [Text("username".tr, style: Get.textTheme.labelLarge)], child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -80,7 +76,7 @@ class _ChangeNameWindowState extends State { loading: _loading, onTap: () => save(), child: Center(child: Text("save".tr, style: Get.theme.textTheme.labelLarge)), - ) + ), ], ), ); diff --git a/lib/pages/settings/account/change_password_window.dart b/lib/pages/settings/account/change_password_window.dart index 96c28d67..b50769f8 100644 --- a/lib/pages/settings/account/change_password_window.dart +++ b/lib/pages/settings/account/change_password_window.dart @@ -1,5 +1,5 @@ -import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/chat/status_service.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; @@ -7,6 +7,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ChangePasswordWindow extends StatefulWidget { const ChangePasswordWindow({super.key}); @@ -15,15 +16,15 @@ class ChangePasswordWindow extends StatefulWidget { State createState() => _ChangeNameWindowState(); } -class _ChangeNameWindowState extends State { +class _ChangeNameWindowState extends State with SignalsMixin { // Text controllers final _currentPasswordController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); // State - final _errorText = ''.obs; - final _loading = false.obs; + late final _errorText = createSignal(''); + late final _loading = createSignal(false); @override void dispose() { @@ -36,9 +37,7 @@ class _ChangeNameWindowState extends State { @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text("password".tr, style: Get.theme.textTheme.labelLarge), - ], + title: [Text("password".tr, style: Get.theme.textTheme.labelLarge)], child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -49,27 +48,15 @@ class _ChangeNameWindowState extends State { // Current password Text("password.current".tr, textAlign: TextAlign.left, style: Get.theme.textTheme.labelMedium), verticalSpacing(elementSpacing), - FJTextField( - hintText: 'placeholder.password'.tr, - obscureText: true, - controller: _currentPasswordController, - ), + FJTextField(hintText: 'placeholder.password'.tr, obscureText: true, controller: _currentPasswordController), verticalSpacing(defaultSpacing), // Password Text("password".tr, textAlign: TextAlign.left, style: Get.theme.textTheme.labelMedium), verticalSpacing(elementSpacing), - FJTextField( - hintText: 'placeholder.password'.tr, - obscureText: true, - controller: _passwordController, - ), + FJTextField(hintText: 'placeholder.password'.tr, obscureText: true, controller: _passwordController), verticalSpacing(defaultSpacing), - FJTextField( - hintText: 'placeholder.password'.tr, - obscureText: true, - controller: _confirmPasswordController, - ), + FJTextField(hintText: 'placeholder.password'.tr, obscureText: true, controller: _confirmPasswordController), verticalSpacing(defaultSpacing), AnimatedErrorContainer( @@ -108,10 +95,10 @@ class _ChangeNameWindowState extends State { } // Log out of this device - await Get.find().logOut(); + await StatusService.logOut(); }, child: Center(child: Text("save".tr, style: Get.theme.textTheme.labelLarge)), - ) + ), ], ), ); diff --git a/lib/pages/settings/account/data_settings.dart b/lib/pages/settings/account/data_settings.dart index d1968b04..bf0e7d56 100644 --- a/lib/pages/settings/account/data_settings.dart +++ b/lib/pages/settings/account/data_settings.dart @@ -1,14 +1,11 @@ import 'dart:async'; -import 'package:chat_interface/controller/account/profile_picture_helper.dart'; +import 'package:chat_interface/services/chat/profile_picture_helper.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/settings/account/change_display_name_window.dart'; import 'package:chat_interface/pages/settings/account/change_name_window.dart'; import 'package:chat_interface/pages/settings/account/log_out_window.dart'; import 'package:chat_interface/pages/settings/account/key_requests_window.dart'; -import 'package:chat_interface/pages/settings/components/bool_selection_small.dart'; -import 'package:chat_interface/pages/settings/data/entities.dart'; -import 'package:chat_interface/pages/settings/data/settings_controller.dart'; import 'package:chat_interface/pages/settings/settings_page_base.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; @@ -19,65 +16,18 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; - -class DataSettings { - static const String socialFeatures = "social.enable"; - - static void registerSettings(SettingController controller) { - controller.settings[socialFeatures] = Setting(socialFeatures, false); - } -} +import 'package:signals/signals_flutter.dart'; class DataSettingsPage extends StatelessWidget { const DataSettingsPage({super.key}); @override Widget build(BuildContext context) { - final controller = Get.find(); return SettingsPageBase( label: "data", child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - //* Social features - if (Get.find().settings[DataSettings.socialFeatures]!.getValue()) - Padding( - padding: const EdgeInsets.only(bottom: sectionSpacing), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Text("settings.data.social".tr, style: Get.theme.textTheme.labelLarge), - horizontalSpacing(defaultSpacing), - Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.error.withOpacity(0.1), - borderRadius: BorderRadius.circular(defaultSpacing), - ), - padding: const EdgeInsets.all(elementSpacing), - child: Row( - children: [ - Icon(Icons.science, color: Get.theme.colorScheme.error), - horizontalSpacing(elementSpacing), - Text( - "settings.experimental".tr, - style: Get.theme.textTheme.bodyMedium!.copyWith(color: Get.theme.colorScheme.error), - ), - horizontalSpacing(elementSpacing) - ], - ), - ) - ], - ), - verticalSpacing(defaultSpacing), - Text("settings.data.social.text".tr, style: Get.theme.textTheme.bodyMedium), - verticalSpacing(defaultSpacing), - const BoolSettingSmall(settingName: DataSettings.socialFeatures), - ], - ), - ), - //* Profile picture Text("settings.data.profile_picture".tr, style: Get.theme.textTheme.labelLarge), verticalSpacing(defaultSpacing), @@ -127,31 +77,29 @@ class DataSettingsPage extends StatelessWidget { horizontalSpacing(defaultSpacing), IconButton( tooltip: "settings.data.profile_picture.remove".tr, - onPressed: () => showConfirmPopup( - ConfirmWindow( - title: "settings.data.profile_picture.remove".tr, - text: "settings.data.profile_picture.remove.confirm".tr, - onConfirm: () async { - // Tell the server to remove the picture - final valid = await ProfileHelper.deleteProfilePicture(); - if (!valid) { - return; - } - }, - onDecline: () => {}, - ), - ), + onPressed: + () => showConfirmPopup( + ConfirmWindow( + title: "settings.data.profile_picture.remove".tr, + text: "settings.data.profile_picture.remove.confirm".tr, + onConfirm: () async { + // Tell the server to remove the picture + final valid = await ProfileHelper.deleteProfilePicture(); + if (!valid) { + return; + } + }, + onDecline: () => {}, + ), + ), icon: Icon(Icons.delete, color: Get.theme.colorScheme.onPrimary), - ) + ), ], - ) + ), ], ), ), - UserAvatar( - id: StatusController.ownAddress, - size: 100, - ) + UserAvatar(id: StatusController.ownAddress, size: 100), ], ), ), @@ -207,11 +155,11 @@ class DataSettingsPage extends StatelessWidget { children: [ Text("display_name".tr, style: Get.theme.textTheme.labelMedium), verticalSpacing(elementSpacing), - Obx( - () => Text( - controller.displayName.value.toLowerCase() == controller.name.value.toLowerCase() - ? List.generate(controller.name.value.length, (index) => "*").join("") - : controller.displayName.value, + Watch( + (ctx) => Text( + StatusController.displayName.value.toLowerCase() == StatusController.name.value.toLowerCase() + ? List.generate(StatusController.name.value.length, (index) => "*").join("") + : StatusController.displayName.value, style: Get.theme.textTheme.bodyMedium, ), ), @@ -244,7 +192,7 @@ class DataSettingsPage extends StatelessWidget { Text("username".tr, style: Get.theme.textTheme.labelMedium), verticalSpacing(elementSpacing), Text( - List.generate(controller.name.value.length, (index) => "*").join(""), + List.generate(StatusController.name.value.length, (index) => "*").join(""), style: Get.theme.textTheme.bodyMedium, ), ], @@ -340,12 +288,14 @@ class DataSettingsPage extends StatelessWidget { FJElevatedButton( smallCorners: true, onTap: () { - showConfirmPopup(ConfirmWindow( - title: "settings.data.danger_zone.delete_account".tr, - text: "settings.data.danger_zone.delete_account.confirm".tr, - onConfirm: () => {}, - onDecline: () => {}, - )); + showConfirmPopup( + ConfirmWindow( + title: "settings.data.danger_zone.delete_account".tr, + text: "settings.data.danger_zone.delete_account.confirm".tr, + onConfirm: () => {}, + onDecline: () => {}, + ), + ); }, color: Get.theme.colorScheme.errorContainer, child: Row( diff --git a/lib/pages/settings/account/invites_page.dart b/lib/pages/settings/account/invites_page.dart index d0bb0fb0..d91d8aa0 100644 --- a/lib/pages/settings/account/invites_page.dart +++ b/lib/pages/settings/account/invites_page.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class InvitesPage extends StatefulWidget { const InvitesPage({super.key}); @@ -16,7 +17,7 @@ class InvitesPage extends StatefulWidget { State createState() => _InvitesPageState(); } -class _InvitesPageState extends State { +class _InvitesPageState extends State with SignalsMixin { @override void initState() { super.initState(); @@ -41,12 +42,12 @@ class _InvitesPageState extends State { } // Data - final _error = "".obs; - final count = 0.obs; - final invites = [].obs; - final loading = false.obs; - final hovering = "".obs; - final generateLoading = false.obs; + late final _error = createSignal(""); + late final count = createSignal(0); + late final invites = createListSignal([]); + late final loading = createSignal(false); + late final hovering = createSignal(""); + late final generateLoading = createSignal(false); /// Generate a new invite code Future generateNewInvite() async { @@ -71,15 +72,13 @@ class _InvitesPageState extends State { Widget build(BuildContext context) { return SettingsPageBase( label: "invites", - child: Obx(() { + child: Watch((ctx) { if (loading.value) { return Padding( padding: const EdgeInsets.only(top: defaultSpacing), child: Padding( padding: const EdgeInsets.all(defaultSpacing), - child: Center( - child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary), - ), + child: Center(child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary)), ), ); } @@ -104,7 +103,12 @@ class _InvitesPageState extends State { if (StatusController.permissions.contains("admin")) Text("settings.invites.title.admin".tr, style: Get.theme.textTheme.headlineMedium) else - Obx(() => Text("settings.invites.title".trParams({"count": count.value.toString()}), style: Get.theme.textTheme.headlineMedium)), + Watch( + (ctx) => Text( + "settings.invites.title".trParams({"count": count.value.toString()}), + style: Get.theme.textTheme.headlineMedium, + ), + ), verticalSpacing(defaultSpacing), Text("settings.invites.description".tr, style: Get.theme.textTheme.bodyMedium), verticalSpacing(defaultSpacing), @@ -127,7 +131,7 @@ class _InvitesPageState extends State { verticalSpacing(defaultSpacing), Text("settings.invites.history.description".tr, style: Get.theme.textTheme.bodyMedium), verticalSpacing(defaultSpacing), - Obx(() { + Watch((ctx) { if (invites.isEmpty) { return Text("settings.invites.history.empty".tr, style: Get.theme.textTheme.labelMedium); } @@ -155,17 +159,12 @@ class _InvitesPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Obx( - () => MouseRegion( + Watch( + (ctx) => MouseRegion( onEnter: (_) => hovering.value = invite, onExit: (_) => hovering.value = "", child: Animate( - effects: [ - BlurEffect( - end: const Offset(5, 5), - duration: 100.ms, - ) - ], + effects: [BlurEffect(end: const Offset(5, 5), duration: 100.ms)], onInit: (controller) { controller.value = 1.0; }, @@ -179,7 +178,7 @@ class _InvitesPageState extends State { Clipboard.setData(ClipboardData(text: invite)); }, icon: Icon(Icons.copy, color: Get.theme.colorScheme.onPrimary), - ) + ), ], ), ), diff --git a/lib/pages/settings/account/key_requests_window.dart b/lib/pages/settings/account/key_requests_window.dart index 2d390a15..0710ec61 100644 --- a/lib/pages/settings/account/key_requests_window.dart +++ b/lib/pages/settings/account/key_requests_window.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/connection/encryption/hash.dart'; -import 'package:chat_interface/connection/encryption/signatures.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/hash.dart'; +import 'package:chat_interface/util/encryption/signatures.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; import 'package:chat_interface/controller/current/steps/account_step.dart'; import 'package:chat_interface/pages/status/error/error_container.dart'; import 'package:chat_interface/controller/current/steps/key_step.dart'; @@ -20,6 +20,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class KeyRequest { final String session; @@ -28,7 +29,7 @@ class KeyRequest { final String payload; final String signature; final int createdAt; - final processing = false.obs; + final processing = signal(false); KeyRequest({ required this.session, @@ -61,6 +62,11 @@ class KeyRequest { }; } + /// Dispose all the signals related to the key request + void dispose() { + processing.dispose(); + } + Future updateStatus(bool delete, Function() success) async { // Make the payload late final String payload; @@ -105,10 +111,10 @@ class KeyRequestsWindow extends StatefulWidget { State createState() => _KeyRequestsWindowState(); } -class _KeyRequestsWindowState extends State { - final loading = false.obs; - final error = "".obs; - final requests = [].obs; +class _KeyRequestsWindowState extends State with SignalsMixin { + late final _loading = createSignal(false); + late final _error = createSignal(""); + late final _requests = createListSignal([]); Timer? _timer; @override @@ -122,23 +128,27 @@ class _KeyRequestsWindowState extends State { @override void dispose() { + for (var req in _requests) { + req.dispose(); + } + _timer?.cancel(); super.dispose(); } Future requestKeyRequests() async { - loading.value = true; + _loading.value = true; // Get the key synchronization requests from the server final json = await postAuthorizedJSON("/account/keys/requests/list", {}); if (!json["success"]) { - error.value = (json["error"] as String).tr; - loading.value = false; + _error.value = (json["error"] as String).tr; + _loading.value = false; return; } - error.value = ""; - loading.value = false; + _error.value = ""; + _loading.value = false; // Parse all the requests for (var request in json["requests"]) { @@ -146,8 +156,8 @@ class _KeyRequestsWindowState extends State { if (keyRequest.payload != "") { continue; } - if (!requests.any((element) => keyRequest.session == element.session)) { - requests.add(keyRequest); + if (!_requests.any((element) => keyRequest.session == element.session)) { + _requests.add(keyRequest); } } } @@ -157,114 +167,106 @@ class _KeyRequestsWindowState extends State { return DialogBase( title: [ Expanded( - child: Text( - "Synchronization requests".tr, - style: Get.theme.textTheme.labelLarge, - overflow: TextOverflow.ellipsis, - )), - Obx( - () => Visibility( - visible: loading.value, - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - color: Get.theme.colorScheme.onPrimary, - ), - ), + child: Text( + "Synchronization requests".tr, + style: Get.theme.textTheme.labelLarge, + overflow: TextOverflow.ellipsis, ), - ) + ), + Visibility( + visible: _loading.value, + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary), + ), + ), ], child: Column( mainAxisSize: MainAxisSize.min, children: [ - AnimatedErrorContainer( - expand: true, - padding: const EdgeInsets.only(bottom: defaultSpacing), - message: error, - ), - Obx(() { - // Check if the requests are empty - if (requests.isEmpty) { - return InfoContainer( - expand: true, - message: "key_requests.empty".tr, - ); - } + AnimatedErrorContainer(expand: true, padding: const EdgeInsets.only(bottom: defaultSpacing), message: _error), + Builder( + builder: (context) { + // Check if the requests are empty + if (_requests.isEmpty) { + return InfoContainer(expand: true, message: "key_requests.empty".tr); + } - // Render the requests (if not empty) - return Column( - children: List.generate(requests.length, (index) { - final request = requests[index]; - return Padding( - padding: EdgeInsets.only(top: index == 0 ? 0 : defaultSpacing), - child: Container( - padding: const EdgeInsets.all(defaultSpacing), - decoration: BoxDecoration( - color: Get.theme.colorScheme.inverseSurface, - borderRadius: BorderRadius.circular(defaultSpacing), - ), - child: Row( - children: [ - Icon(Icons.key, color: Get.theme.colorScheme.onPrimary), - horizontalSpacing(defaultSpacing), - Expanded( - child: Text( - formatGeneralTime(DateTime.fromMillisecondsSinceEpoch(request.createdAt)), - style: Get.textTheme.labelMedium, - overflow: TextOverflow.ellipsis, + // Render the requests (if not empty) + return Column( + children: List.generate(_requests.length, (index) { + final request = _requests[index]; + return Padding( + padding: EdgeInsets.only(top: index == 0 ? 0 : defaultSpacing), + child: Container( + padding: const EdgeInsets.all(defaultSpacing), + decoration: BoxDecoration( + color: Get.theme.colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(defaultSpacing), + ), + child: Row( + children: [ + Icon(Icons.key, color: Get.theme.colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Expanded( + child: Text( + formatGeneralTime(DateTime.fromMillisecondsSinceEpoch(request.createdAt)), + style: Get.textTheme.labelMedium, + overflow: TextOverflow.ellipsis, + ), ), - ), - horizontalSpacing(defaultSpacing), - Obx(() { - if (request.processing.value) { - return SizedBox( - width: 31, - height: 31, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: CircularProgressIndicator( - strokeWidth: 3.0, - color: Get.theme.colorScheme.onPrimary, + horizontalSpacing(defaultSpacing), + Watch((ctx) { + if (request.processing.value) { + return SizedBox( + width: 31, + height: 31, + child: Padding( + padding: const EdgeInsets.all(6.0), + child: CircularProgressIndicator( + strokeWidth: 3.0, + color: Get.theme.colorScheme.onPrimary, + ), ), - ), - ); - } + ); + } - return Row( - children: [ - LoadingIconButton( - onTap: () async { - final result = await Get.dialog(KeyRequestAcceptWindow(request: request)); - if (result != null && result) { - requests.remove(request); - } - }, - padding: 0, - extra: defaultSpacing, - icon: Icons.check, - ), - horizontalSpacing(elementSpacing), - LoadingIconButton( - onTap: () { - request.updateStatus(true, () { - requests.remove(request); - }); - }, - padding: 0, - extra: defaultSpacing, - icon: Icons.close, - ), - ], - ); - }), - ], + return Row( + children: [ + LoadingIconButton( + onTap: () async { + final result = await Get.dialog(KeyRequestAcceptWindow(request: request)); + if (result != null && result) { + _requests.remove(request); + } + }, + padding: 0, + extra: defaultSpacing, + icon: Icons.check, + ), + horizontalSpacing(elementSpacing), + LoadingIconButton( + onTap: () { + request.updateStatus(true, () { + _requests.remove(request); + }); + }, + padding: 0, + extra: defaultSpacing, + icon: Icons.close, + ), + ], + ); + }), + ], + ), ), - ), - ); - }), - ); - }) + ); + }), + ); + }, + ), ], ), ); @@ -280,8 +282,8 @@ class KeyRequestAcceptWindow extends StatefulWidget { State createState() => _KeyRequestAcceptWindowState(); } -class _KeyRequestAcceptWindowState extends State { - final _error = "".obs; +class _KeyRequestAcceptWindowState extends State with SignalsMixin { + late final _error = createSignal(""); final TextEditingController _codeController = TextEditingController(); @override @@ -293,18 +295,12 @@ class _KeyRequestAcceptWindowState extends State { @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text("key_requests.code.title".tr, style: Get.theme.textTheme.labelLarge), - ], + title: [Text("key_requests.code.title".tr, style: Get.theme.textTheme.labelLarge)], child: Column( children: [ Text("key_requests.code.description".tr, style: Get.theme.textTheme.bodyMedium), verticalSpacing(defaultSpacing), - AnimatedErrorContainer( - expand: true, - padding: const EdgeInsets.only(bottom: defaultSpacing), - message: _error, - ), + AnimatedErrorContainer(expand: true, padding: const EdgeInsets.only(bottom: defaultSpacing), message: _error), FJTextField( controller: _codeController, hintText: "key_requests.code.placeholder".tr, // DRa6KS @@ -315,8 +311,11 @@ class _KeyRequestAcceptWindowState extends State { label: "key_requests.code.button".tr, onTap: () { // Verify the code - if (!checkSignature(widget.request.signature, widget.request.signaturePub, - hashSha(_codeController.text + packagePublicKey(widget.request.encryptionPub)))) { + if (!checkSignature( + widget.request.signature, + widget.request.signaturePub, + hashSha(_codeController.text + packagePublicKey(widget.request.encryptionPub)), + )) { _error.value = "key_requests.code.error".tr; // JEeSqn return; } diff --git a/lib/pages/settings/account/log_out_window.dart b/lib/pages/settings/account/log_out_window.dart index 2ab8012e..0d6a8107 100644 --- a/lib/pages/settings/account/log_out_window.dart +++ b/lib/pages/settings/account/log_out_window.dart @@ -1,10 +1,11 @@ -import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/services/chat/status_service.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_switch.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class LogOutWindow extends StatefulWidget { const LogOutWindow({super.key}); @@ -14,7 +15,7 @@ class LogOutWindow extends StatefulWidget { } class _ChangeNameWindowState extends State { - final _deleteFiles = false.obs; + final _deleteFiles = signal(false); @override Widget build(BuildContext context) { @@ -30,12 +31,7 @@ class _ChangeNameWindowState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("log_out.delete_files".tr, style: Get.theme.textTheme.bodyMedium), - Obx( - () => FJSwitch( - value: _deleteFiles.value, - onChanged: (value) => _deleteFiles.value = value, - ), - ), + Watch((ctx) => FJSwitch(value: _deleteFiles.value, onChanged: (value) => _deleteFiles.value = value)), ], ), verticalSpacing(defaultSpacing), @@ -44,7 +40,7 @@ class _ChangeNameWindowState extends State { Expanded( child: FJElevatedButton( onTap: () async { - await Get.find().logOut(deleteEverything: true, deleteFiles: _deleteFiles.value); + await StatusService.logOut(deleteEverything: true, deleteFiles: _deleteFiles.value); }, child: Center(child: Text("yes".tr, style: Get.theme.textTheme.labelLarge)), ), @@ -57,7 +53,7 @@ class _ChangeNameWindowState extends State { ), ), ], - ) + ), ], ), ); diff --git a/lib/pages/settings/app/audio_settings.dart b/lib/pages/settings/app/audio_settings.dart new file mode 100644 index 00000000..91aafcd6 --- /dev/null +++ b/lib/pages/settings/app/audio_settings.dart @@ -0,0 +1,327 @@ +import 'dart:async'; + +import 'package:chat_interface/controller/spaces/studio/studio_device_manager.dart'; +import 'package:chat_interface/pages/settings/components/bool_selection_small.dart'; +import 'package:chat_interface/pages/settings/components/double_selection.dart'; +import 'package:chat_interface/pages/settings/components/list_selection.dart'; +import 'package:chat_interface/pages/settings/data/entities.dart'; +import 'package:chat_interface/pages/settings/data/settings_controller.dart'; +import 'package:chat_interface/pages/settings/settings_page_base.dart'; +import 'package:chat_interface/src/rust/api/engine.dart' as libspace; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class AudioSettings { + /// Value for the device selection to use the default device + static final String useDefaultDevice = "def"; + + /// The currently selected microphone + static Setting microphone = Setting("microphone", useDefaultDevice); + + /// The activation mode for the microphone + static Setting microphoneActivationMode = Setting("microphone.activation_mode", 0); + static final activationModes = [ + SelectableItem("microphone.mode.voice_activity", Icons.graphic_eq), + SelectableItem("microphone.mode.always_on", Icons.radio_button_checked), + ]; + + /// The bitrate for sending audio to the server + static Setting audioBitrateMode = Setting("audio.encoding.mode", 0); + static final audioBitrateModes = [ + SelectableItem("audio.encoding.mode.auto", Icons.auto_awesome), + SelectableItem("audio.encoding.mode.max", Icons.speed), + SelectableItem("audio.encoding.mode.high", Icons.signal_cellular_alt), + SelectableItem("audio.encoding.mode.medium", Icons.signal_cellular_alt_2_bar), + SelectableItem("audio.encoding.mode.low", Icons.signal_cellular_alt_1_bar), + ]; + + /// All the activation modes for the microphone + + /// If the sensitivity should be determined automatically + static Setting automaticVoiceActivity = Setting("microphone.auto_activity", true); + + /// The threshold for the microphone to activate + static Setting microphoneSensitivity = Setting("microphone.sensitivity", -50); + + /// The currently selected output device + static Setting outputDevice = Setting("output_device", useDefaultDevice); + + static void addSettings() { + SettingController.addSetting(microphone); + SettingController.addSetting(microphoneActivationMode); + SettingController.addSetting(automaticVoiceActivity); + SettingController.addSetting(microphoneSensitivity); + + SettingController.addSetting(outputDevice); + } + + static void applyBitrate(libspace.LightwireEngine engine) { + switch (audioBitrateMode.getValue()) { + case 0: + libspace.setEncodingBitrate(engine: engine, auto: true, max: false, bitrate: 0); + case 1: + libspace.setEncodingBitrate(engine: engine, auto: false, max: true, bitrate: 0); + case 2: + libspace.setEncodingBitrate(engine: engine, auto: false, max: false, bitrate: 144000); + case 3: + libspace.setEncodingBitrate(engine: engine, auto: false, max: false, bitrate: 96000); + case 4: + libspace.setEncodingBitrate(engine: engine, auto: false, max: false, bitrate: 40000); + } + } + + /// Apply all the audio settings to a lightwire engine. + /// + /// Returns all the dispose functions for the signals. + static Future> subscribeToSettings(libspace.LightwireEngine engine) async { + final list = []; + + list.add( + AudioSettings.microphoneActivationMode.value.subscribe((value) { + libspace.setActivityDetection(engine: engine, enabled: AudioSettings.microphoneActivationMode.getValue() == 0); + }), + ); + await libspace.setActivityDetection( + engine: engine, + enabled: AudioSettings.microphoneActivationMode.getValue() == 0, + ); + list.add( + AudioSettings.automaticVoiceActivity.value.subscribe((value) { + libspace.setAutomaticDetection(engine: engine, enabled: AudioSettings.automaticVoiceActivity.getValue()); + }), + ); + await libspace.setAutomaticDetection(engine: engine, enabled: AudioSettings.automaticVoiceActivity.getValue()); + list.add( + AudioSettings.microphoneSensitivity.value.subscribe((value) { + libspace.setTalkingAmplitude(engine: engine, amplitude: AudioSettings.microphoneSensitivity.getValue()); + }), + ); + await libspace.setTalkingAmplitude(engine: engine, amplitude: AudioSettings.microphoneSensitivity.getValue()); + list.add( + AudioSettings.audioBitrateMode.value.subscribe((mode) { + AudioSettings.applyBitrate(engine); + }), + ); + AudioSettings.applyBitrate(engine); + + return list; + } +} + +class AudioSettingsPage extends StatefulWidget { + const AudioSettingsPage({super.key}); + + @override + State createState() => _AudioSettingsPageState(); +} + +class _AudioSettingsPageState extends State { + // State for the talking indicator + final _speechDetected = signal(false); + late final _talking = computed(() { + if (AudioSettings.microphoneActivationMode.getValue() == 1) { + return true; + } + + return _speechDetected.value; + }); + + final _engine = signal(null); + final _disposeFunctions = []; + + @override + void initState() { + initLightwire(); + super.initState(); + } + + // Initialize the lightwire engine + Future initLightwire() async { + _engine.value = await libspace.createLightwireEngine(); + + // Start the engine + libspace.startPacketStream(engine: _engine.peek()!).listen((packet) { + final (_, _, speech) = packet; + _speechDetected.value = speech ?? false; + }); + + // Set the selected devices + await libspace.setInputDevice(engine: _engine.peek()!, device: AudioSettings.microphone.getValue()); + await libspace.setOutputDevice(engine: _engine.peek()!, device: AudioSettings.microphone.getValue()); + + // Add subscriptions to automatically update the engine + _disposeFunctions.addAll(await AudioSettings.subscribeToSettings(_engine.peek()!)); + + // Enable the packet sending + await libspace.setVoiceEnabled(engine: _engine.peek()!, enabled: true); + } + + @override + void dispose() { + // Dispose all the subscriptions + for (var func in _disposeFunctions) { + func(); + } + + // Stop the engine + if (_engine.peek() != null) { + libspace.stopEngine(engine: _engine.peek()!); + } + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SettingsPageBase( + label: "audio", + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Render a list selection for the microphones + Text("settings.audio.microphone".tr, style: theme.textTheme.labelLarge), + verticalSpacing(defaultSpacing), + Watch((ctx) => _engine.value == null ? SizedBox() : MicrophoneSelection(engine: _engine.value!)), + verticalSpacing(sectionSpacing), + + // Render a selection for the output device + Text("settings.audio.output_device".tr, style: theme.textTheme.labelLarge), + verticalSpacing(defaultSpacing), + Watch((ctx) => _engine.value == null ? SizedBox() : OutputDeviceSelection(engine: _engine.value!)), + verticalSpacing(sectionSpacing), + + // The selection for the microphone activation mode + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text("settings.audio.activation_mode".tr, style: theme.textTheme.labelLarge), + verticalSpacing(defaultSpacing), + Text("settings.audio.activation_mode.desc".tr, style: theme.textTheme.bodyMedium), + verticalSpacing(sectionSpacing), + ], + ), + Watch( + (ctx) => Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: _talking.value ? theme.colorScheme.onPrimary : theme.colorScheme.primary, + borderRadius: BorderRadius.circular(defaultSpacing), + ), + ), + ), + ], + ), + ListSelectionSetting(setting: AudioSettings.microphoneActivationMode, items: AudioSettings.activationModes), + verticalSpacing(defaultSpacing), + + // Only render automatic sensitivity detection when + Watch( + (ctx) => Visibility( + visible: AudioSettings.microphoneActivationMode.getValue() == 0, + child: Padding( + padding: const EdgeInsets.only(bottom: defaultSpacing), + child: BoolSettingSmall(settingName: AudioSettings.automaticVoiceActivity.label), + ), + ), + ), + + // Only render the sensitivity slider in case automatic detection is off + Watch( + (ctx) => Visibility( + visible: + AudioSettings.microphoneActivationMode.getValue() == 0 && + !AudioSettings.automaticVoiceActivity.getValue(), + child: DoubleSelectionSetting( + settingName: AudioSettings.microphoneSensitivity.label, + description: "settings.audio.microphone.sensitivity.desc", + min: -100, + max: 0, + unit: "dB", + ), + ), + ), + verticalSpacing(sectionSpacing), + + // Render a selection for the encoding mode + Text("settings.audio.advanced".tr, style: theme.textTheme.labelLarge), + verticalSpacing(defaultSpacing), + Text("audio.encoding.mode".tr, style: theme.textTheme.bodyMedium), + verticalSpacing(defaultSpacing), + ListSelectionSetting(setting: AudioSettings.audioBitrateMode, items: AudioSettings.audioBitrateModes), + ], + ), + ); + } +} + +class MicrophoneSelection extends StatelessWidget { + final libspace.LightwireEngine engine; + final bool secondary; + + const MicrophoneSelection({super.key, required this.engine, this.secondary = false}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Watch((ctx) { + // Make sure to show a message in case there are no devices + if (StudioDeviceManager.microphones.value.isEmpty) { + return Text("settings.audio.devices_empty".tr, style: theme.textTheme.bodyMedium); + } + + return ListSelection( + selected: StudioDeviceManager.selectedMicrophone, + items: StudioDeviceManager.microphones.value, + secondary: secondary, + callback: (item, i) { + if (i == 0) { + AudioSettings.microphone.setValue(AudioSettings.useDefaultDevice); + } else { + AudioSettings.microphone.setValue(item.label); + } + libspace.setInputDevice(engine: engine, device: AudioSettings.microphone.getValue()); + }, + ); + }); + } +} + +class OutputDeviceSelection extends StatelessWidget { + final libspace.LightwireEngine engine; + final bool secondary; + + const OutputDeviceSelection({super.key, required this.engine, this.secondary = false}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Watch((ctx) { + // Make sure to show a message in case there are no devices + if (StudioDeviceManager.outputDevices.value.isEmpty) { + return Text("settings.audio.devices_empty".tr, style: theme.textTheme.bodyMedium); + } + + return ListSelection( + selected: StudioDeviceManager.selectedOutputDevice, + items: StudioDeviceManager.outputDevices.value, + secondary: secondary, + callback: (item, i) { + if (i == 0) { + AudioSettings.outputDevice.setValue(AudioSettings.useDefaultDevice); + } else { + AudioSettings.outputDevice.setValue(item.label); + } + libspace.setOutputDevice(engine: engine, device: AudioSettings.outputDevice.getValue()); + }, + ); + }); + } +} diff --git a/lib/pages/settings/app/general_settings.dart b/lib/pages/settings/app/general_settings.dart index 8efb24c5..6ed3774a 100644 --- a/lib/pages/settings/app/general_settings.dart +++ b/lib/pages/settings/app/general_settings.dart @@ -26,15 +26,15 @@ class GeneralSettings { static const String ringOnInvite = "ring.enable"; static const String ringIgnoreTray = "ring.ignore_tray"; - static void addSettings(SettingController controller) { - controller.settings[language] = Setting(language, 0); + static void addSettings() { + SettingController.addSetting(Setting(language, 0)); // Default notification sounds settings - controller.settings[soundsEnabled] = Setting(soundsEnabled, true); - controller.settings[soundsDoNotDisturb] = Setting(soundsDoNotDisturb, false); - controller.settings[soundsOnlyWhenTray] = Setting(soundsOnlyWhenTray, true); - controller.settings[ringOnInvite] = Setting(ringOnInvite, true); - controller.settings[ringIgnoreTray] = Setting(ringIgnoreTray, true); + SettingController.addSetting(Setting(soundsEnabled, true)); + SettingController.addSetting(Setting(soundsDoNotDisturb, false)); + SettingController.addSetting(Setting(soundsOnlyWhenTray, true)); + SettingController.addSetting(Setting(ringOnInvite, true)); + SettingController.addSetting(Setting(ringIgnoreTray, true)); } } @@ -58,7 +58,7 @@ class _GeneralSettingsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - //* Notification settings + //* Notification settings6 Row( children: [ Text("settings.general.notifications".tr, style: Get.theme.textTheme.labelLarge), @@ -104,10 +104,7 @@ class _GeneralSettingsPageState extends State { ], ), verticalSpacing(defaultSpacing), - InfoContainer( - message: "settings.general.ringtone.disabled".tr, - expand: true, - ), + InfoContainer(message: "settings.general.ringtone.disabled".tr, expand: true), verticalSpacing(defaultSpacing), Text("ring.desc".tr, style: Get.textTheme.bodyMedium), verticalSpacing(defaultSpacing), @@ -121,7 +118,7 @@ class _GeneralSettingsPageState extends State { verticalSpacing(defaultSpacing), ListSelectionSetting( - settingName: "language", + setting: SettingController.settings[GeneralSettings.language]! as Setting, items: GeneralSettings.languages, callback: (language) { Get.updateLocale((language as LanguageSelection).locale); diff --git a/lib/pages/settings/app/log_settings.dart b/lib/pages/settings/app/log_settings.dart index 4f87a981..a966ba24 100644 --- a/lib/pages/settings/app/log_settings.dart +++ b/lib/pages/settings/app/log_settings.dart @@ -42,7 +42,11 @@ class LogManager { // Initialize the newest log file currentLogFile = File( - path.join(loggingDirectory!.path, "log-${DateTime.now().toUtc().toString().replaceAll(" ", "_").replaceAll(":", "-").split(".")[0]}.txt")); + path.join( + loggingDirectory!.path, + "log-${DateTime.now().toUtc().toString().replaceAll(" ", "_").replaceAll(":", "-").split(".")[0]}.txt", + ), + ); await currentLogFile!.create(); return true; @@ -68,16 +72,18 @@ class LogManager { /// Custom log function copied from GetUtils.printFunction to write things to the file too static void _errorLogFunction(String prefix, dynamic value, String info, {isError = false}) { - currentLogFile! - .writeAsStringSync("${DateTime.now().toUtc()}: ${isError ? "error" : "info"}: ${value.toString()} ($info) \n", mode: FileMode.append); + currentLogFile!.writeAsStringSync( + "${DateTime.now().toUtc()}: ${isError ? "error" : "info"}: ${value.toString()} ($info) \n", + mode: FileMode.append, + ); } } class LogSettings { static String amountOfLogs = "logging.amount"; - static void registerSettings(SettingController controller) { - controller.settings[amountOfLogs] = Setting(amountOfLogs, 5); + static void addSettings() { + SettingController.addSetting(Setting(amountOfLogs, 5)); } } @@ -104,18 +110,12 @@ class LogSettingsPage extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.launch, - color: Get.theme.colorScheme.onPrimary, - ), + Icon(Icons.launch, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(elementSpacing), - Text( - "logging.launch".tr, - style: Get.textTheme.labelLarge, - ), + Text("logging.launch".tr, style: Get.textTheme.labelLarge), ], ), - ) + ), ], ), ); diff --git a/lib/pages/settings/appearance/call_preview.dart b/lib/pages/settings/appearance/call_preview.dart deleted file mode 100644 index 502ec97e..00000000 --- a/lib/pages/settings/appearance/call_preview.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:chat_interface/pages/settings/data/settings_controller.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -class CallPreview extends StatefulWidget { - const CallPreview({super.key}); - - @override - State createState() => _CallPreviewState(); -} - -class _CallPreviewState extends State { - @override - Widget build(BuildContext context) { - ThemeData theme = Theme.of(context); - SettingController controller = Get.find(); - - return Padding( - padding: const EdgeInsets.all(defaultSpacing * 0.5), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Column( - children: [ - //* Top preview - Obx(() => Visibility( - visible: controller.settings["call_app.expansionPosition"]!.getValue() == 0, - child: Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.only(bottom: defaultSpacing), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: _buildEntities(theme, 0, defaultSpacing)))))), - - Expanded( - flex: 3, - child: Row( - children: [ - //* Left preview - Obx(() => Visibility( - visible: controller.settings["call_app.expansionPosition"]!.getValue() == 3, - child: Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.only(right: defaultSpacing), - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: _buildEntities(theme, defaultSpacing, 0)))))), - - Expanded( - flex: 3, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(defaultSpacing), - color: theme.colorScheme.primaryContainer, - ), - ), - ), - - //* Right preview - Obx(() => Visibility( - visible: controller.settings["call_app.expansionPosition"]!.getValue() == 1, - child: Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.only(left: defaultSpacing), - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: _buildEntities(theme, defaultSpacing, 0)))))), - ], - ), - ), - - //* Bottom preview - Obx(() => Visibility( - visible: controller.settings["call_app.expansionPosition"]!.getValue() == 2, - child: Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.only(top: defaultSpacing), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: _buildEntities(theme, 0, defaultSpacing)))))), - ], - )), - ); - } - - // Build the entities for the preview - List _buildEntities(ThemeData theme, double bottom, double right) { - return [ - Padding( - padding: EdgeInsets.only(bottom: bottom, right: right), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.tertiary, - borderRadius: BorderRadius.circular(defaultSpacing), - border: Border.all(color: Colors.green, width: 2), - ), - alignment: Alignment.bottomLeft, - child: SizedBox( - height: 25, - child: Padding( - padding: const EdgeInsets.all(defaultSpacing * 0.5), - child: Container( - width: 60, - decoration: BoxDecoration( - color: theme.colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(defaultSpacing), - ))), - )), - ), - ), - Padding( - padding: EdgeInsets.only(bottom: bottom, right: right), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(defaultSpacing), - ), - alignment: Alignment.bottomLeft, - child: SizedBox( - height: 25, - child: Padding( - padding: const EdgeInsets.all(defaultSpacing * 0.5), - child: Container( - width: 50, - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(defaultSpacing), - ))), - )), - ), - ), - Padding( - padding: EdgeInsets.only(bottom: bottom, right: right), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.error, - borderRadius: BorderRadius.circular(defaultSpacing), - ), - alignment: Alignment.bottomLeft, - child: SizedBox( - height: 25, - child: Padding( - padding: const EdgeInsets.all(defaultSpacing * 0.5), - child: Container( - width: 75, - decoration: BoxDecoration( - color: theme.colorScheme.errorContainer, - borderRadius: BorderRadius.circular(defaultSpacing), - ))), - )), - ), - ) - ]; - } -} diff --git a/lib/pages/settings/appearance/call_settings.dart b/lib/pages/settings/appearance/call_settings.dart deleted file mode 100644 index b6aacbd1..00000000 --- a/lib/pages/settings/appearance/call_settings.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:chat_interface/pages/settings/appearance/call_preview.dart'; -import 'package:chat_interface/pages/settings/components/list_selection.dart'; -import 'package:chat_interface/pages/settings/data/entities.dart'; -import 'package:chat_interface/pages/settings/data/settings_controller.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -void addCallAppearanceSettings(SettingController controller) { - // If participants should be shown in a maximized screenshare - // 0 - Scroll view, 1 - Talking overlay, 2 - None - controller.addSetting(Setting("call_app.expansionMode", 0)); - - // Postion of the participants in a maximized screenshare - // 0 - Top, 1 - Right, 2 - Bottom, 3 - Left - controller.addSetting(Setting("call_app.expansionPosition", 1)); -} - -class CallSettingsPage extends StatefulWidget { - const CallSettingsPage({super.key}); - - @override - State createState() => _CallSettingsPageState(); -} - -class _CallSettingsPageState extends State { - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.topLeft, - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //* Preview - Text("call_app.preview".tr, style: Get.theme.textTheme.labelLarge), - verticalSpacing(defaultSpacing * 0.5), - - const CallPreview(), - verticalSpacing(defaultSpacing), - - //* Expansion mode - Text("expansion.mode".tr, style: Get.theme.textTheme.labelLarge), - verticalSpacing(defaultSpacing * 0.5), - - ListSelectionSetting(settingName: "call_app.expansionMode", items: [ - SelectableItem("expansion.scroll_view".tr, Icons.view_list), - SelectableItem("expansion.talking_overlay".tr, Icons.people), - SelectableItem("expansion.none".tr, Icons.close), - ]), - - //* Expansion positions - Text("expansion.position".tr, style: Get.theme.textTheme.labelLarge), - verticalSpacing(defaultSpacing * 0.5), - - ListSelectionSetting(settingName: "call_app.expansionPosition", items: [ - SelectableItem("expansion.top".tr, Icons.arrow_upward), - SelectableItem("expansion.right".tr, Icons.arrow_forward), - SelectableItem("expansion.bottom".tr, Icons.arrow_downward), - SelectableItem("expansion.left".tr, Icons.arrow_back), - ]), - ], - ), - ), - ); - } -} diff --git a/lib/pages/settings/appearance/chat_settings.dart b/lib/pages/settings/appearance/chat_settings.dart index 6cd3e8b4..48562bfd 100644 --- a/lib/pages/settings/appearance/chat_settings.dart +++ b/lib/pages/settings/appearance/chat_settings.dart @@ -1,3 +1,4 @@ +import 'package:chat_interface/pages/settings/components/double_selection.dart'; import 'package:chat_interface/pages/settings/components/list_selection.dart'; import 'package:chat_interface/pages/settings/data/entities.dart'; import 'package:chat_interface/pages/settings/data/settings_controller.dart'; @@ -7,23 +8,17 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; class ChatSettings { + static final dotAmount = Setting("appearance.chat.dot_amount", 3); static const String chatTheme = "appearance.chat.theme"; - static final Setting chatThemeSetting = Setting(chatTheme, 1); static final chatThemes = [ - SelectableItem( - "appearance.chat.theme.material".tr, - Icons.view_list, - experimental: true, - ), - SelectableItem( - "appearance.chat.theme.bubbles".tr, - Icons.comment, - ), + SelectableItem("appearance.chat.theme.material".tr, Icons.view_list, experimental: true), + SelectableItem("appearance.chat.theme.bubbles".tr, Icons.comment), ]; - static void registerSettings(SettingController controller) { - controller.settings[chatTheme] = chatThemeSetting; + static void addSettings() { + SettingController.addSetting(Setting(chatTheme, 1)); + SettingController.addSetting(dotAmount); } } @@ -42,14 +37,26 @@ class _ChatSettingsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - //* Chat theme + // Chat theme Text("appearance.chat.theme".tr, style: Get.theme.textTheme.labelLarge), verticalSpacing(defaultSpacing), - ListSelectionSetting( - settingName: ChatSettings.chatTheme, + setting: SettingController.settings[ChatSettings.chatTheme]! as Setting, items: ChatSettings.chatThemes, ), + verticalSpacing(sectionSpacing), + + Text("appearance.chat.dot_amount.title".tr, style: Get.theme.textTheme.labelLarge), + verticalSpacing(defaultSpacing), + + // How many dots appear after Create in the create window (VERY IMPORTANT) + DoubleSelectionSetting( + settingName: ChatSettings.dotAmount.label, + description: "appearance.chat.dot_amount", + min: 1, + max: 5, + rounded: true, + ), ], ), ); diff --git a/lib/pages/settings/appearance/color_generator.dart b/lib/pages/settings/appearance/color_generator.dart index a5536b49..93964cc3 100644 --- a/lib/pages/settings/appearance/color_generator.dart +++ b/lib/pages/settings/appearance/color_generator.dart @@ -11,19 +11,25 @@ class ColorFactory { const ColorFactory(this.primHue, this.secHue, this.sat, this.lum, this.themeMode, this.lumJumps, this.backgroundMode); Color getPrimary() => HSLColor.fromAHSL(1.0, primHue, sat, themeMode == -1 ? _iconBlack : _iconWhite).toColor(); - Color getPrimaryContainer() => HSLColor.fromAHSL(1.0, primHue, sat, themeMode == -1 ? _containerBlack : _containerWhite).toColor(); + Color getPrimaryContainer() => + HSLColor.fromAHSL(1.0, primHue, sat, themeMode == -1 ? _containerBlack : _containerWhite).toColor(); Color getSecondary() => HSLColor.fromAHSL(1.0, secHue, sat, themeMode == -1 ? _iconBlack : _iconWhite).toColor(); - Color getSecondaryContainer() => HSLColor.fromAHSL(1.0, secHue, sat, themeMode == -1 ? _containerBlack : _containerWhite).toColor(); + Color getSecondaryContainer() => + HSLColor.fromAHSL(1.0, secHue, sat, themeMode == -1 ? _containerBlack : _containerWhite).toColor(); Color getBackground1() => HSLColor.fromAHSL(1.0, primHue, backgroundMode == 1 ? sat : 0.03, lum).toColor(); - Color getBackground2() => HSLColor.fromAHSL(1.0, primHue, backgroundMode == 1 ? sat : 0.03, clampDouble(lum - 0.03, 0.0, 1.0)).toColor(); - Color getBackground3() => HSLColor.fromAHSL(1.0, primHue, backgroundMode == 1 ? sat : 0.03, clampDouble(lum - 0.06, 0.0, 1.0)).toColor(); + Color getBackground2() => + HSLColor.fromAHSL(1.0, primHue, backgroundMode == 1 ? sat : 0.03, clampDouble(lum - 0.03, 0.0, 1.0)).toColor(); + Color getBackground3() => + HSLColor.fromAHSL(1.0, primHue, backgroundMode == 1 ? sat : 0.03, clampDouble(lum - 0.06, 0.0, 1.0)).toColor(); Color completeBackground() => HSLColor.fromAHSL(1.0, primHue, backgroundMode == 1 ? sat : 0, 0.0).toColor(); - Color customHue(double hue) => HSLColor.fromAHSL(1.0, hue * 360.0, sat, themeMode == -1 ? _iconBlack : _iconWhite).toColor(); - Color customHueContainer(double hue) => HSLColor.fromAHSL(1.0, hue * 360.0, sat, themeMode == -1 ? _containerBlack : _containerWhite).toColor(); + Color customHue(double hue) => + HSLColor.fromAHSL(1.0, hue * 360.0, sat, themeMode == -1 ? _iconBlack : _iconWhite).toColor(); + Color customHueContainer(double hue) => + HSLColor.fromAHSL(1.0, hue * 360.0, sat, themeMode == -1 ? _containerBlack : _containerWhite).toColor(); Color getFontColor() { final hsl = HSLColor.fromColor(getBackground1()); @@ -43,8 +49,7 @@ class ColorFactory { } ColorFactory buildColorFactoryFromSettings() { - final SettingController controller = Get.find(); - var index = controller.settings[ThemeSettings.themePreset]!.getValue() as int; + var index = SettingController.settings[ThemeSettings.themePreset]!.getValue() as int; if (index > ThemeSettings.customThemeIndex) { index = ThemeSettings.customThemeIndex; } @@ -60,17 +65,24 @@ ColorFactory buildColorFactoryFromSettings() { var backgroundMode = preset.backgroundMode; if (index == ThemeSettings.customThemeIndex) { - primHue = controller.settings[ThemeSettings.primaryHue]!.getValue() * 360.0; - secHue = controller.settings[ThemeSettings.secondaryHue]!.getValue() * 360.0; - sat = controller.settings[ThemeSettings.baseSaturation]!.getValue() as double; + primHue = SettingController.settings[ThemeSettings.primaryHue]!.getValue() * 360.0; + secHue = SettingController.settings[ThemeSettings.secondaryHue]!.getValue() * 360.0; + sat = SettingController.settings[ThemeSettings.baseSaturation]!.getValue() as double; // Advanced color - themeMode = ThemeSettings.themeModes[controller.settings[ThemeSettings.themeMode]!.getValue() as int]; - backgroundMode = controller.settings[ThemeSettings.backgroundMode]!.getValue() as int; + themeMode = ThemeSettings.themeModes[SettingController.settings[ThemeSettings.themeMode]!.getValue() as int]; + backgroundMode = SettingController.settings[ThemeSettings.backgroundMode]!.getValue() as int; } - return ColorFactory(primHue, secHue, sat, themeMode == -1 ? ThemeSettings.baseLuminosityDark : ThemeSettings.baseLuminosityLight, themeMode, - ThemeSettings.luminosityJumps, backgroundMode); + return ColorFactory( + primHue, + secHue, + sat, + themeMode == -1 ? ThemeSettings.baseLuminosityDark : ThemeSettings.baseLuminosityLight, + themeMode, + ThemeSettings.luminosityJumps, + backgroundMode, + ); } ColorFactory buildColorFactoryFromPreset(ThemePreset preset) { @@ -83,8 +95,15 @@ ColorFactory buildColorFactoryFromPreset(ThemePreset preset) { var themeMode = ThemeSettings.themeModes[preset.themeMode]; var backgroundMode = preset.backgroundMode; - return ColorFactory(primHue, secHue, sat, themeMode == -1 ? ThemeSettings.baseLuminosityDark : ThemeSettings.baseLuminosityLight, themeMode, - ThemeSettings.luminosityJumps, backgroundMode); + return ColorFactory( + primHue, + secHue, + sat, + themeMode == -1 ? ThemeSettings.baseLuminosityDark : ThemeSettings.baseLuminosityLight, + themeMode, + ThemeSettings.luminosityJumps, + backgroundMode, + ); } ThemeData getThemeData() { @@ -96,221 +115,223 @@ ThemeData getThemeDataFromFactory(ColorFactory factory) { if (factory.themeMode.isNegative) { //* Dark theme return defaultDarkTheme.copyWith( + brightness: Brightness.dark, + colorScheme: ColorScheme( + // Background color brightness: Brightness.dark, - colorScheme: ColorScheme( - // Background color - brightness: Brightness.dark, - inverseSurface: factory.getBackground1(), - onInverseSurface: factory.getBackground2(), - primaryContainer: factory.getBackground3(), - - // Online color - secondary: factory.customHue(0.3), - - // AFK color - secondaryContainer: factory.customHue(0.14), - - // Primary color - primary: factory.getPrimaryContainer(), - onPrimary: factory.getPrimary(), - - // Tertiary color - tertiary: factory.getSecondary(), - onTertiary: factory.getSecondaryContainer(), - tertiaryContainer: factory.getSecondaryContainer(), - - // Error color - error: factory.customHue(0.0), - onError: factory.customHueContainer(0.0), - errorContainer: factory.customHueContainer(0.0), + inverseSurface: factory.getBackground1(), + onInverseSurface: factory.getBackground2(), + primaryContainer: factory.getBackground3(), + + // Online color + secondary: factory.customHue(0.3), + + // AFK color + secondaryContainer: factory.customHue(0.14), + + // Primary color + primary: factory.getPrimaryContainer(), + onPrimary: factory.getPrimary(), + + // Tertiary color + tertiary: factory.getSecondary(), + onTertiary: factory.getSecondaryContainer(), + tertiaryContainer: factory.getSecondaryContainer(), + + // Error color + error: factory.customHue(0.0), + onError: factory.customHueContainer(0.0), + errorContainer: factory.customHueContainer(0.0), + + // Unused + onSecondary: const Color(0xFFbababa), + + // Unimportant font colors + surface: factory.getUnimportantFontColor(), + + // Important font color + onSurface: factory.getFontColor(), + ), + textSelectionTheme: const TextSelectionThemeData( + cursorColor: Color(0xFF99c1f1), + selectionColor: Color(0xFF5c5c5c), + selectionHandleColor: Color(0xFF99c1f1), + ), + dividerColor: const Color(0xFF5c5c5c), + textTheme: defaultDarkTheme.textTheme.copyWith( + //* Headlines + headlineMedium: defaultDarkTheme.textTheme.headlineMedium!.copyWith( + fontFamily: 'Roboto Mono', + fontWeight: FontWeight.bold, + ), - // Unused - onSecondary: const Color(0xFFbababa), + //* Normal body text + bodySmall: defaultDarkTheme.textTheme.bodySmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.normal, + color: factory.getUnimportantFontColor(), + ), + bodyMedium: defaultDarkTheme.textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + color: factory.getUnimportantFontColor(), + ), + bodyLarge: defaultDarkTheme.textTheme.bodyLarge!.copyWith( + fontSize: 18, + fontWeight: FontWeight.normal, + color: factory.getUnimportantFontColor(), + ), - // Unimportant font colors - surface: factory.getUnimportantFontColor(), + //* Labels + labelLarge: defaultDarkTheme.textTheme.labelLarge!.copyWith( + fontSize: 18, + fontWeight: FontWeight.normal, + color: factory.getFontColor(), + ), + labelMedium: defaultDarkTheme.textTheme.labelMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + color: factory.getFontColor(), + ), + labelSmall: defaultDarkTheme.textTheme.labelSmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.normal, + color: factory.getFontColor(), + ), - // Important font color - onSurface: factory.getFontColor(), + //* Titles + titleLarge: defaultDarkTheme.textTheme.titleLarge!.copyWith( + fontSize: 20, + fontWeight: FontWeight.w600, + color: factory.getFontColor(), + ), + titleMedium: defaultDarkTheme.textTheme.titleMedium!.copyWith( + fontSize: 18, + fontWeight: FontWeight.w500, + color: factory.getFontColor(), + ), + titleSmall: defaultDarkTheme.textTheme.titleSmall!.copyWith( + fontSize: 16, + fontWeight: FontWeight.w400, + color: factory.getFontColor(), ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: Color(0xFF99c1f1), - selectionColor: Color(0xFF5c5c5c), - selectionHandleColor: Color(0xFF99c1f1), + ), + tooltipTheme: TooltipThemeData( + decoration: BoxDecoration( + color: factory.completeBackground(), + borderRadius: BorderRadius.circular(defaultSpacing), ), - dividerColor: const Color(0xFF5c5c5c), - textTheme: defaultDarkTheme.textTheme.copyWith( - //* Headlines - headlineMedium: defaultDarkTheme.textTheme.headlineMedium!.copyWith( - fontFamily: 'Roboto Mono', - fontWeight: FontWeight.bold, - ), - - //* Normal body text - bodySmall: defaultDarkTheme.textTheme.bodySmall!.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: factory.getUnimportantFontColor(), - ), - bodyMedium: defaultDarkTheme.textTheme.bodyMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: factory.getUnimportantFontColor(), - ), - bodyLarge: defaultDarkTheme.textTheme.bodyLarge!.copyWith( - fontSize: 18, - fontWeight: FontWeight.normal, - color: factory.getUnimportantFontColor(), - ), - - //* Labels - labelLarge: defaultDarkTheme.textTheme.labelLarge!.copyWith( - fontSize: 18, - fontWeight: FontWeight.normal, - color: factory.getFontColor(), - ), - labelMedium: defaultDarkTheme.textTheme.labelMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: factory.getFontColor(), - ), - labelSmall: defaultDarkTheme.textTheme.labelSmall!.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: factory.getFontColor(), - ), - - //* Titles - titleLarge: defaultDarkTheme.textTheme.titleLarge!.copyWith( - fontSize: 20, - fontWeight: FontWeight.w600, - color: factory.getFontColor(), - ), - titleMedium: defaultDarkTheme.textTheme.titleMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w500, - color: factory.getFontColor(), - ), - titleSmall: defaultDarkTheme.textTheme.titleSmall!.copyWith( - fontSize: 16, - fontWeight: FontWeight.w400, - color: factory.getFontColor(), - ), + textStyle: defaultDarkTheme.textTheme.labelMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + color: factory.getFontColor(), ), - tooltipTheme: TooltipThemeData( - decoration: BoxDecoration( - color: factory.completeBackground(), - borderRadius: BorderRadius.circular(defaultSpacing), - ), - textStyle: defaultDarkTheme.textTheme.labelMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: factory.getFontColor(), - ), - )); + ), + ); } else { //* Light theme return defaultLightTheme.copyWith( + brightness: Brightness.light, + colorScheme: ColorScheme( + // Background color brightness: Brightness.light, - colorScheme: ColorScheme( - // Background color - brightness: Brightness.light, - inverseSurface: factory.getBackground1(), - onInverseSurface: factory.getBackground2(), - primaryContainer: factory.getBackground3(), - - // Online color - secondary: factory.customHue(0.3), - - // AFK color - secondaryContainer: factory.customHue(0.14), - - // Primary color - primary: factory.getPrimaryContainer(), - onPrimary: factory.getPrimary(), - - // Tertiary color - tertiary: factory.getSecondary(), - onTertiary: factory.getSecondaryContainer(), - tertiaryContainer: factory.getSecondaryContainer(), - - // Error color - error: factory.customHue(0.0), - onError: factory.customHueContainer(0.0), - errorContainer: factory.customHueContainer(0.0), + inverseSurface: factory.getBackground1(), + onInverseSurface: factory.getBackground2(), + primaryContainer: factory.getBackground3(), + + // Online color + secondary: factory.customHue(0.3), + + // AFK color + secondaryContainer: factory.customHue(0.14), + + // Primary color + primary: factory.getPrimaryContainer(), + onPrimary: factory.getPrimary(), + + // Tertiary color + tertiary: factory.getSecondary(), + onTertiary: factory.getSecondaryContainer(), + tertiaryContainer: factory.getSecondaryContainer(), + + // Error color + error: factory.customHue(0.0), + onError: factory.customHueContainer(0.0), + errorContainer: factory.customHueContainer(0.0), + + // Unused + onSecondary: factory.getFontColor(), + + // Unimportant font colors + surface: factory.getUnimportantFontColor(), + + // Important font color + onSurface: factory.getFontColor(), + ), + textSelectionTheme: const TextSelectionThemeData( + cursorColor: Color(0xFF99c1f1), + selectionColor: Color(0xFF5c5c5c), + selectionHandleColor: Color(0xFF99c1f1), + ), + dividerColor: const Color(0xFF5c5c5c), + textTheme: defaultLightTheme.textTheme.copyWith( + //* Headlines + headlineMedium: defaultLightTheme.textTheme.headlineMedium!.copyWith( + fontFamily: 'Roboto Mono', + fontWeight: FontWeight.bold, + ), - // Unused - onSecondary: factory.getFontColor(), + //* Normal body text + bodySmall: defaultLightTheme.textTheme.bodySmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.normal, + color: factory.getUnimportantFontColor(), + ), + bodyMedium: defaultLightTheme.textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + color: factory.getUnimportantFontColor(), + ), + bodyLarge: defaultLightTheme.textTheme.bodyLarge!.copyWith( + fontSize: 18, + fontWeight: FontWeight.normal, + color: factory.getUnimportantFontColor(), + ), - // Unimportant font colors - surface: factory.getUnimportantFontColor(), + //* Labels + labelLarge: defaultLightTheme.textTheme.labelLarge!.copyWith( + fontSize: 18, + fontWeight: FontWeight.normal, + color: factory.getFontColor(), + ), + labelMedium: defaultLightTheme.textTheme.labelMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + color: factory.getFontColor(), + ), + labelSmall: defaultLightTheme.textTheme.labelSmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.normal, + color: factory.getFontColor(), + ), - // Important font color - onSurface: factory.getFontColor(), + //* Titles + titleLarge: defaultLightTheme.textTheme.titleLarge!.copyWith( + fontSize: 20, + fontWeight: FontWeight.w600, + color: factory.getFontColor(), + ), + titleMedium: defaultLightTheme.textTheme.titleMedium!.copyWith( + fontSize: 18, + fontWeight: FontWeight.w500, + color: factory.getFontColor(), ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: Color(0xFF99c1f1), - selectionColor: Color(0xFF5c5c5c), - selectionHandleColor: Color(0xFF99c1f1), + titleSmall: defaultLightTheme.textTheme.titleSmall!.copyWith( + fontSize: 16, + fontWeight: FontWeight.w400, + color: factory.getFontColor(), ), - dividerColor: const Color(0xFF5c5c5c), - textTheme: defaultLightTheme.textTheme.copyWith( - //* Headlines - headlineMedium: defaultLightTheme.textTheme.headlineMedium!.copyWith( - fontFamily: 'Roboto Mono', - fontWeight: FontWeight.bold, - ), - - //* Normal body text - bodySmall: defaultLightTheme.textTheme.bodySmall!.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: factory.getUnimportantFontColor(), - ), - bodyMedium: defaultLightTheme.textTheme.bodyMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: factory.getUnimportantFontColor(), - ), - bodyLarge: defaultLightTheme.textTheme.bodyLarge!.copyWith( - fontSize: 18, - fontWeight: FontWeight.normal, - color: factory.getUnimportantFontColor(), - ), - - //* Labels - labelLarge: defaultLightTheme.textTheme.labelLarge!.copyWith( - fontSize: 18, - fontWeight: FontWeight.normal, - color: factory.getFontColor(), - ), - labelMedium: defaultLightTheme.textTheme.labelMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: factory.getFontColor(), - ), - labelSmall: defaultLightTheme.textTheme.labelSmall!.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: factory.getFontColor(), - ), - - //* Titles - titleLarge: defaultLightTheme.textTheme.titleLarge!.copyWith( - fontSize: 20, - fontWeight: FontWeight.w600, - color: factory.getFontColor(), - ), - titleMedium: defaultLightTheme.textTheme.titleMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w500, - color: factory.getFontColor(), - ), - titleSmall: defaultLightTheme.textTheme.titleSmall!.copyWith( - fontSize: 16, - fontWeight: FontWeight.w400, - color: factory.getFontColor(), - ), - )); + ), + ); } } diff --git a/lib/pages/settings/appearance/color_preview.dart b/lib/pages/settings/appearance/color_preview.dart index 69f365dd..f5bc2c3d 100644 --- a/lib/pages/settings/appearance/color_preview.dart +++ b/lib/pages/settings/appearance/color_preview.dart @@ -3,16 +3,13 @@ import 'package:chat_interface/pages/settings/appearance/theme_settings.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ColorPreview extends StatefulWidget { - final Rx factory; + final Signal factory; final bool mobile; - const ColorPreview({ - super.key, - required this.factory, - this.mobile = false, - }); + const ColorPreview({super.key, required this.factory, this.mobile = false}); @override State createState() => _ColorPreviewState(); @@ -21,7 +18,7 @@ class ColorPreview extends StatefulWidget { class _ColorPreviewState extends State { @override Widget build(BuildContext context) { - return Obx(() { + return Watch((ctx) { final colors = widget.factory.value!; return Padding( @@ -81,7 +78,7 @@ class _ColorPreviewState extends State { children: [ Icon(Icons.person, color: colors.getPrimary(), size: 40), horizontalSpacing(defaultSpacing), - Expanded(child: Text(Get.find().name.value, style: Get.theme.textTheme.labelLarge)), + Expanded(child: Text(StatusController.name.value, style: Get.theme.textTheme.labelLarge)), ], ), ), diff --git a/lib/pages/settings/appearance/theme_settings.dart b/lib/pages/settings/appearance/theme_settings.dart index fe59a22e..9d02f42d 100644 --- a/lib/pages/settings/appearance/theme_settings.dart +++ b/lib/pages/settings/appearance/theme_settings.dart @@ -14,6 +14,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; part 'color_generator.dart'; @@ -24,7 +25,15 @@ class ThemePreset extends SelectableItem { final int themeMode; final int backgroundMode; - const ThemePreset(super.label, super.icon, this.primaryHue, this.secondaryHue, this.baseSaturation, this.themeMode, this.backgroundMode); + const ThemePreset( + super.label, + super.icon, + this.primaryHue, + this.secondaryHue, + this.baseSaturation, + this.themeMode, + this.backgroundMode, + ); } class ThemeSettings { @@ -44,10 +53,13 @@ class ThemeSettings { static final themeModes = [ -1, // Dark - 1 // Light + 1, // Light ]; - static final backgroundModes = [SelectableItem("custom.none".tr, Icons.close), SelectableItem("custom.colored".tr, Icons.color_lens)]; + static final backgroundModes = [ + SelectableItem("custom.none".tr, Icons.close), + SelectableItem("custom.colored".tr, Icons.color_lens), + ]; static final themePresets = [ ThemePreset("theme.default_dark".tr, Icons.dark_mode, 0.54, 0.62, 0.6, 0, 0), @@ -57,13 +69,13 @@ class ThemeSettings { ]; static const int customThemeIndex = 3; - static void addThemeSettings(SettingController controller) { - controller.addSetting(Setting(themePreset, 0)); - controller.addSetting(Setting(primaryHue, 0.54)); - controller.addSetting(Setting(secondaryHue, 0.62)); - controller.addSetting(Setting(baseSaturation, 0.6)); - controller.addSetting(Setting(backgroundMode, 0)); - controller.addSetting(Setting(themeMode, 0)); + static void addSettings() { + SettingController.addSetting((Setting(themePreset, 0))); + SettingController.addSetting(Setting(primaryHue, 0.54)); + SettingController.addSetting(Setting(secondaryHue, 0.62)); + SettingController.addSetting(Setting(baseSaturation, 0.6)); + SettingController.addSetting(Setting(backgroundMode, 0)); + SettingController.addSetting(Setting(themeMode, 0)); } } @@ -75,11 +87,12 @@ class ThemeSettingsPage extends StatefulWidget { } class _ThemeSettingsPageState extends State { - final _factory = Rx(null); + final _factory = Signal(null); Timer? _timer; @override void dispose() { + _factory.dispose(); _timer!.cancel(); super.dispose(); } @@ -92,13 +105,7 @@ class _ThemeSettingsPageState extends State { }); if (isMobileMode()) { - return SettingsPageBase( - label: "colors", - child: ColorPreview( - factory: _factory, - mobile: true, - ), - ); + return SettingsPageBase(label: "colors", child: ColorPreview(factory: _factory, mobile: true)); } return SettingsPageBase( @@ -106,14 +113,10 @@ class _ThemeSettingsPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Expanded( - child: ThemeSettingsElement(), - ), + const Expanded(child: ThemeSettingsElement()), //* Color preview - Expanded( - child: ColorPreview(factory: _factory), - ), + Expanded(child: ColorPreview(factory: _factory)), ], ), ); @@ -136,11 +139,15 @@ class _ThemeSettingsElementState extends State { children: [ Text("theme.presets".tr, style: Get.theme.textTheme.labelLarge), verticalSpacing(elementSpacing), - ListSelectionSetting(settingName: ThemeSettings.themePreset, items: ThemeSettings.themePresets), + ListSelectionSetting( + setting: SettingController.settings[ThemeSettings.themePreset]! as Setting, + items: ThemeSettings.themePresets, + ), verticalSpacing(sectionSpacing), - Obx( - () => Visibility( - visible: Get.find().settings[ThemeSettings.themePreset]!.getValue() == ThemeSettings.customThemeIndex, + Watch( + (ctx) => Visibility( + visible: + SettingController.settings[ThemeSettings.themePreset]!.getValue() == ThemeSettings.customThemeIndex, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -149,33 +156,50 @@ class _ThemeSettingsElementState extends State { verticalSpacing(defaultSpacing), // Sliders - const DoubleSelectionSetting(settingName: ThemeSettings.primaryHue, description: "custom.primary_hue", min: 0.0, max: 1.0), + const DoubleSelectionSetting( + settingName: ThemeSettings.primaryHue, + description: "custom.primary_hue", + min: 0.0, + max: 1.0, + ), verticalSpacing(defaultSpacing), - const DoubleSelectionSetting(settingName: ThemeSettings.secondaryHue, description: "custom.secondary_hue", min: 0.0, max: 1.0), + const DoubleSelectionSetting( + settingName: ThemeSettings.secondaryHue, + description: "custom.secondary_hue", + min: 0.0, + max: 1.0, + ), verticalSpacing(defaultSpacing), - const DoubleSelectionSetting(settingName: ThemeSettings.baseSaturation, description: "custom.base_saturation", min: 0.0, max: 1.0), + const DoubleSelectionSetting( + settingName: ThemeSettings.baseSaturation, + description: "custom.base_saturation", + min: 0.0, + max: 1.0, + ), verticalSpacing(defaultSpacing), // Selections - Text( - "custom.theme_mode".tr, - ), + Text("custom.theme_mode".tr), verticalSpacing(elementSpacing), ListSelectionSetting( - settingName: ThemeSettings.themeMode, - items: [SelectableItem("custom.dark".tr, Icons.dark_mode), SelectableItem("custom.light".tr, Icons.light_mode)], + setting: SettingController.settings[ThemeSettings.themeMode]! as Setting, + items: [ + SelectableItem("custom.dark".tr, Icons.dark_mode), + SelectableItem("custom.light".tr, Icons.light_mode), + ], ), verticalSpacing(defaultSpacing), - Text( - "custom.background_mode".tr, - ), + Text("custom.background_mode".tr), verticalSpacing(elementSpacing), - ListSelectionSetting(settingName: ThemeSettings.backgroundMode, items: ThemeSettings.backgroundModes), + ListSelectionSetting( + setting: SettingController.settings[ThemeSettings.backgroundMode]! as Setting, + items: ThemeSettings.backgroundModes, + ), - verticalSpacing(sectionSpacing) + verticalSpacing(sectionSpacing), ], ), ), @@ -183,10 +207,10 @@ class _ThemeSettingsElementState extends State { FJElevatedButton( onTap: () { final ThemeData theme = getThemeData(); - Get.find().changeTheme(theme); + ThemeManager.changeTheme(theme); }, child: Text("theme.apply".tr, style: Get.theme.textTheme.labelLarge), - ) + ), ], ); } diff --git a/lib/pages/settings/components/bool_selection_small.dart b/lib/pages/settings/components/bool_selection_small.dart index f6fe7dce..e04421a9 100644 --- a/lib/pages/settings/components/bool_selection_small.dart +++ b/lib/pages/settings/components/bool_selection_small.dart @@ -2,6 +2,7 @@ import 'package:chat_interface/pages/settings/data/settings_controller.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class BoolSettingSmall extends StatelessWidget { final String settingName; @@ -11,9 +12,8 @@ class BoolSettingSmall extends StatelessWidget { @override Widget build(BuildContext context) { - final controller = Get.find(); - return Obx( - () => Row( + return Watch( + (ctx) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -22,18 +22,24 @@ class BoolSettingSmall extends StatelessWidget { Switch( activeColor: Get.theme.colorScheme.secondary, trackColor: WidgetStateColor.resolveWith( - (states) => states.contains(WidgetState.selected) ? Get.theme.colorScheme.primary : Get.theme.colorScheme.onInverseSurface, + (states) => + states.contains(WidgetState.selected) + ? Get.theme.colorScheme.primary + : Get.theme.colorScheme.onInverseSurface, ), hoverColor: Get.theme.hoverColor, thumbColor: WidgetStateColor.resolveWith( - (states) => states.contains(WidgetState.selected) ? Get.theme.colorScheme.onPrimary : Get.theme.colorScheme.surface, + (states) => + states.contains(WidgetState.selected) + ? Get.theme.colorScheme.onPrimary + : Get.theme.colorScheme.surface, ), - value: controller.settings[settingName]!.getValue(), + value: SettingController.settings[settingName]!.getValue(), onChanged: (value) { - controller.settings[settingName]!.setValue(value); + SettingController.settings[settingName]!.setValue(value); onChanged?.call(value); }, - ) + ), ], ), ); diff --git a/lib/pages/settings/components/double_selection.dart b/lib/pages/settings/components/double_selection.dart index 57b581a0..3a64e7a1 100644 --- a/lib/pages/settings/components/double_selection.dart +++ b/lib/pages/settings/components/double_selection.dart @@ -4,6 +4,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class DoubleSelectionSetting extends StatefulWidget { final String settingName; @@ -33,26 +34,31 @@ class DoubleSelectionSetting extends StatefulWidget { class _ListSelectionSettingState extends State { // Current value - final current = 0.0.obs; - DateTime? lastSet; + final _current = signal(0.0); + + @override + void dispose() { + _current.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - SettingController controller = Get.find(); - final setting = controller.settings[widget.settingName]!; - current.value = setting.getValue() as double; + final setting = SettingController.settings[widget.settingName]!; + _current.value = setting.getValue() as double; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Visibility( - visible: widget.description.isNotEmpty, - child: Padding( - padding: const EdgeInsets.only(bottom: elementSpacing), - child: Text(widget.description.tr, style: Get.theme.textTheme.bodyMedium), - )), - Obx(() { - final value = current.value; + visible: widget.description.isNotEmpty, + child: Padding( + padding: const EdgeInsets.only(bottom: elementSpacing), + child: Text(widget.description.tr, style: Get.theme.textTheme.bodyMedium), + ), + ), + Watch((ctx) { + final value = _current.value; final roundedCurrent = widget.rounded ? value.toStringAsFixed(0) : value.toStringAsFixed(1); return Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -64,11 +70,11 @@ class _ListSelectionSettingState extends State { max: widget.max, onChanged: (value) { if (widget.rounded) { - current.value = value.roundToDouble(); + _current.value = value.roundToDouble(); } else { - current.value = value; + _current.value = value; } - widget.onChange?.call(current.value); + widget.onChange?.call(_current.value); }, onChangeEnd: (value) { if (widget.rounded) { diff --git a/lib/pages/settings/components/list_selection.dart b/lib/pages/settings/components/list_selection.dart index b4f5e9a3..93a34377 100644 --- a/lib/pages/settings/components/list_selection.dart +++ b/lib/pages/settings/components/list_selection.dart @@ -1,7 +1,8 @@ -import 'package:chat_interface/pages/settings/data/settings_controller.dart'; +import 'package:chat_interface/pages/settings/data/entities.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SelectableItem { final String label; @@ -12,11 +13,11 @@ class SelectableItem { } class ListSelectionSetting extends StatefulWidget { - final String settingName; + final Setting setting; final List items; final Function(SelectableItem)? callback; - const ListSelectionSetting({super.key, required this.settingName, required this.items, this.callback}); + const ListSelectionSetting({super.key, required this.setting, required this.items, this.callback}); @override State createState() => _ListSelectionSettingState(); @@ -25,8 +26,38 @@ class ListSelectionSetting extends StatefulWidget { class _ListSelectionSettingState extends State { @override Widget build(BuildContext context) { - SettingController controller = Get.find(); + return ListSelection( + selected: computed(() => widget.setting.getValue()), + items: widget.items, + callback: (item, index) { + widget.callback?.call(item); + widget.setting.setValue(index); + }, + ); + } +} +class ListSelection extends StatefulWidget { + final ReadonlySignal selected; + final List items; + final Function(SelectableItem, int) callback; + final bool secondary; + + const ListSelection({ + super.key, + required this.selected, + required this.items, + required this.callback, + this.secondary = false, + }); + + @override + State createState() => _ListSelectionState(); +} + +class _ListSelectionState extends State { + @override + Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: List.generate(widget.items.length, (index) { @@ -40,19 +71,19 @@ class _ListSelectionSettingState extends State { return Padding( padding: const EdgeInsets.only(bottom: defaultSpacing * 0.5), - child: Obx( - () => Material( - color: controller.settings[widget.settingName]!.getWhenValue(0, 0) == index - ? Get.theme.colorScheme.primary - : Get.theme.colorScheme.onInverseSurface, + child: Watch( + (ctx) => Material( + color: + widget.selected.value == index + ? Get.theme.colorScheme.primary + : (widget.secondary + ? Get.theme.colorScheme.inverseSurface + : Get.theme.colorScheme.onInverseSurface), borderRadius: radius, child: InkWell( borderRadius: radius, onTap: () { - controller.settings[widget.settingName]!.setValue(index); - if (widget.callback != null) { - widget.callback!(widget.items[index]); - } + widget.callback(widget.items[index], index); }, child: Padding( padding: const EdgeInsets.all(defaultSpacing), @@ -68,37 +99,41 @@ class _ListSelectionSettingState extends State { ), horizontalSpacing(defaultSpacing), if (widget.items[index].experimental) - LayoutBuilder(builder: (context, constraints) { - if (isMobileMode()) { - return Tooltip( - message: "settings.experimental".tr, - child: Icon(Icons.science, color: Get.theme.colorScheme.error), - ); - } + LayoutBuilder( + builder: (context, constraints) { + if (isMobileMode()) { + return Tooltip( + message: "settings.experimental".tr, + child: Icon(Icons.science, color: Get.theme.colorScheme.error), + ); + } - return Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.error.withOpacity(0.1), - borderRadius: BorderRadius.circular(defaultSpacing), - ), - padding: const EdgeInsets.all(elementSpacing), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.science, color: Get.theme.colorScheme.error), - horizontalSpacing(elementSpacing), - Flexible( - child: Text( - "settings.experimental".tr, - style: Get.theme.textTheme.bodyMedium!.copyWith(color: Get.theme.colorScheme.error), - overflow: TextOverflow.clip, + return Container( + decoration: BoxDecoration( + color: Get.theme.colorScheme.error.withAlpha(25), + borderRadius: BorderRadius.circular(defaultSpacing), + ), + padding: const EdgeInsets.all(elementSpacing), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.science, color: Get.theme.colorScheme.error), + horizontalSpacing(elementSpacing), + Flexible( + child: Text( + "settings.experimental".tr, + style: Get.theme.textTheme.bodyMedium!.copyWith( + color: Get.theme.colorScheme.error, + ), + overflow: TextOverflow.clip, + ), ), - ), - horizontalSpacing(elementSpacing) - ], - ), - ); - }) + horizontalSpacing(elementSpacing), + ], + ), + ); + }, + ) else const SizedBox.shrink(), ], diff --git a/lib/pages/settings/data/entities.dart b/lib/pages/settings/data/entities.dart index 722f264c..d550730e 100644 --- a/lib/pages/settings/data/entities.dart +++ b/lib/pages/settings/data/entities.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:chat_interface/database/database.dart'; import 'package:chat_interface/pages/settings/account/data_settings.dart'; import 'package:chat_interface/pages/settings/account/invites_page.dart'; +import 'package:chat_interface/pages/settings/app/audio_settings.dart'; import 'package:chat_interface/pages/settings/app/general_settings.dart'; import 'package:chat_interface/pages/settings/town/admin_accounts_page.dart'; import 'package:chat_interface/pages/settings/town/file_settings.dart'; @@ -14,6 +15,7 @@ import 'package:chat_interface/pages/settings/security/trusted_links_settings.da import 'package:chat_interface/pages/settings/town/town_settings.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; enum SettingLabel { // Account settings (everything to do with the account and stored on the server) @@ -36,6 +38,7 @@ enum SettingLabel { // Everything to do with the app (that's stored locally) app("settings.tab.app", [ SettingCategory("general", Icons.dashboard, GeneralSettingsPage()), + SettingCategory("audio", Icons.volume_up, AudioSettingsPage()), //SettingCategory("notifications", Icons.notifications, null), SettingCategory("logging", Icons.insights, LogSettingsPage()), ]), @@ -47,9 +50,7 @@ enum SettingLabel { //SettingCategory("call_app", Icons.cable, CallSettingsPage()), ]), - privacy("settings.tab.security", [ - SettingCategory("trusted_links", Icons.link, TrustedLinkSettingsPage()), - ]); + privacy("settings.tab.security", [SettingCategory("trusted_links", Icons.link, TrustedLinkSettingsPage())]); final String _label; final List categories; @@ -69,12 +70,20 @@ class SettingCategory { final bool displayTitle; final bool web; - const SettingCategory(this.label, this.icon, this.widget, {this.displayTitle = true, this.mobile = true, this.admin = false, this.web = true}); + const SettingCategory( + this.label, + this.icon, + this.widget, { + this.displayTitle = true, + this.mobile = true, + this.admin = false, + this.web = true, + }); } class Setting { final String label; - final Rx value = Rx(null); + final Signal value = signal(null); T defaultValue; Setting(this.label, this.defaultValue); diff --git a/lib/pages/settings/data/settings_controller.dart b/lib/pages/settings/data/settings_controller.dart index 0508cae4..1c49def4 100644 --- a/lib/pages/settings/data/settings_controller.dart +++ b/lib/pages/settings/data/settings_controller.dart @@ -1,10 +1,10 @@ +import 'package:chat_interface/controller/spaces/studio/studio_device_manager.dart'; import 'package:chat_interface/pages/chat/chat_page_mobile.dart'; -import 'package:chat_interface/pages/settings/account/data_settings.dart'; +import 'package:chat_interface/pages/settings/app/audio_settings.dart'; import 'package:chat_interface/pages/settings/app/general_settings.dart'; import 'package:chat_interface/pages/settings/town/file_settings.dart'; import 'package:chat_interface/pages/settings/app/log_settings.dart'; import 'package:chat_interface/pages/settings/town/tabletop_settings.dart'; -import 'package:chat_interface/pages/settings/appearance/call_settings.dart'; import 'package:chat_interface/pages/settings/appearance/chat_settings.dart'; import 'package:chat_interface/pages/settings/appearance/theme_settings.dart'; import 'package:chat_interface/pages/settings/data/entities.dart'; @@ -12,14 +12,15 @@ import 'package:chat_interface/pages/settings/security/trusted_links_settings.da import 'package:chat_interface/pages/settings/settings_page_desktop.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class AppSettings { static String showGroupMembers = "chat.group_members"; } -class SettingController extends GetxController { - final currentCategory = Rx(null); // For persisting the page in the settings - final settings = {}; // label: Setting +class SettingController { + static final currentCategory = signal(null); // For persisting the page in the settings + static final settings = {}; // label: Setting static void openSettingsPage() { if (isMobileMode()) { @@ -29,22 +30,22 @@ class SettingController extends GetxController { } } - SettingController() { - addCallAppearanceSettings(this); - GeneralSettings.addSettings(this); - TabletopSettings.addSettings(this); - ThemeSettings.addThemeSettings(this); - FileSettings.addSettings(this); - TrustedLinkSettings.registerSettings(this); - ChatSettings.registerSettings(this); - DataSettings.registerSettings(this); - LogSettings.registerSettings(this); + static void init() { + GeneralSettings.addSettings(); + TabletopSettings.addSettings(); + ThemeSettings.addSettings(); + FileSettings.addSettings(); + TrustedLinkSettings.addSettings(); + ChatSettings.addSettings(); + LogSettings.addSettings(); + AudioSettings.addSettings(); + StudioDeviceManager.init(); // Add app settings (not in settings page) addSetting(Setting(AppSettings.showGroupMembers, true)); } - void addSetting(Setting setting) { + static void addSetting(Setting setting) { settings[setting.label] = setting; } } diff --git a/lib/pages/settings/security/trusted_links_settings.dart b/lib/pages/settings/security/trusted_links_settings.dart index 41613488..3dad6b6b 100644 --- a/lib/pages/settings/security/trusted_links_settings.dart +++ b/lib/pages/settings/security/trusted_links_settings.dart @@ -12,6 +12,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:drift/drift.dart' as drift; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class TrustedLinkSettings { static const String unsafeSources = "links.unsafe_sources"; @@ -23,9 +24,9 @@ class TrustedLinkSettings { SelectableItem("links.trust_mode.none", Icons.close), ]; - static void registerSettings(SettingController controller) { - controller.addSetting(Setting(unsafeSources, false)); - controller.addSetting(Setting(trustMode, 1)); + static void addSettings() { + SettingController.addSetting(Setting(unsafeSources, false)); + SettingController.addSetting(Setting(trustMode, 1)); } } @@ -37,7 +38,7 @@ class TrustedLinkSettingsPage extends StatefulWidget { } class _TrustedLinkSettingsPageState extends State { - final _trusted = [].obs; + final _trusted = listSignal([]); @override void initState() { @@ -45,6 +46,12 @@ class _TrustedLinkSettingsPageState extends State { loadTrusted(); } + @override + void dispose() { + _trusted.dispose(); + super.dispose(); + } + Future loadTrusted() async { _trusted.value = await db.trustedLink.select().get(); } @@ -56,10 +63,7 @@ class _TrustedLinkSettingsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - InfoContainer( - message: "links.warning".tr, - expand: true, - ), + InfoContainer(message: "links.warning".tr, expand: true), verticalSpacing(sectionSpacing), //* Location trust types @@ -67,9 +71,7 @@ class _TrustedLinkSettingsPageState extends State { verticalSpacing(defaultSpacing), // Unsafe locations - const BoolSettingSmall( - settingName: TrustedLinkSettings.unsafeSources, - ), + const BoolSettingSmall(settingName: TrustedLinkSettings.unsafeSources), verticalSpacing(sectionSpacing), Text("links.trusted_domains".tr, style: Get.theme.textTheme.labelLarge), @@ -78,8 +80,8 @@ class _TrustedLinkSettingsPageState extends State { // Trust mode Text("links.trust_mode".tr, style: Get.theme.textTheme.bodyMedium), verticalSpacing(defaultSpacing), - const ListSelectionSetting( - settingName: TrustedLinkSettings.trustMode, + ListSelectionSetting( + setting: SettingController.settings[TrustedLinkSettings.trustMode]! as Setting, items: TrustedLinkSettings.trustModes, ), verticalSpacing(sectionSpacing), @@ -96,53 +98,45 @@ class _TrustedLinkSettingsPageState extends State { child: Text("links.trusted_list.add".tr, style: Get.theme.textTheme.labelLarge), ), verticalSpacing(defaultSpacing), - Obx(() { + Watch((ctx) { if (_trusted.isEmpty) { return Text("links.trusted_list.empty".tr, style: Get.theme.textTheme.labelMedium); } return Column( - children: List.generate( - _trusted.length, - (index) { - return Padding( - padding: const EdgeInsets.only(bottom: elementSpacing), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: defaultSpacing, vertical: elementSpacing), - decoration: BoxDecoration( - color: Get.theme.colorScheme.onInverseSurface, - borderRadius: BorderRadius.circular(defaultSpacing), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Row( - children: [ - Icon(Icons.done_all, color: Get.theme.colorScheme.onPrimary), - horizontalSpacing(defaultSpacing), - Flexible( - child: Text( - _trusted[index].domain, - style: Get.theme.textTheme.labelMedium, - ), - ), - ], - ), - ), - IconButton( - onPressed: () { - db.trustedLink.deleteWhere((tbl) => tbl.domain.equals(_trusted[index].domain)); - _trusted.removeAt(index); - }, - icon: const Icon(Icons.delete), + children: List.generate(_trusted.length, (index) { + return Padding( + padding: const EdgeInsets.only(bottom: elementSpacing), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing, vertical: elementSpacing), + decoration: BoxDecoration( + color: Get.theme.colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(defaultSpacing), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + Icon(Icons.done_all, color: Get.theme.colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Flexible(child: Text(_trusted[index].domain, style: Get.theme.textTheme.labelMedium)), + ], ), - ], - ), + ), + IconButton( + onPressed: () { + db.trustedLink.deleteWhere((tbl) => tbl.domain.equals(_trusted[index].domain)); + _trusted.removeAt(index); + }, + icon: const Icon(Icons.delete), + ), + ], ), - ); - }, - ), + ), + ); + }), ); }), ], @@ -170,23 +164,16 @@ class _TrustedLinkCreationWindowState extends State { @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text("links.trusted_list.add".tr, style: Get.theme.textTheme.titleMedium), - ], + title: [Text("links.trusted_list.add".tr, style: Get.theme.textTheme.titleMedium)], child: Column( mainAxisSize: MainAxisSize.min, children: [ - FJTextField( - controller: _controller, - hintText: "links.trusted_list.placeholder".tr, - ), + FJTextField(controller: _controller, hintText: "links.trusted_list.placeholder".tr), verticalSpacing(defaultSpacing), FJElevatedButton( onTap: () => Get.back(result: _controller.text), - child: Center( - child: Text("add".tr, style: Get.theme.textTheme.labelLarge), - ), - ) + child: Center(child: Text("add".tr, style: Get.theme.textTheme.labelLarge)), + ), ], ), ); diff --git a/lib/pages/settings/setting_selection_mobile.dart b/lib/pages/settings/setting_selection_mobile.dart index 31fbc6ef..54aef1b3 100644 --- a/lib/pages/settings/setting_selection_mobile.dart +++ b/lib/pages/settings/setting_selection_mobile.dart @@ -4,9 +4,10 @@ import 'package:chat_interface/pages/settings/data/entities.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SettingSelectionMobile extends StatelessWidget { - final Rx? category; + final Signal? category; final bool desktop; const SettingSelectionMobile({super.key, this.desktop = false, this.category}); @@ -31,78 +32,73 @@ class SettingSelectionMobile extends StatelessWidget { children: [ Padding( padding: EdgeInsets.symmetric(horizontal: desktop ? 0 : elementSpacing), - child: Text( - current.label.tr, - style: Theme.of(context).textTheme.headlineMedium, - ), + child: Text(current.label.tr, style: Theme.of(context).textTheme.headlineMedium), ), verticalSpacing(defaultSpacing * 0.5), Column( mainAxisAlignment: MainAxisAlignment.start, - children: current.categories.map((element) { - if (!element.mobile && GetPlatform.isMobile) { - return const SizedBox(); - } - if (!element.web && isWeb) { - return const SizedBox(); - } - if (!StatusController.permissions.contains("admin") && element.admin) { - return const SizedBox(); - } + children: + current.categories.map((element) { + if (!element.mobile && GetPlatform.isMobile) { + return const SizedBox(); + } + if (!element.web && isWeb) { + return const SizedBox(); + } + if (!StatusController.permissions.contains("admin") && element.admin) { + return const SizedBox(); + } - return Padding( - padding: const EdgeInsets.only(top: defaultSpacing), - child: Material( - color: Get.theme.colorScheme.inverseSurface, - borderRadius: BorderRadius.circular(sectionSpacing), - child: InkWell( - onTap: () { - if (category == null) { - Get.to(element.widget); - } else { - category!.value = element; - } - }, - borderRadius: BorderRadius.circular(sectionSpacing), - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Row( - children: [ - Icon( - element.icon, - color: Theme.of(context).colorScheme.onPrimary, - size: Get.theme.textTheme.titleLarge!.fontSize! * 2, - ), - horizontalSpacing(defaultSpacing), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "settings.${element.label}".tr, - style: Theme.of(context).textTheme.labelLarge!, - overflow: TextOverflow.ellipsis, - ), - Text( - "settings.${element.label}.desc".tr, - style: Theme.of(context).textTheme.bodyMedium!, + return Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: Material( + color: Get.theme.colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(sectionSpacing), + child: InkWell( + onTap: () { + if (category == null) { + Get.to(element.widget); + } else { + category!.value = element; + } + }, + borderRadius: BorderRadius.circular(sectionSpacing), + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + Icon( + element.icon, + color: Theme.of(context).colorScheme.onPrimary, + size: Get.theme.textTheme.titleLarge!.fontSize! * 2, + ), + horizontalSpacing(defaultSpacing), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "settings.${element.label}".tr, + style: Theme.of(context).textTheme.labelLarge!, + overflow: TextOverflow.ellipsis, + ), + Text( + "settings.${element.label}.desc".tr, + style: Theme.of(context).textTheme.bodyMedium!, + ), + ], ), - ], - ), + ), + horizontalSpacing(defaultSpacing), + Icon(Icons.arrow_forward, size: Get.theme.textTheme.titleLarge!.fontSize! * 1.5), + ], ), - horizontalSpacing(defaultSpacing), - Icon( - Icons.arrow_forward, - size: Get.theme.textTheme.titleLarge!.fontSize! * 1.5, - ), - ], + ), ), ), - ), - ), - ); - }).toList(), - ) + ); + }).toList(), + ), ], ), ); diff --git a/lib/pages/settings/settings_page_base.dart b/lib/pages/settings/settings_page_base.dart index 6f5aa80b..1d311970 100644 --- a/lib/pages/settings/settings_page_base.dart +++ b/lib/pages/settings/settings_page_base.dart @@ -12,11 +12,7 @@ class SettingsPageBase extends StatelessWidget { final String label; final Widget child; - const SettingsPageBase({ - super.key, - required this.child, - required this.label, - }); + const SettingsPageBase({super.key, required this.child, required this.label}); @override Widget build(BuildContext context) { @@ -35,7 +31,7 @@ class SettingsPageBase extends StatelessWidget { for (var settingLabel in SettingLabel.values) { final category = settingLabel.categories.firstWhereOrNull((e) => e.label == label); if (category != null) { - Get.find().currentCategory.value = category; + SettingController.currentCategory.value = category; break; } } @@ -44,10 +40,7 @@ class SettingsPageBase extends StatelessWidget { backgroundColor: Theme.of(context).colorScheme.inverseSurface, body: Column( children: [ - UniversalAppBar( - label: "settings.$label".tr, - applyPadding: true, - ), + UniversalAppBar(label: "settings.$label".tr, applyPadding: true), Expanded( child: Padding( padding: const EdgeInsets.only(left: sectionSpacing), diff --git a/lib/pages/settings/settings_page_desktop.dart b/lib/pages/settings/settings_page_desktop.dart index 78c3a86d..e2f22c3c 100644 --- a/lib/pages/settings/settings_page_desktop.dart +++ b/lib/pages/settings/settings_page_desktop.dart @@ -4,10 +4,12 @@ import 'package:chat_interface/pages/chat/chat_page_mobile.dart'; import 'package:chat_interface/pages/settings/data/settings_controller.dart'; import 'package:chat_interface/pages/settings/settings_sidebar.dart'; import 'package:chat_interface/pages/settings/setting_selection_mobile.dart'; +import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/platform_callback.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SettingsPageDesktop extends StatefulWidget { const SettingsPageDesktop({super.key}); @@ -26,7 +28,7 @@ class _SettingsHomepageState extends State { bottom: false, child: PlatformCallback( mobile: () { - final current = Get.find().currentCategory.value; + final current = SettingController.currentCategory.value; if (current != null) { Get.off(const ChatPageMobile(selected: 3)); Get.to(current.widget); @@ -34,53 +36,48 @@ class _SettingsHomepageState extends State { Get.off(const ChatPageMobile(selected: 3)); } }, - child: LayoutBuilder(builder: (context, constraints) { - const sidebarWidth = 300.0; - final biggestWidth = constraints.biggest.width; - var containerWidth = 0.0; - var pageWidth = 1000.0; - if (biggestWidth > 1000 + sidebarWidth + 24) { - containerWidth = (biggestWidth - 1000 - sidebarWidth) / 2; - } else { - pageWidth = biggestWidth - sidebarWidth - defaultSpacing * 1.5; - } + child: LayoutBuilder( + builder: (context, constraints) { + const sidebarWidth = 300.0; + final biggestWidth = constraints.biggest.width; + var containerWidth = 0.0; + var pageWidth = 1000.0; + if (biggestWidth > 1000 + sidebarWidth + 24) { + containerWidth = (biggestWidth - 1000 - sidebarWidth) / 2; + } else { + pageWidth = biggestWidth - sidebarWidth - defaultSpacing * 1.5; + } - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - width: containerWidth, - ), - Obx( - () { - final category = Get.find().currentCategory; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(width: containerWidth), + Watch((ctx) { return SettingsSidebar( sidebarWidth: sidebarWidth, - currentCategory: category.value?.label, - category: category, + currentCategory: SettingController.currentCategory.value?.label, + category: SettingController.currentCategory, ); - }, - ), + }), - //* Content - Flexible( - child: Padding( - padding: const EdgeInsets.only(left: defaultSpacing), - child: SingleChildScrollView( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ConstrainedBox( - constraints: BoxConstraints(maxWidth: pageWidth), - child: Padding( - padding: const EdgeInsets.only(bottom: defaultSpacing, right: defaultSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx( - () { - final category = Get.find().currentCategory; + //* Content + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: defaultSpacing), + child: SingleChildScrollView( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: pageWidth), + child: Padding( + padding: const EdgeInsets.only(bottom: defaultSpacing, right: defaultSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Watch((ctx) { + final category = SettingController.currentCategory; if (category.value != null && category.value!.displayTitle) { return Padding( padding: const EdgeInsets.only(top: defaultSpacing, bottom: sectionSpacing), @@ -92,36 +89,29 @@ class _SettingsHomepageState extends State { } return const SizedBox(); - }, - ), - Obx( - () { - final category = Get.find().currentCategory; + }), + Watch((ctx) { + final category = SettingController.currentCategory; if (category.value == null) { - return SettingSelectionMobile( - category: category, - desktop: true, - ); + return SettingSelectionMobile(category: category, desktop: true); } return category.value!.widget ?? const Placeholder(); - }, - ), - ], + }), + ], + ), ), ), - ), - SizedBox( - width: max(containerWidth, 8) - 8, - ), - ], + SizedBox(width: max(containerWidth, 8) - 8), + ], + ), ), ), ), - ), - ], - ); - }), + ], + ); + }, + ), ), ), ); diff --git a/lib/pages/settings/settings_sidebar.dart b/lib/pages/settings/settings_sidebar.dart index 0268d889..ef680b22 100644 --- a/lib/pages/settings/settings_sidebar.dart +++ b/lib/pages/settings/settings_sidebar.dart @@ -6,9 +6,10 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SettingsSidebar extends StatefulWidget { - final Rx? category; + final Signal? category; final String? currentCategory; final double sidebarWidth; @@ -44,13 +45,10 @@ class _SettingsSidebarState extends State { borderRadius: BorderRadius.circular(sectionSpacing), ), child: Column( + mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.only( - top: sectionSpacing, - right: sectionSpacing, - left: sectionSpacing, - ), + padding: const EdgeInsets.only(top: sectionSpacing, right: sectionSpacing, left: sectionSpacing), child: FJElevatedButton( onTap: () => Get.back(), child: Padding( @@ -65,91 +63,89 @@ class _SettingsSidebarState extends State { ), ), ), - Expanded( - child: FadingEdgeScrollView.fromScrollView( - child: ListView.builder( - controller: _controller, - itemCount: SettingLabel.values.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final current = SettingLabel.values[index]; + Flexible( + child: ListView.builder( + controller: _controller, + itemCount: SettingLabel.values.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final current = SettingLabel.values[index]; - //* Sidebar buttons - return Padding( - padding: EdgeInsets.only( - right: sectionSpacing, - left: sectionSpacing, - bottom: index == SettingLabel.values.length - 1 ? sectionSpacing : 0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - verticalSpacing(sectionSpacing), - Text(current.label.tr, style: Theme.of(context).textTheme.titleLarge), - verticalSpacing(defaultSpacing * 0.5), - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: current.categories.map((element) { - if (!element.mobile && GetPlatform.isMobile) { - return const SizedBox(); - } - if (!element.web && isWeb) { - return const SizedBox(); - } - if (!StatusController.permissions.contains("admin") && element.admin) { - return const SizedBox(); - } + //* Sidebar buttons + return Padding( + padding: EdgeInsets.only( + right: sectionSpacing, + left: sectionSpacing, + bottom: index == SettingLabel.values.length - 1 ? sectionSpacing : 0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + verticalSpacing(sectionSpacing), + Text(current.label.tr, style: Theme.of(context).textTheme.titleLarge), + verticalSpacing(defaultSpacing * 0.5), + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: + current.categories.map((element) { + if (!element.mobile && GetPlatform.isMobile) { + return const SizedBox(); + } + if (!element.web && isWeb) { + return const SizedBox(); + } + if (!StatusController.permissions.contains("admin") && element.admin) { + return const SizedBox(); + } - return Padding( - padding: const EdgeInsets.only(top: defaultSpacing), - child: Material( - color: widget.currentCategory == element.label - ? Get.theme.colorScheme.primary - : Get.theme.colorScheme.inverseSurface, - borderRadius: BorderRadius.circular(defaultSpacing), - child: InkWell( - onTap: () { - if (widget.category != null) { - widget.category!.value = element; - } else { - Get.to( - element.widget, - transition: Transition.fadeIn, - ); - } - }, + return Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: Material( + color: + widget.currentCategory == element.label + ? Get.theme.colorScheme.primary + : Get.theme.colorScheme.inverseSurface, borderRadius: BorderRadius.circular(defaultSpacing), - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Row( - children: [ - Icon( - element.icon, - color: Theme.of(context).colorScheme.onPrimary, - size: Get.theme.textTheme.titleLarge!.fontSize! * 1.5, - ), - horizontalSpacing(defaultSpacing), - Expanded( - child: Text( - "settings.${element.label}".tr, - style: Theme.of(context).textTheme.labelLarge!, - overflow: TextOverflow.ellipsis, + child: InkWell( + onTap: () { + if (widget.category != null) { + widget.category!.value = element; + } else { + Get.to(element.widget, transition: Transition.fadeIn); + } + }, + borderRadius: BorderRadius.circular(defaultSpacing), + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + Icon( + element.icon, + color: Theme.of(context).colorScheme.onPrimary, + size: Get.theme.textTheme.titleLarge!.fontSize! * 1.5, + ), + horizontalSpacing(defaultSpacing), + Expanded( + child: Text( + "settings.${element.label}".tr, + style: Theme.of(context).textTheme.labelLarge!, + overflow: TextOverflow.ellipsis, + ), ), - ), - ], + ], + ), ), ), ), - ), - ); - }).toList(), - ) - ], - ), - ); - }, - ), + ); + }).toList(), + ), + ], + ), + ); + }, ), ), ], diff --git a/lib/pages/settings/town/admin_account_profile.dart b/lib/pages/settings/town/admin_account_profile.dart index 78c684d3..800e11ec 100644 --- a/lib/pages/settings/town/admin_account_profile.dart +++ b/lib/pages/settings/town/admin_account_profile.dart @@ -9,30 +9,34 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class AdminAccountProfile extends StatefulWidget { final AccountData account; - const AdminAccountProfile({ - super.key, - required this.account, - }); + const AdminAccountProfile({super.key, required this.account}); @override State createState() => _AdminAccountProfileState(); } class _AdminAccountProfileState extends State { - late SmoothDialogController controller; - late AccountData current; + late SmoothDialogController _controller; - final currentTab = "settings.acc_profile.tab.info".tr.obs; - late Map tabs; + final _currentTab = signal("settings.acc_profile.tab.info".tr); + late Map _tabs; + + @override + void dispose() { + _currentTab.dispose(); + super.dispose(); + } @override void initState() { - tabs = { - "settings.acc_profile.tab.info".tr: () => Column( + _tabs = { + "settings.acc_profile.tab.info".tr: + () => Column( children: [ // Fields for copying all the account data LPHCopyField(label: "settings.acc_profile.info.id".tr, value: widget.account.id), @@ -42,14 +46,22 @@ class _AdminAccountProfileState extends State { Row( mainAxisSize: MainAxisSize.max, children: [ - Expanded(child: LPHCopyField(label: "settings.acc_profile.info.username".tr, value: widget.account.username)), + Expanded( + child: LPHCopyField(label: "settings.acc_profile.info.username".tr, value: widget.account.username), + ), horizontalSpacing(defaultSpacing), - Expanded(child: LPHCopyField(label: "settings.acc_profile.info.display_name".tr, value: widget.account.displayName)), + Expanded( + child: LPHCopyField( + label: "settings.acc_profile.info.display_name".tr, + value: widget.account.displayName, + ), + ), ], ), ], ), - "settings.acc_profile.tab.actions".tr: () => Column( + "settings.acc_profile.tab.actions".tr: + () => Column( children: [ LPHActionField( primary: "rank".tr, @@ -58,24 +70,20 @@ class _AdminAccountProfileState extends State { LPHActionData( icon: Icons.edit, tooltip: "edit".tr, - onClick: () => Get.dialog(ChangeRankWindow( - data: widget.account, - onUpdate: acceptUpdate, - )), - ) + onClick: () => Get.dialog(ChangeRankWindow(data: widget.account, onUpdate: acceptUpdate)), + ), ], ), ], ), }; - current = widget.account; - controller = SmoothDialogController(tabs[currentTab.value]!()); + _controller = SmoothDialogController(_tabs[_currentTab.value]!()); super.initState(); } void acceptUpdate(AccountData data) { - controller.transitionToContinuos(tabs[currentTab.value]!()); + _controller.transitionToContinuos(_tabs[_currentTab.value]!()); } @override @@ -83,30 +91,23 @@ class _AdminAccountProfileState extends State { return DialogBase( title: [ Text( - "settings.acc_profile.title".trParams({ - "name": "${widget.account.displayName} (${widget.account.username})", - }), + "settings.acc_profile.title".trParams({"name": "${widget.account.displayName} (${widget.account.username})"}), style: Get.textTheme.labelLarge, - ) + ), ], maxWidth: 500, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LPHTabElement( - tabs: [ - "settings.acc_profile.tab.info".tr, - "settings.acc_profile.tab.actions".tr, - ], + tabs: ["settings.acc_profile.tab.info".tr, "settings.acc_profile.tab.actions".tr], onTabSwitch: (tab) { - currentTab.value = tab; - controller.transitionToContinuos(tabs[tab]!()); + _currentTab.value = tab; + _controller.transitionToContinuos(_tabs[tab]!()); }, ), verticalSpacing(defaultSpacing), - SmoothBox( - controller: controller, - ), + SmoothBox(controller: _controller), ], ), ); @@ -127,9 +128,7 @@ class _ChangeRankWindowState extends State { @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text("rank".tr, style: Get.textTheme.labelLarge), - ], + title: [Text("rank".tr, style: Get.textTheme.labelLarge)], child: Column( children: [ Text("settings.rank_change.desc".tr, style: Get.textTheme.bodyMedium), @@ -141,50 +140,47 @@ class _ChangeRankWindowState extends State { verticalSpacing(elementSpacing), Column( mainAxisSize: MainAxisSize.min, - children: List.generate( - StatusController.ranks.length, - (index) { - final rank = StatusController.ranks[index]; - - return Padding( - padding: const EdgeInsets.only(top: defaultSpacing), - child: Material( - color: Get.theme.colorScheme.inverseSurface, + children: List.generate(StatusController.ranks.length, (index) { + final rank = StatusController.ranks[index]; + + return Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: Material( + color: Get.theme.colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(defaultSpacing), + child: InkWell( borderRadius: BorderRadius.circular(defaultSpacing), - child: InkWell( - borderRadius: BorderRadius.circular(defaultSpacing), - onTap: () async { - final json = await postAuthorizedJSON("/townhall/accounts/change_rank", { - "account": widget.data.id, - "rank": rank.id, - }); - - if (!json["success"]) { - showErrorPopup("error", json["error"]); - return; - } - - // Update the rank in the UI - widget.data.rankID = rank.id; - widget.onUpdate(widget.data); - - Get.back(); - }, - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Row( - children: [ - Icon(Icons.military_tech, color: Get.theme.colorScheme.onPrimary), - horizontalSpacing(defaultSpacing), - Text("${rank.name} (${rank.level})", style: Get.textTheme.labelLarge), - ], - ), + onTap: () async { + final json = await postAuthorizedJSON("/townhall/accounts/change_rank", { + "account": widget.data.id, + "rank": rank.id, + }); + + if (!json["success"]) { + showErrorPopup("error", json["error"]); + return; + } + + // Update the rank in the UI + widget.data.rankID = rank.id; + widget.onUpdate(widget.data); + + Get.back(); + }, + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + Icon(Icons.military_tech, color: Get.theme.colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Text("${rank.name} (${rank.level})", style: Get.textTheme.labelLarge), + ], ), ), ), - ); - }, - ), + ), + ); + }), ), ], ), diff --git a/lib/pages/settings/town/admin_accounts_page.dart b/lib/pages/settings/town/admin_accounts_page.dart index 0b82ca0a..dd89845c 100644 --- a/lib/pages/settings/town/admin_accounts_page.dart +++ b/lib/pages/settings/town/admin_accounts_page.dart @@ -3,9 +3,9 @@ import 'dart:isolate'; import 'package:chat_interface/pages/settings/settings_page_base.dart'; import 'package:chat_interface/pages/settings/town/admin_account_profile.dart'; -import 'package:chat_interface/pages/settings/town/server_file_viewer.dart'; import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; +import 'package:chat_interface/theme/components/lph_page_switcher.dart'; import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; @@ -13,6 +13,7 @@ import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class AccountData { String id; @@ -21,8 +22,8 @@ class AccountData { String displayName; int rankID; DateTime createdAt; - final deleted = false.obs; - final deleteLoading = false.obs; + final deleted = signal(false); + final deleteLoading = signal(false); AccountData({ required this.id, @@ -66,46 +67,52 @@ class AdminAccountsPage extends StatefulWidget { } class _AdminAccountsPageState extends State { - final accounts = RxList.empty(); - final query = "".obs; - final startLoading = true.obs; - final pageLoading = false.obs; - final currentPage = 0.obs; - final totalCount = 0.obs; - Future? currentFuture; + final _accounts = listSignal([]); + final _query = signal(""); + final _startLoading = signal(true); + final _pageLoading = signal(false); + final _currentPage = signal(0); + final _totalCount = signal(0); + Future? _currentFuture; + + @override + void dispose() { + _accounts.dispose(); + _query.dispose(); + _startLoading.dispose(); + _pageLoading.dispose(); + _currentPage.dispose(); + _totalCount.dispose(); + super.dispose(); + } @override void initState() { goToPage(0); - query.listen( - (qry) async { - if (currentFuture != null) { - await currentFuture; - } - unawaited(goToPage(currentPage.value)); - currentFuture = Future.delayed(500.ms); - }, - ); + _query.subscribe((qry) async { + if (_currentFuture != null) { + await _currentFuture; + } + unawaited(goToPage(_currentPage.value)); + _currentFuture = Future.delayed(500.ms); + }); super.initState(); } Future goToPage(int page) async { // Set the current page - if (pageLoading.value) { + if (_pageLoading.value) { return; } - pageLoading.value = true; - currentPage.value = page; + _pageLoading.value = true; + _currentPage.value = page; // Get the files from the server - final json = await postAuthorizedJSON("/townhall/accounts/list", { - "page": page, - "query": query.value, - }); - startLoading.value = false; - pageLoading.value = false; + final json = await postAuthorizedJSON("/townhall/accounts/list", {"page": page, "query": _query.value}); + _startLoading.value = false; + _pageLoading.value = false; // Check if there was an error if (!json["success"]) { @@ -115,12 +122,12 @@ class _AdminAccountsPageState extends State { // Parse the entire json if (json["accounts"] == null) { - accounts.clear(); + _accounts.clear(); return; } // Set the total amount of files - totalCount.value = json["count"]; + _totalCount.value = json["count"]; // Decrypt some stuff in an isolate final list = await Isolate.run(() { @@ -134,7 +141,7 @@ class _AdminAccountsPageState extends State { }); // Update the UI - accounts.value = list; + _accounts.value = list; } @override @@ -145,27 +152,23 @@ class _AdminAccountsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400, - ), + constraints: const BoxConstraints(maxWidth: 400), child: FJTextField( prefixIcon: Icons.search, hintText: "settings.accounts.search".tr, secondaryColor: true, onChange: (qry) { - query.value = qry; + _query.value = qry; }, ), ), verticalSpacing(defaultSpacing), - Obx(() { - if (startLoading.value) { - return CircularProgressIndicator( - color: Get.theme.colorScheme.onPrimary, - ); + Watch((ctx) { + if (_startLoading.value) { + return CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary); } - if (!accounts.isNotEmpty) { + if (!_accounts.isNotEmpty) { return Padding( padding: const EdgeInsets.only(top: elementSpacing), child: Text("settings.accounts.none".tr, style: Get.theme.textTheme.bodyMedium), @@ -176,24 +179,24 @@ class _AdminAccountsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Top page switcher element - PageSwitcher( - loading: pageLoading, - currentPage: currentPage, - count: totalCount, + LPHPageSwitcher( + loading: _pageLoading, + currentPage: _currentPage, + count: _totalCount, page: (page) => goToPage(page), ), verticalSpacing(defaultSpacing), // The view rendering all the accounts ListView.builder( - itemCount: accounts.length, + itemCount: _accounts.length, shrinkWrap: true, itemBuilder: (context, index) { - final account = accounts[index]; + final account = _accounts[index]; - // Obx for delete animation - return Obx( - () => Animate( + // Render the delete animation + return Watch( + (ctx) => Animate( key: ValueKey(account.id), effects: [ ReverseExpandEffect( @@ -207,11 +210,7 @@ class _AdminAccountsPageState extends State { curve: Curves.ease, duration: 1000.ms, ), - FadeEffect( - begin: 1, - end: 0, - duration: 1000.ms, - ) + FadeEffect(begin: 1, end: 0, duration: 1000.ms), ], onInit: (controller) => controller.value = account.deleted.value ? 1 : 0, target: account.deleted.value ? 1 : 0, @@ -222,75 +221,84 @@ class _AdminAccountsPageState extends State { borderRadius: BorderRadius.circular(10), child: Padding( padding: const EdgeInsets.symmetric(horizontal: defaultSpacing, vertical: defaultSpacing), - child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Icon( - Icons.account_circle, - color: Theme.of(context).colorScheme.onPrimary, - size: 30, - ), - horizontalSpacing(defaultSpacing), - - // Account data (name and creation) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("${account.displayName} (${account.username})", style: Get.theme.textTheme.labelMedium), - Text( - "settings.accounts.created".trParams({ - "date": formatOnlyYear(account.createdAt), - "time": formatMessageTime(account.createdAt), - }), - style: Get.theme.textTheme.bodyMedium, - ), - ], - ), - ], - ), - - // Actions that can be performed on the account - Row( - children: [ - // Launch button (go to their profile) - LoadingIconButton( - loading: false.obs, - onTap: () => Get.dialog(AdminAccountProfile(account: account)), - icon: Icons.launch, - ), - - // Delete icon button - LoadingIconButton( - loading: account.deleteLoading, - onTap: () async { - if (account.deleteLoading.value) { - return; - } - - unawaited(showConfirmPopup(ConfirmWindow( - title: "settings.accounts.delete.confirm".tr, - text: "settings.accounts.delete.desc".tr, - onConfirm: () async { - account.deleteLoading.value = true; - final json = await postAuthorizedJSON("/townhall/accounts/delete", { - "account": account.id, - }); - account.deleteLoading.value = false; - - if (!json["success"]) { - showErrorPopup("error", json["error"]); - return; - } - - account.deleted.value = true; - }, - ))); - }, - icon: Icons.delete, - ), - ], - ) - ]), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.account_circle, + color: Theme.of(context).colorScheme.onPrimary, + size: 30, + ), + horizontalSpacing(defaultSpacing), + + // Account data (name and creation) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${account.displayName} (${account.username})", + style: Get.theme.textTheme.labelMedium, + ), + Text( + "settings.accounts.created".trParams({ + "date": formatOnlyYear(account.createdAt), + "time": formatMessageTime(account.createdAt), + }), + style: Get.theme.textTheme.bodyMedium, + ), + ], + ), + ], + ), + + // Actions that can be performed on the account + Row( + children: [ + // Launch button (go to manage their account) + LoadingIconButton( + onTap: () => Get.dialog(AdminAccountProfile(account: account)), + icon: Icons.launch, + ), + + // Delete icon button + LoadingIconButton( + loading: account.deleteLoading, + onTap: () async { + if (account.deleteLoading.value) { + return; + } + + unawaited( + showConfirmPopup( + ConfirmWindow( + title: "settings.accounts.delete.confirm".tr, + text: "settings.accounts.delete.desc".tr, + onConfirm: () async { + account.deleteLoading.value = true; + final json = await postAuthorizedJSON("/townhall/accounts/delete", { + "account": account.id, + }); + account.deleteLoading.value = false; + + if (!json["success"]) { + showErrorPopup("error", json["error"]); + return; + } + + account.deleted.value = true; + }, + ), + ), + ); + }, + icon: Icons.delete, + ), + ], + ), + ], + ), ), ), ), @@ -300,16 +308,16 @@ class _AdminAccountsPageState extends State { ), // Bottom page switcher - PageSwitcher( - loading: pageLoading, - currentPage: currentPage, - count: totalCount, + LPHPageSwitcher( + loading: _pageLoading, + currentPage: _currentPage, + count: _totalCount, page: (page) => goToPage(page), ), verticalSpacing(defaultSpacing), ], ); - }) + }), ], ), ); diff --git a/lib/pages/settings/town/file_settings.dart b/lib/pages/settings/town/file_settings.dart index ad9e3a59..9e98abae 100644 --- a/lib/pages/settings/town/file_settings.dart +++ b/lib/pages/settings/town/file_settings.dart @@ -17,6 +17,7 @@ import 'package:get/get.dart'; import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; +import 'package:signals/signals_flutter.dart'; class FileSettings { // Auto download @@ -47,13 +48,13 @@ class FileSettings { return imageTypes.contains(ext) || audioTypes.contains(ext) || videoTypes.contains(ext); } - static void addSettings(SettingController controller) { - controller.settings[autoDownloadImages] = Setting(autoDownloadImages, isWeb ? false : true); - controller.settings[autoDownloadVideos] = Setting(autoDownloadVideos, false); - controller.settings[autoDownloadAudio] = Setting(autoDownloadAudio, false); - controller.settings[maxFileSize] = Setting(maxFileSize, isWeb ? 1.0 : 5.0); - controller.settings[maxCacheSize] = Setting(maxCacheSize, 500.0); - controller.settings[fileCacheType] = Setting(fileCacheType, 0); + static void addSettings() { + SettingController.addSetting(Setting(autoDownloadImages, isWeb ? false : true)); + SettingController.addSetting(Setting(autoDownloadVideos, false)); + SettingController.addSetting(Setting(autoDownloadAudio, false)); + SettingController.addSetting(Setting(maxFileSize, isWeb ? 1.0 : 5.0)); + SettingController.addSetting(Setting(maxCacheSize, 500.0)); + SettingController.addSetting(Setting(fileCacheType, 0)); } } @@ -96,13 +97,13 @@ class FileSettingsPage extends StatelessWidget { verticalSpacing(defaultSpacing + elementSpacing), ListSelectionSetting( - settingName: FileSettings.fileCacheType, + setting: SettingController.settings[FileSettings.fileCacheType]! as Setting, items: FileSettings.fileCacheTypes, ), - Obx( - () => Visibility( - visible: Get.find().settings[FileSettings.fileCacheType]!.getValue() == 1, + Watch( + (ctx) => Visibility( + visible: SettingController.settings[FileSettings.fileCacheType]!.getValue() == 1, child: const DoubleSelectionSetting( settingName: FileSettings.maxCacheSize, description: "", @@ -120,7 +121,10 @@ class FileSettingsPage extends StatelessWidget { children: [ FJElevatedButton( onTap: () async { - final cacheFolder = path.join((await getApplicationCacheDirectory()).path, ".file_cache_${StatusController.ownAccountId}"); + final cacheFolder = path.join( + (await getApplicationCacheDirectory()).path, + ".file_cache_${StatusController.ownAccountId}", + ); unawaited(OpenFile.open(cacheFolder)); }, child: Row( @@ -134,7 +138,10 @@ class FileSettingsPage extends StatelessWidget { ), FJElevatedButton( onTap: () async { - final fileFolder = path.join((await getApplicationSupportDirectory()).path, "saved_files_${StatusController.ownAccountId}"); + final fileFolder = path.join( + (await getApplicationSupportDirectory()).path, + "saved_files_${StatusController.ownAccountId}", + ); unawaited(OpenFile.open(fileFolder)); }, child: Row( @@ -148,7 +155,10 @@ class FileSettingsPage extends StatelessWidget { ), FJElevatedButton( onTap: () async { - final fileFolder = path.join((await getApplicationSupportDirectory()).path, "cloud_files_${StatusController.ownAccountId}"); + final fileFolder = path.join( + (await getApplicationSupportDirectory()).path, + "cloud_files_${StatusController.ownAccountId}", + ); unawaited(OpenFile.open(fileFolder)); }, child: Row( diff --git a/lib/pages/settings/town/server_file_viewer.dart b/lib/pages/settings/town/server_file_viewer.dart index 91062a47..c08e6bc2 100644 --- a/lib/pages/settings/town/server_file_viewer.dart +++ b/lib/pages/settings/town/server_file_viewer.dart @@ -1,5 +1,6 @@ -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/theme/components/lph_page_switcher.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; import 'package:chat_interface/main.dart'; import 'package:chat_interface/pages/chat/components/message/renderer/bubbles/bubbles_zap_renderer.dart'; @@ -13,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'package:open_file/open_file.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:sodium_libs/sodium_libs.dart'; class ServerFileViewer extends StatefulWidget { @@ -23,13 +25,13 @@ class ServerFileViewer extends StatefulWidget { } class _ConversationsPageState extends State { - final files = RxList.empty(); - final query = "".obs; - final startLoading = true.obs; - final pageLoading = false.obs; - final currentPage = 0.obs; - final totalCount = 0.obs; - final storageLine = "loading".tr.obs; + final _files = listSignal([]); + final _query = signal(""); + final _startLoading = signal(true); + final _pageLoading = signal(false); + final _currentPage = signal(0); + final _totalCount = signal(0); + final _storageLine = signal("loading".tr); final extensionMap = { "webp": Icons.image, @@ -44,6 +46,18 @@ class _ConversationsPageState extends State { "mp3": Icons.library_music, }; + @override + void dispose() { + _files.dispose(); + _query.dispose(); + _startLoading.dispose(); + _pageLoading.dispose(); + _currentPage.dispose(); + _totalCount.dispose(); + _storageLine.dispose(); + super.dispose(); + } + @override void initState() { goToPage(0); @@ -54,10 +68,10 @@ class _ConversationsPageState extends State { Future getStorageData() async { final json = await postAuthorizedJSON("/account/files/storage", {}); if (!json["success"]) { - storageLine.value = json["error"]; + _storageLine.value = json["error"]; return; } - storageLine.value = "settings.file.uploaded.description".trParams({ + _storageLine.value = "settings.file.uploaded.description".trParams({ "current": formatFileSize(json["amount"]), "max": formatFileSize(json["max"]), }); @@ -65,18 +79,16 @@ class _ConversationsPageState extends State { Future goToPage(int page) async { // Set the current page - if (pageLoading.value) { + if (_pageLoading.value) { return; } - pageLoading.value = true; - currentPage.value = page; + _pageLoading.value = true; + _currentPage.value = page; // Get the files from the server - final json = await postAuthorizedJSON("/account/files/list", { - "page": page, - }); - startLoading.value = false; - pageLoading.value = false; + final json = await postAuthorizedJSON("/account/files/list", {"page": page}); + _startLoading.value = false; + _pageLoading.value = false; // Check if there was an error if (!json["success"]) { @@ -86,12 +98,12 @@ class _ConversationsPageState extends State { // Parse the entire json if (json["files"] == null) { - files.clear(); + _files.clear(); return; } // Set the total amount of files - totalCount.value = json["count"]; + _totalCount.value = json["count"]; // Decrypt some stuff in an isolate final list = await sodiumLib.runIsolated((sodium, secureKeys, keyPairs) async { @@ -110,7 +122,7 @@ class _ConversationsPageState extends State { } // Update the UI - files.value = list; + _files.value = list; } @override @@ -118,25 +130,21 @@ class _ConversationsPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Obx( - () => Text( - "settings.file.uploaded.title".trParams({ - "count": totalCount.value.toString(), - }), + Watch( + (ctx) => Text( + "settings.file.uploaded.title".trParams({"count": _totalCount.value.toString()}), style: Get.theme.textTheme.labelLarge, ), ), verticalSpacing(defaultSpacing), - Obx(() => Text(storageLine.value, style: Get.theme.textTheme.bodyMedium)), + Watch((ctx) => Text(_storageLine.value, style: Get.theme.textTheme.bodyMedium)), verticalSpacing(defaultSpacing), - Obx(() { - if (startLoading.value) { - return CircularProgressIndicator( - color: Get.theme.colorScheme.onPrimary, - ); + Watch((ctx) { + if (_startLoading.value) { + return CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary); } - if (!files.isNotEmpty) { + if (!_files.isNotEmpty) { return Padding( padding: const EdgeInsets.only(top: elementSpacing), child: Text("settings.file.uploaded.none".tr, style: Get.theme.textTheme.labelMedium), @@ -146,40 +154,32 @@ class _ConversationsPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - PageSwitcher( - loading: pageLoading, - currentPage: currentPage, - count: totalCount, + LPHPageSwitcher( + loading: _pageLoading, + currentPage: _currentPage, + count: _totalCount, page: (page) => goToPage(page), ), verticalSpacing(defaultSpacing), ListView.builder( - itemCount: files.length, + itemCount: _files.length, shrinkWrap: true, itemBuilder: (context, index) { - final file = files[index]; + final file = _files[index]; final extension = file.id.split(".").last; - return Obx( - () => Animate( + return Watch( + (ctx) => Animate( key: ValueKey(file.id), effects: [ - ReverseExpandEffect( - axis: Axis.vertical, - curve: const ElasticOutCurve(2.0), - duration: 1000.ms, - ), + ReverseExpandEffect(axis: Axis.vertical, curve: const ElasticOutCurve(2.0), duration: 1000.ms), ScaleEffect( begin: const Offset(1, 1), end: const Offset(0, 0), curve: Curves.ease, duration: 1000.ms, ), - FadeEffect( - begin: 1, - end: 0, - duration: 1000.ms, - ) + FadeEffect(begin: 1, end: 0, duration: 1000.ms), ], onInit: (controller) => controller.value = file.deleted.value ? 1 : 0, target: file.deleted.value ? 1 : 0, @@ -237,7 +237,7 @@ class _ConversationsPageState extends State { file.deleteLoading.value = true; // Make a request to the server - final success = await Get.find().deleteFileFromPath( + final success = await AttachmentController.deleteFileFromPath( file.id, file.path != null ? XFile(file.path!) : null, popup: true, @@ -252,7 +252,7 @@ class _ConversationsPageState extends State { icon: Icons.delete, ), ], - ) + ), ], ), ), @@ -262,99 +262,16 @@ class _ConversationsPageState extends State { ); }, ), - PageSwitcher( - loading: pageLoading, - currentPage: currentPage, - count: totalCount, + LPHPageSwitcher( + loading: _pageLoading, + currentPage: _currentPage, + count: _totalCount, page: (page) => goToPage(page), ), verticalSpacing(defaultSpacing), ], ); - }) - ], - ); - } -} - -class PageSwitcher extends StatefulWidget { - final RxInt currentPage; - final RxInt count; - final RxBool loading; - final Function(int) page; - - const PageSwitcher({ - super.key, - required this.currentPage, - required this.count, - required this.loading, - required this.page, - }); - - @override - State createState() => _PageSwitcherState(); -} - -class _PageSwitcherState extends State { - int getMaxPage() => (widget.count.value / 20).ceil(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - LoadingIconButton( - loading: widget.loading, - onTap: () { - if (widget.currentPage.value == 0) { - return; - } - widget.page(0); - }, - icon: Icons.skip_previous, - ), - horizontalSpacing(elementSpacing), - LoadingIconButton( - loading: widget.loading, - onTap: () { - if (widget.currentPage.value == 0) { - return; - } - widget.page(widget.currentPage.value - 1); - }, - icon: Icons.arrow_back, - ), - const Spacer(), - Obx( - () => Text( - "page_switcher".trParams({ - "count": (widget.currentPage.value + 1).toString(), - "max": getMaxPage().toString(), - }), - style: Get.textTheme.labelLarge, - ), - ), - const Spacer(), - LoadingIconButton( - loading: widget.loading, - onTap: () { - if (widget.currentPage.value == getMaxPage() - 1) { - return; - } - widget.page(widget.currentPage.value + 1); - }, - icon: Icons.arrow_forward, - ), - horizontalSpacing(elementSpacing), - LoadingIconButton( - loading: widget.loading, - onTap: () { - if (widget.currentPage.value == getMaxPage() - 1) { - return; - } - widget.page(getMaxPage() - 1); - }, - icon: Icons.skip_next, - ), + }), ], ); } @@ -371,8 +288,8 @@ class FileContainer { bool system; int createdAt; String? path; - final deleteLoading = false.obs; - final deleted = false.obs; + final deleteLoading = signal(false); + final deleted = signal(false); FileContainer( this.id, diff --git a/lib/pages/settings/town/tabletop_settings.dart b/lib/pages/settings/town/tabletop_settings.dart index 4afb1d10..f0bbb2c9 100644 --- a/lib/pages/settings/town/tabletop_settings.dart +++ b/lib/pages/settings/town/tabletop_settings.dart @@ -29,6 +29,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:drift/drift.dart' as drift; import 'package:liphium_bridge/liphium_bridge.dart'; +import 'package:signals/signals_flutter.dart'; class TabletopSettings { static const String framerate = "tabletop.framerate"; @@ -37,29 +38,30 @@ class TabletopSettings { // Experimental settings static const String smoothDragging = "tabletop.smooth_dragging"; - static void addSettings(SettingController controller) { - controller.settings[framerate] = Setting(framerate, 60.0); - controller.settings[cursorHue] = Setting(cursorHue, 0.0); + static void addSettings() { + SettingController.addSetting(Setting(framerate, 60.0)); + SettingController.addSetting(Setting(cursorHue, 0.0)); - controller.settings[smoothDragging] = Setting(smoothDragging, false); + // I don't know if we will ever do this xd + SettingController.addSetting(Setting(smoothDragging, false)); } /// Initialize the cursor hue to make sure it's actually randomized by default static Future initSettings() async { final val = await (db.setting.select()..where((tbl) => tbl.key.equals(cursorHue))).getSingleOrNull(); if (val == null) { - await Get.find().settings[cursorHue]!.setValue(Random().nextDouble()); + await SettingController.settings[cursorHue]!.setValue(Random().nextDouble()); } } static Color getCursorColor({double? hue}) { final themeHSL = HSLColor.fromColor(Get.theme.colorScheme.onPrimary); - hue ??= Get.find().settings[cursorHue]!.getValue(); + hue ??= SettingController.settings[cursorHue]!.getValue(); return HSLColor.fromAHSL(1.0, hue! * 360, themeHSL.saturation, themeHSL.lightness).toColor(); } static double getHue() { - return Get.find().settings[cursorHue]!.getValue(); + return SettingController.settings[cursorHue]!.getValue(); } } @@ -70,8 +72,8 @@ class TabletopSettingsPage extends StatefulWidget { State createState() => _TabletopSettingsPageState(); } -class _TabletopSettingsPageState extends State { - final _selected = "settings.tabletop.general".tr.obs; +class _TabletopSettingsPageState extends State with SignalsMixin { + late final _selected = createSignal("settings.tabletop.general".tr); // Tabs final _tabs = { @@ -97,7 +99,7 @@ class _TabletopSettingsPageState extends State { verticalSpacing(sectionSpacing), //* Current tab - Obx(() => _tabs[_selected.value]!) + _tabs[_selected.value]!, ], ), ); @@ -113,14 +115,20 @@ class TabletopGeneralTab extends StatefulWidget { class _TabletopGeneralTabState extends State { /// The hue of the cursor (for updating the preview) - final _cursorHue = 0.0.obs; + final _cursorHue = signal(0.0); @override void initState() { - _cursorHue.value = Get.find().settings[TabletopSettings.cursorHue]!.getValue(); + _cursorHue.value = SettingController.settings[TabletopSettings.cursorHue]!.getValue(); super.initState(); } + @override + void dispose() { + _cursorHue.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( @@ -151,8 +159,8 @@ class _TabletopGeneralTabState extends State { height: 45, child: Stack( children: [ - Obx( - () => Container( + Watch( + (ctx) => Container( width: 45, height: 45, decoration: BoxDecoration( @@ -161,17 +169,11 @@ class _TabletopGeneralTabState extends State { ), ), ), - Align( - alignment: Alignment.center, - child: UserAvatar( - id: StatusController.ownAddress, - size: 40, - ), - ), + Align(alignment: Alignment.center, child: UserAvatar(id: StatusController.ownAddress, size: 40)), Align( alignment: Alignment.bottomRight, - child: Obx( - () => Padding( + child: Watch( + (ctx) => Padding( padding: const EdgeInsets.all(0.0), child: Container( width: 12, @@ -216,9 +218,17 @@ class TabletopDeckTab extends StatefulWidget { class _TabletopDeckTabState extends State { // Deck list - final _decks = [].obs; - final _loading = true.obs; - final _error = false.obs; + final _decks = listSignal([]); + final _loading = signal(true); + final _error = signal(false); + + @override + void dispose() { + _decks.dispose(); + _loading.dispose(); + _error.dispose(); + super.dispose(); + } @override void initState() { @@ -239,22 +249,13 @@ class _TabletopDeckTabState extends State { @override Widget build(BuildContext context) { - return Obx(() { + return Watch((ctx) { if (_loading.value) { - return Center( - child: CircularProgressIndicator( - color: Get.theme.colorScheme.onPrimary, - ), - ); + return Center(child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary)); } if (_error.value) { - return Center( - child: ErrorContainer( - message: "settings.tabletop.decks.error".tr, - expand: true, - ), - ); + return Center(child: ErrorContainer(message: "settings.tabletop.decks.error".tr, expand: true)); } return Column( @@ -263,8 +264,8 @@ class _TabletopDeckTabState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Obx( - () => Text( + Watch( + (ctx) => Text( "settings.tabletop.decks.limit".trParams({ "count": _decks.length.toString(), "limit": Constants.maxDecks.toString(), @@ -288,10 +289,7 @@ class _TabletopDeckTabState extends State { children: [ Icon(Icons.add, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(elementSpacing), - Text( - "decks.create".tr, - style: Get.theme.textTheme.labelLarge, - ), + Text("decks.create".tr, style: Get.theme.textTheme.labelLarge), ], ), ), @@ -320,9 +318,9 @@ class _TabletopDeckTabState extends State { children: [ Text(deck.name, style: Get.theme.textTheme.labelLarge), verticalSpacing(elementSpacing), - Obx( - () => Text( - "decks.cards".trParams({"count": deck.cards.length.toString()}), + Watch( + (context) => Text( + "decks.cards".trParams({"count": deck.cards.value.length.toString()}), style: Get.theme.textTheme.bodyMedium, ), ), @@ -344,16 +342,19 @@ class _TabletopDeckTabState extends State { ), horizontalSpacing(elementSpacing), IconButton( - onPressed: () => showConfirmPopup(ConfirmWindow( - title: "decks.dialog.delete.title".tr, - text: "decks.dialog.delete".tr, - onConfirm: () async { - final res = await deck.delete(); - if (res) { - _decks.remove(deck); - } - }, - )), + onPressed: + () => showConfirmPopup( + ConfirmWindow( + title: "decks.dialog.delete.title".tr, + text: "decks.dialog.delete".tr, + onConfirm: () async { + final res = await deck.delete(); + if (res) { + _decks.remove(deck); + } + }, + ), + ), icon: const Icon(Icons.delete), ), horizontalSpacing(defaultSpacing), @@ -364,10 +365,7 @@ class _TabletopDeckTabState extends State { children: [ Icon(Icons.filter, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(elementSpacing), - Text( - "decks.view_cards".tr, - style: Get.theme.textTheme.labelLarge, - ), + Text("decks.view_cards".tr, style: Get.theme.textTheme.labelLarge), ], ), ), @@ -377,7 +375,7 @@ class _TabletopDeckTabState extends State { ), ); }, - ) + ), ], ); }); @@ -396,11 +394,13 @@ class DeckCreationWindow extends StatefulWidget { class _DeckCreationWindowState extends State { final TextEditingController _nameController = TextEditingController(); - final _errorText = "".obs; - final _loading = false.obs; + final _errorText = signal(""); + final _loading = signal(false); @override void dispose() { + _errorText.dispose(); + _loading.dispose(); _nameController.dispose(); super.dispose(); } @@ -438,7 +438,10 @@ class _DeckCreationWindowState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(widget.deck == null ? "decks.dialog.name".tr : "decks.dialog.new_name".tr, style: Get.theme.textTheme.bodyMedium), + Text( + widget.deck == null ? "decks.dialog.name".tr : "decks.dialog.new_name".tr, + style: Get.theme.textTheme.bodyMedium, + ), verticalSpacing(sectionSpacing), FJTextField( hintText: "decks.dialog.name.placeholder".tr, @@ -456,16 +459,17 @@ class _DeckCreationWindowState extends State { FJElevatedLoadingButtonCustom( loading: _loading, onTap: () => createDeck(), - builder: () => Center( - child: SizedBox( - height: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, - width: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, - child: Padding( - padding: const EdgeInsets.all(defaultSpacing * 0.25), - child: CircularProgressIndicator(strokeWidth: 3.0, color: Get.theme.colorScheme.onPrimary), + builder: + () => Center( + child: SizedBox( + height: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, + width: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, + child: Padding( + padding: const EdgeInsets.all(defaultSpacing * 0.25), + child: CircularProgressIndicator(strokeWidth: 3.0, color: Get.theme.colorScheme.onPrimary), + ), + ), ), - ), - ), child: Center( child: Text(widget.deck == null ? "create".tr : "save".tr, style: Get.theme.textTheme.labelLarge), ), @@ -480,10 +484,7 @@ class _DeckCreationWindowState extends State { class DeckCardsWindow extends StatefulWidget { final TabletopDeck deck; - const DeckCardsWindow({ - super.key, - required this.deck, - }); + const DeckCardsWindow({super.key, required this.deck}); @override State createState() => _DeckCardsWindowState(); @@ -515,9 +516,9 @@ class _DeckCardsWindowState extends State { Text(widget.deck.name, style: Get.theme.textTheme.titleLarge), FJElevatedButton( onTap: () async { - final result = await openFiles(acceptedTypeGroups: [ - const XTypeGroup(label: "Image", extensions: FileSettings.staticImageTypes), - ]); + final result = await openFiles( + acceptedTypeGroups: [const XTypeGroup(label: "Image", extensions: FileSettings.staticImageTypes)], + ); if (result.isEmpty) { return; } @@ -542,11 +543,11 @@ class _DeckCardsWindowState extends State { } // Save to the vault - widget.deck.cards.addAll(response); + widget.deck.cards.value.addAll(response); // Set the amount for all of them to 1 for (var card in response) { - widget.deck.amounts[card.id] = 1; + widget.deck.amounts.value[card.id] = 1; } final res = await widget.deck.save(); @@ -560,10 +561,7 @@ class _DeckCardsWindowState extends State { children: [ Icon(Icons.add, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(elementSpacing), - Text( - "add".tr, - style: Get.theme.textTheme.labelLarge, - ), + Text("add".tr, style: Get.theme.textTheme.labelLarge), ], ), ), @@ -573,22 +571,19 @@ class _DeckCardsWindowState extends State { Flexible( child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 700), - child: Obx(() { - if (widget.deck.cards.isEmpty) { - return Text( - "decks.cards.empty".tr, - style: Get.theme.textTheme.bodyMedium, - ); + child: Watch((context) { + if (widget.deck.cards.value.isEmpty) { + return Text("decks.cards.empty".tr, style: Get.theme.textTheme.bodyMedium); } return SingleChildScrollView( child: GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 200), - itemCount: widget.deck.cards.length, + itemCount: widget.deck.cards.value.length, shrinkWrap: true, itemBuilder: (context, index) { - final card = widget.deck.cards[index]; - final file = widget.deck.cards[index].file!; + final card = widget.deck.cards.value[index]; + final file = widget.deck.cards.value[index].file!; return Stack( children: [ Padding( @@ -608,8 +603,8 @@ class _DeckCardsWindowState extends State { ), child: IconButton( onPressed: () async { - widget.deck.cards.remove(card); - unawaited(Get.find().deleteFile(card)); + widget.deck.cards.value.remove(card); + unawaited(AttachmentController.deleteFile(card)); final result = await widget.deck.save(); if (!result) { showErrorPopup("error", "server.error".tr); @@ -651,17 +646,18 @@ class _DeckCardsWindowState extends State { IconButton( onPressed: () async { changed = true; - if ((widget.deck.amounts[card.id] ?? 1) == 1) { + if ((widget.deck.amounts.value[card.id] ?? 1) == 1) { return; } - widget.deck.amounts[card.id] = (widget.deck.amounts[card.id] ?? 1) - 1; + widget.deck.amounts.value[card.id] = + (widget.deck.amounts.value[card.id] ?? 1) - 1; }, icon: const Icon(Icons.remove), ), horizontalSpacing(elementSpacing), - Obx( - () => Text( - widget.deck.amounts[card.id].toString(), + Watch( + (context) => Text( + widget.deck.amounts.value[card.id].toString(), style: Get.theme.textTheme.labelLarge, ), ), @@ -669,7 +665,8 @@ class _DeckCardsWindowState extends State { IconButton( onPressed: () async { changed = true; - widget.deck.amounts[card.id] = (widget.deck.amounts[card.id] ?? 1) + 1; + widget.deck.amounts.value[card.id] = + (widget.deck.amounts.value[card.id] ?? 1) + 1; }, icon: const Icon(Icons.add), ), @@ -702,8 +699,8 @@ class CardsUploadWindow extends StatefulWidget { State createState() => _CardsUploadWindowState(); } -class _CardsUploadWindowState extends State { - final _current = 0.obs; +class _CardsUploadWindowState extends State with SignalsMixin { + late final _current = createSignal(0); final finished = []; @override @@ -713,11 +710,10 @@ class _CardsUploadWindowState extends State { } Future startFileUploading() async { - final controller = Get.find(); _current.value = 0; for (var file in widget.files) { // Upload the card to the server - final response = await controller.uploadFile( + final response = await AttachmentController.uploadFile( UploadData(file), StorageType.permanent, Constants.fileDeckTag, @@ -744,22 +740,16 @@ class _CardsUploadWindowState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Obx( - () => Text( - "file.uploading".trParams({ - "index": (_current.value).toString(), - "total": widget.files.length.toString(), - }), - style: Get.theme.textTheme.titleLarge), + Text( + "file.uploading".trParams({"index": (_current.value).toString(), "total": widget.files.length.toString()}), + style: Get.theme.textTheme.titleLarge, ), verticalSpacing(sectionSpacing), - Obx( - () => LinearProgressIndicator( - value: _current.value / widget.files.length, - minHeight: 10, - color: Get.theme.colorScheme.onPrimary, - backgroundColor: Get.theme.colorScheme.primary, - ), + LinearProgressIndicator( + value: _current.value / widget.files.length, + minHeight: 10, + color: Get.theme.colorScheme.onPrimary, + backgroundColor: Get.theme.colorScheme.primary, ), ], ), diff --git a/lib/pages/settings/town/town_admin_settings.dart b/lib/pages/settings/town/town_admin_settings.dart index f70ebe86..fd3f32e0 100644 --- a/lib/pages/settings/town/town_admin_settings.dart +++ b/lib/pages/settings/town/town_admin_settings.dart @@ -1,11 +1,14 @@ import 'package:chat_interface/theme/components/forms/fj_slider.dart'; import 'package:chat_interface/theme/components/forms/fj_switch.dart'; import 'package:chat_interface/theme/components/lph_tab_element.dart'; +import 'package:chat_interface/util/dispose_hook.dart'; +import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class CategoryData { final String name; @@ -31,13 +34,20 @@ class TownAdminSettings extends StatefulWidget { class _TownAdminSettingsState extends State { // Error things - final error = "".obs; - final loading = true.obs; - String? currentCategory; + final _error = signal(""); + final _loading = signal(true); + String? _currentCategory; // Current tabs - final categories = []; - final currentTab = Rx?>(null); + final _categories = []; + final _currentTab = listSignal([]); + + @override + void dispose() { + _error.dispose(); + _loading.dispose(); + super.dispose(); + } @override void initState() { @@ -47,30 +57,30 @@ class _TownAdminSettingsState extends State { /// Get all the categories from the server Future fetchCategories() async { - loading.value = true; - error.value = ""; + _loading.value = true; + _error.value = ""; final json = await postAuthorizedJSON("/townhall/settings/categories", {}); - loading.value = false; if (!json["success"]) { - error.value = json["error"]; + _error.value = json["error"]; return; } // Parse all the categories for (var category in json["categories"]) { - categories.add(CategoryData.fromJson(category)); + _categories.add(CategoryData.fromJson(category)); } + _loading.value = false; // Fetch category one - await fetchSettings(categories[0].name); + await fetchSettings(_categories[0].name); } /// Fetch the settings for a category Future fetchSettings(String name) async { - final category = categories.firstWhere((c) => c.name == name); - currentCategory = category.name; + final category = _categories.firstWhere((c) => c.name == name); + _currentCategory = category.name; final json = await postAuthorizedJSON("/townhall/settings/${category.id}", {}); - if (currentCategory != category.name) { + if (_currentCategory != category.name) { return; } @@ -81,7 +91,7 @@ class _TownAdminSettingsState extends State { } // Load the settings - currentTab.value = json["settings"]; + _currentTab.value = json["settings"]; } @override @@ -94,57 +104,52 @@ class _TownAdminSettingsState extends State { Text("settings.town.settings".tr, style: Get.theme.textTheme.labelLarge), verticalSpacing(defaultSpacing), - Obx(() { + Watch((ctx) { // Render a loading indicator in case the tabs are still loading - if (loading.value) { + if (_loading.value) { return Padding( padding: const EdgeInsets.only(top: defaultSpacing), child: Padding( padding: const EdgeInsets.all(defaultSpacing), - child: Center( - child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary), - ), + child: Center(child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary)), ), ); } // Render an error message - if (error.value != "") { - return Text(error.value, style: Get.theme.textTheme.bodyMedium); + if (_error.value != "") { + return Text(_error.value, style: Get.theme.textTheme.bodyMedium); } // Render the tab overview - return LPHTabElement( - tabs: categories.map((c) => c.name).toList(), - onTabSwitch: (tab) => fetchSettings(tab), - ); - }), + return LPHTabElement(tabs: _categories.map((c) => c.name).toList(), onTabSwitch: (tab) => fetchSettings(tab)); + }, dependencies: [_loading, _error]), verticalSpacing(defaultSpacing), - Obx(() { + Watch((ctx) { // Return nothing if there is no tab content - if (currentTab.value == null) { + if (_currentTab.value.isEmpty) { return const SizedBox(); } return Column( - children: List.generate( - currentTab.value!.length, - (index) { - final setting = currentTab.value![index]!; - if (setting["visible"] != null && !setting["visible"]) { - return const SizedBox(); - } - - Widget? settingsWidget; - if (setting["value"] is num) { - final devider = (setting["dev"] as num).toDouble(); - final currentValue = (setting["value"] as num).toDouble().obs; - settingsWidget = Column( + children: List.generate(_currentTab.value.length, (index) { + final setting = _currentTab.value[index]!; + if (setting["visible"] != null && !setting["visible"]) { + return const SizedBox(); + } + + Widget? settingsWidget; + if (setting["value"] is num) { + final devider = (setting["dev"] as num).toDouble(); + final currentValue = signal((setting["value"] as num).toDouble()); + settingsWidget = DisposeHook( + dispose: () => currentValue.dispose(), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(setting["label"], style: Get.textTheme.bodyMedium), - Obx( - () => FJSliderWithInput( + Watch( + (ctx) => FJSliderWithInput( secondaryColor: true, min: (setting["min"] as num).toDouble() / devider, value: currentValue.value / devider, @@ -167,15 +172,18 @@ class _TownAdminSettingsState extends State { ), ), ], - ); - } else if (setting["value"] is bool) { - final currentValue = (setting["value"] as bool).obs; - settingsWidget = Row( + ), + ); + } else if (setting["value"] is bool) { + final currentValue = signal((setting["value"] as bool)); + settingsWidget = DisposeHook( + dispose: () => currentValue.dispose(), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(setting["label"], style: Get.textTheme.bodyMedium), - Obx( - () => FJSwitch( + Watch( + (ctx) => FJSwitch( value: currentValue.value, onChanged: (val) async { currentValue.value = val; @@ -191,15 +199,12 @@ class _TownAdminSettingsState extends State { ), ), ], - ); - } - - return Padding( - padding: const EdgeInsets.only(bottom: defaultSpacing), - child: settingsWidget, + ), ); - }, - ), + } + + return Padding(padding: const EdgeInsets.only(bottom: defaultSpacing), child: settingsWidget); + }), ); }), ], diff --git a/lib/pages/settings/town/town_settings.dart b/lib/pages/settings/town/town_settings.dart index 871c3a69..20c96d10 100644 --- a/lib/pages/settings/town/town_settings.dart +++ b/lib/pages/settings/town/town_settings.dart @@ -10,7 +10,6 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -22,8 +21,6 @@ class TownSettingsPage extends StatefulWidget { } class _TownSettingsPageState extends State { - final maxFileSize = 10.0.obs; - @override Widget build(BuildContext context) { return SettingsPageBase( @@ -80,10 +77,7 @@ class _TownSettingsPageState extends State { children: [ Text("settings.town.address".tr, style: Get.theme.textTheme.labelMedium), verticalSpacing(elementSpacing), - Text( - "settings.town.address.desc".trParams(), - style: Get.theme.textTheme.bodyMedium, - ), + Text("settings.town.address.desc".trParams(), style: Get.theme.textTheme.bodyMedium), ], ), ), @@ -92,11 +86,8 @@ class _TownSettingsPageState extends State { await Clipboard.setData(ClipboardData(text: StatusController.ownAddress.encode())); showSuccessPopup("success", "settings.town.address.copied".tr); }, - child: Text( - "copy".tr, - style: Get.textTheme.labelMedium, - ), - ) + child: Text("copy".tr, style: Get.textTheme.labelMedium), + ), ], ), ), diff --git a/lib/pages/spaces/call_page.dart b/lib/pages/spaces/call_page.dart deleted file mode 100644 index f23a2249..00000000 --- a/lib/pages/spaces/call_page.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:chat_interface/pages/spaces/space_rectangle.dart'; -import 'package:flutter/material.dart'; - -class CallPage extends StatelessWidget { - const CallPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Scaffold(body: SpaceRectangle()); - } -} diff --git a/lib/pages/spaces/entities/circle_member_entity.dart b/lib/pages/spaces/entities/circle_member_entity.dart deleted file mode 100644 index c8c82fe8..00000000 --- a/lib/pages/spaces/entities/circle_member_entity.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; -import 'package:chat_interface/theme/components/user_renderer.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -class CircleMemberEntity extends StatefulWidget { - final SpaceMember member; - - final double bottomPadding; - final double rightPadding; - - const CircleMemberEntity({super.key, required this.bottomPadding, required this.rightPadding, required this.member}); - - @override - State createState() => _MemberEntityState(); -} - -class _MemberEntityState extends State { - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(bottom: widget.bottomPadding, right: widget.rightPadding), - child: Stack( - children: [ - Obx( - () => Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: widget.member.isSpeaking.value ? Colors.green : Colors.transparent, - width: 4, - ), - ), - width: 100, - height: 100, - child: UserAvatar( - id: widget.member.friend.id, - size: 100, - ), - ), - ), - - //* Muted indicator - Obx( - () => Visibility( - visible: widget.member.isMuted.value, - child: Positioned( - right: 0, - bottom: 0, - child: Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.errorContainer, - borderRadius: BorderRadius.circular(200), - boxShadow: [ - BoxShadow( - color: Get.theme.colorScheme.primaryContainer, - blurRadius: 10, - ), - ], - ), - width: 30, - height: 30, - child: Center( - child: Icon( - Icons.mic_off, - color: Get.theme.colorScheme.error, - ), - ), - ), - ), - ), - ), - - //* Deafened indicator - Obx( - () => Visibility( - visible: widget.member.isDeafened.value, - child: Positioned( - right: 0, - bottom: 0, - child: Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.errorContainer, - borderRadius: BorderRadius.circular(200), - boxShadow: [ - if (!widget.member.isMuted.value) - BoxShadow( - color: Get.theme.colorScheme.primaryContainer, - blurRadius: 10, - ), - ], - ), - width: 30, - height: 30, - child: Center( - child: Icon( - Icons.volume_off, - color: Get.theme.colorScheme.error, - ), - ), - ), - ), - ), - ) - ], - ), - ); - } -} diff --git a/lib/pages/spaces/entities/entity_renderer.dart b/lib/pages/spaces/entities/entity_renderer.dart deleted file mode 100644 index 9b5b9afd..00000000 --- a/lib/pages/spaces/entities/entity_renderer.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; -import 'package:chat_interface/pages/spaces/entities/circle_member_entity.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -List renderEntites(double bottom, double right, BoxConstraints constraints, {int maxHero = 17, List? toRender, Axis? axis}) { - SpaceMemberController memberController = Get.find(); - toRender ??= memberController.members.keys.toList(); - - final entities = []; - - for (int i = 0; i < toRender.length; i++) { - //* Add member renderer - Widget memberRenderer = ConstrainedBox( - constraints: constraints, - child: CircleMemberEntity( - member: memberController.members[toRender[i]]!, - bottomPadding: bottom, - rightPadding: right, - ), - ); - - // Add to list - if (axis != null) { - final spacing = i == 0 ? 0.0 : defaultSpacing; - entities.add( - Padding( - padding: axis == Axis.vertical ? EdgeInsets.only(top: spacing) : EdgeInsets.only(left: spacing), - child: memberRenderer, - ), - ); - } else { - entities.add(memberRenderer); - } - } - - return entities; -} - -List renderCircleEntites(double bottom, double right, [List? toRender]) { - SpaceMemberController memberController = Get.find(); - toRender ??= memberController.members.keys.toList(); - final entities = []; - - for (int i = 0; i < toRender.length; i++) { - // Add to list - entities.add(CircleMemberEntity( - member: memberController.members[toRender[i]]!, - bottomPadding: bottom, - rightPadding: right, - )); - } - - return entities; -} diff --git a/lib/pages/spaces/space_rectangle.dart b/lib/pages/spaces/space_rectangle.dart index 82a74a12..e23ffd19 100644 --- a/lib/pages/spaces/space_rectangle.dart +++ b/lib/pages/spaces/space_rectangle.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/pages/spaces/tabletop/tabletop_page.dart'; import 'package:chat_interface/pages/spaces/widgets/space_controls.dart'; import 'package:chat_interface/pages/spaces/widgets/space_info_tab.dart'; @@ -11,6 +11,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SpaceRectangle extends StatefulWidget { const SpaceRectangle({super.key}); @@ -20,109 +21,96 @@ class SpaceRectangle extends StatefulWidget { } class _SpaceRectangleState extends State { - final controlsHovered = false.obs; - final hovered = true.obs; - Timer? timer; - final GlobalKey tabletopKey = GlobalKey(); + final _controlsHovered = signal(true); + final _hovered = signal(true); + Timer? _timer; // Space tabs - final _tabs = [ - const SpaceInfoTab(), - const TabletopView(), - ]; + final _tabs = [const SpaceInfoTab(), const TabletopView()]; // Sidebar tabs - final _sidebarTabs = [ - const SpacesMessageFeed(), - const SpaceMembersTab(), - ]; + final _sidebarTabs = [const SpacesMessageFeed(), const SpaceMembersTab()]; @override - Widget build(BuildContext context) { - return Hero( - tag: "call", - child: buildRectangle(Get.find()), - ); + void dispose() { + _controlsHovered.dispose(); + _hovered.dispose(); + _timer?.cancel(); + super.dispose(); } - Widget buildRectangle(SpacesController controller) { + @override + Widget build(BuildContext context) { return Container( color: Get.theme.colorScheme.inverseSurface, - child: LayoutBuilder(builder: (context, constraints) { - return MouseRegion( - onEnter: (event) { - hovered.value = true; - }, - onHover: (event) { - hovered.value = true; - if (timer != null) timer?.cancel(); - timer = Timer(1000.ms, () { - hovered.value = false; - }); - }, - onExit: (event) { - hovered.value = false; - timer?.cancel(); - }, - child: Row( - children: [ - Expanded( - child: Stack( - children: [ - Obx(() { - return _tabs[Get.find().currentTab.value]; - }), + child: LayoutBuilder( + builder: (context, constraints) { + return MouseRegion( + onEnter: (event) { + _hovered.value = true; + }, + onHover: (event) { + _hovered.value = true; + if (_timer != null) _timer?.cancel(); + _timer = Timer(1000.ms, () { + _hovered.value = false; + }); + }, + onExit: (event) { + _hovered.value = false; + _timer?.cancel(); + }, + child: Row( + children: [ + Expanded( + child: Stack( + children: [ + Watch((context) { + return _tabs[SpaceController.currentTab.value]; + }), - //* Tab - Align( - alignment: Alignment.topCenter, - child: Obx( - () => Animate( - effects: [ - FadeEffect( - duration: 150.ms, - end: 0.0, - begin: 1.0, - ) - ], - target: hovered.value || controlsHovered.value ? 0 : 1, - child: Container( - width: double.infinity, - // Create a gradient on this container from bottom to top - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black.withAlpha(0), - Colors.black.withAlpha(70), - ], + //* Tab + Align( + alignment: Alignment.topCenter, + child: Watch( + (context) => Animate( + effects: [FadeEffect(duration: 150.ms, end: 0.0, begin: 1.0)], + target: _hovered.value || _controlsHovered.value ? 0 : 1, + child: Container( + width: double.infinity, + // Create a gradient on this container from bottom to top + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black.withAlpha(0), Colors.black.withAlpha(70)], + ), ), - ), - child: MouseRegion( - onEnter: (event) => controlsHovered.value = true, - onExit: (event) => controlsHovered.value = false, - child: Center( - heightFactor: 1, - child: Padding( - padding: const EdgeInsets.only(top: sectionSpacing), - child: Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(defaultSpacing), - ), - padding: EdgeInsets.all(elementSpacing), - child: LPHTabElement( - tabs: SpaceTabType.values.map((e) => e.name.tr).toList(), - onTabSwitch: (el) { - final type = SpaceTabType.values.firstWhereOrNull((t) => t.name.tr == el); - if (type == null) { - return; - } - Get.find().switchToTabAndChange(type); - }, - selected: Get.find().currentTab, + child: MouseRegion( + onEnter: (event) => _controlsHovered.value = true, + onExit: (event) => _controlsHovered.value = false, + child: Center( + heightFactor: 1, + child: Padding( + padding: const EdgeInsets.only(top: sectionSpacing), + child: Container( + decoration: BoxDecoration( + color: Get.theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(defaultSpacing), + ), + padding: EdgeInsets.all(elementSpacing), + child: LPHTabElement( + tabs: SpaceTabType.values.map((e) => e.name.tr).toList(), + onTabSwitch: (el) { + final type = SpaceTabType.values.firstWhereOrNull((t) => t.name.tr == el); + if (type == null) { + return; + } + SpaceController.switchToTabAndChange(type); + }, + selected: SpaceController.currentTab, + ), ), ), ), @@ -131,98 +119,85 @@ class _SpaceRectangleState extends State { ), ), ), - ), - //* Controls - Align( - alignment: Alignment.bottomCenter, - child: Obx( - () => Animate( - effects: [ - FadeEffect( - duration: 150.ms, - end: 0.0, - begin: 1.0, - ) - ], - target: hovered.value || controlsHovered.value ? 0 : 1, - child: Container( - width: double.infinity, - // Create a gradient on this container from bottom to top - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black.withAlpha(70), - Colors.black.withAlpha(0), - ], + //* Controls + Align( + alignment: Alignment.bottomCenter, + child: Watch( + (context) => Animate( + effects: [FadeEffect(duration: 150.ms, end: 0.0, begin: 1.0)], + target: _hovered.value || _controlsHovered.value ? 0 : 1, + child: Container( + width: double.infinity, + // Create a gradient on this container from bottom to top + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black.withAlpha(70), Colors.black.withAlpha(0)], + ), ), - ), - child: MouseRegion( - onEnter: (event) => controlsHovered.value = true, - onExit: (event) => controlsHovered.value = false, - child: SpaceControls(), + child: MouseRegion( + onEnter: (event) => _controlsHovered.value = true, + onExit: (event) => _controlsHovered.value = false, + child: SpaceControls(), + ), ), ), ), ), - ), - ], + ], + ), ), - ), - // The chat sidebar - Obx( - () => Animate( - effects: [ - ExpandEffect( - curve: Curves.easeInOut, - duration: 250.ms, - axis: Axis.horizontal, - alignment: Alignment.centerRight, - ), - FadeEffect( - duration: 250.ms, - ) - ], - onInit: (ac) => ac.value = controller.chatOpen.value ? 1 : 0, - target: controller.chatOpen.value ? 1 : 0, - child: Container( - color: Get.theme.colorScheme.onInverseSurface, - width: 380, - child: Column( - children: [ - Container( - color: Get.theme.colorScheme.primaryContainer, - padding: const EdgeInsets.all(defaultSpacing), - child: Center( - child: LPHTabElement( - tabs: SpaceSidebarTabType.values.map((e) => e.name.tr).toList(), - onTabSwitch: (el) { - final type = SpaceSidebarTabType.values.firstWhereOrNull((t) => t.name.tr == el); - if (type == null) { - return; - } - controller.sidebarTabType.value = type.index; - }, - selected: controller.sidebarTabType, + // The Space sidebar + Watch( + (context) => Animate( + effects: [ + ExpandEffect( + curve: Curves.easeInOut, + duration: 250.ms, + axis: Axis.horizontal, + alignment: Alignment.centerLeft, + ), + FadeEffect(duration: 250.ms), + ], + onInit: (ac) => ac.value = SpaceController.chatOpen.value ? 1 : 0, + target: SpaceController.chatOpen.value ? 1 : 0, + child: Container( + color: Get.theme.colorScheme.onInverseSurface, + width: 380, + child: Column( + children: [ + Container( + color: Get.theme.colorScheme.primaryContainer, + padding: const EdgeInsets.all(defaultSpacing), + child: Center( + child: LPHTabElement( + tabs: SpaceSidebarTabType.values.map((e) => e.name.tr).toList(), + onTabSwitch: (el) { + final type = SpaceSidebarTabType.values.firstWhereOrNull((t) => t.name.tr == el); + if (type == null) { + return; + } + SpaceController.sidebarTabType.value = type.index; + }, + selected: SpaceController.sidebarTabType, + ), ), ), - ), - Expanded( - child: Obx(() => _sidebarTabs[controller.sidebarTabType.value]), - ), - ], + Expanded(child: Watch((context) => _sidebarTabs[SpaceController.sidebarTabType.value])), + ], + ), ), ), ), - ) - ], - ), - ); - }), + ], + ), + ); + }, + ), ); } } diff --git a/lib/pages/spaces/tabletop/object_context_menu.dart b/lib/pages/spaces/tabletop/object_context_menu.dart index dad246be..ef0b7ce6 100644 --- a/lib/pages/spaces/tabletop/object_context_menu.dart +++ b/lib/pages/spaces/tabletop/object_context_menu.dart @@ -1,8 +1,8 @@ -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_card.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_card.dart'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/profile_button.dart'; -import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -40,9 +40,8 @@ class _ObjectContextMenuState extends State { iconColor: addition.iconColor, color: addition.color, label: addition.label, - loading: false.obs, onTap: () { - addition.onTap.call(Get.find()); + addition.onTap.call(); if (addition.goBack) { Get.back(); } @@ -56,11 +55,8 @@ class _ObjectContextMenuState extends State { ProfileButton( icon: Icons.crop_rotate, label: "tabletop.match_viewport".tr, - loading: false.obs, onTap: () { - final controller = Get.find(); - sendLog(controller.canvasRotation.value); - widget.object.newRotation(-Get.find().canvasRotation.value); + widget.object.newRotation(-TabletopController.canvasRotation.value); Get.back(); }, ), @@ -69,7 +65,6 @@ class _ObjectContextMenuState extends State { ProfileButton( icon: Icons.delete, label: "remove".tr, - loading: false.obs, color: Get.theme.colorScheme.errorContainer, iconColor: Get.theme.colorScheme.error, onTap: () { diff --git a/lib/pages/spaces/tabletop/object_create_menu.dart b/lib/pages/spaces/tabletop/object_create_menu.dart index 2ab7b431..ccca20b2 100644 --- a/lib/pages/spaces/tabletop/object_create_menu.dart +++ b/lib/pages/spaces/tabletop/object_create_menu.dart @@ -1,7 +1,7 @@ -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_inventory.dart'; -import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_deck.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_text.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_inventory.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_deck.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_text.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; @@ -72,7 +72,7 @@ class _ObjectCreateMenuState extends State { ), ); }, - ) + ), ], ), ); diff --git a/lib/controller/spaces/tabletop/objects/tabletop_card.dart b/lib/pages/spaces/tabletop/objects/tabletop_card.dart similarity index 67% rename from lib/controller/spaces/tabletop/objects/tabletop_card.dart rename to lib/pages/spaces/tabletop/objects/tabletop_card.dart index c81dd1d7..755a397d 100644 --- a/lib/controller/spaces/tabletop/objects/tabletop_card.dart +++ b/lib/pages/spaces/tabletop/objects/tabletop_card.dart @@ -4,9 +4,10 @@ import 'dart:math' as math; import 'dart:ui' as ui; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_inventory.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_inventory.dart'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_deck.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_deck.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; import 'package:chat_interface/theme/ui/dialogs/image_preview_window.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; @@ -36,28 +37,25 @@ class CardObject extends TableObject { // Make size fit with canvas standards (900x900 in this case) final size = Size(container.width!.toDouble(), container.height!.toDouble()); final normalized = normalizeSize(size, cardNormalizer); - final obj = CardObject( - id, - 0, - location, - normalized, - ); + final obj = CardObject(id, 0, location, normalized); obj.container = container; obj.imageSize = size; // Download the file - unawaited(Get.find().downloadAttachment(container).then((success) async { - if (success) { - // Get the actual image and add it to the object - final buffer = await ui.ImmutableBuffer.fromUint8List(await container.file!.readAsBytes()); - final descriptor = await ui.ImageDescriptor.encoded(buffer); - final codec = await descriptor.instantiateCodec(); - obj.image = (await codec.getNextFrame()).image; - obj.downloaded = true; - } else { - obj.error = true; - } - })); + unawaited( + AttachmentController.downloadAttachment(container).then((success) async { + if (success) { + // Get the actual image and add it to the object + final buffer = await ui.ImmutableBuffer.fromUint8List(await container.file!.readAsBytes()); + final descriptor = await ui.ImageDescriptor.encoded(buffer); + final codec = await descriptor.instantiateCodec(); + obj.image = (await codec.getNextFrame()).image; + obj.downloaded = true; + } else { + obj.error = true; + } + }), + ); return obj; } @@ -78,36 +76,48 @@ class CardObject extends TableObject { } /// Renders the decorations for flipped cards - static void renderFlippedDecorations(Canvas canvas, Rect card, {bool ui = false}) { - final padding = ui ? sectionSpacing : sectionSpacing * 2; - final spacing = ui ? defaultSpacing + defaultSpacing / 2 : sectionSpacing * 2; - final size = ui ? 30.0 : 75.0; + static void renderFlippedDecorations(Canvas canvas, Rect card) { + final padding = sectionSpacing * 2; + final spacing = sectionSpacing * 2; + final size = 75.0; final cornerPaint = Paint()..color = Get.theme.colorScheme.onPrimary; canvas.drawRRect( - RRect.fromRectAndRadius(Rect.fromLTWH(card.left + padding, card.top + padding, size, size), Radius.circular(spacing)), + RRect.fromRectAndRadius( + Rect.fromLTWH(card.left + padding, card.top + padding, size, size), + Radius.circular(spacing), + ), cornerPaint, ); canvas.drawRRect( - RRect.fromRectAndRadius(Rect.fromLTWH(card.left + padding, card.bottom - size - padding, size, size), Radius.circular(spacing)), + RRect.fromRectAndRadius( + Rect.fromLTWH(card.left + padding, card.bottom - size - padding, size, size), + Radius.circular(spacing), + ), cornerPaint, ); canvas.drawRRect( - RRect.fromRectAndRadius(Rect.fromLTWH(card.right - size - padding, card.top + padding, size, size), Radius.circular(spacing)), + RRect.fromRectAndRadius( + Rect.fromLTWH(card.right - size - padding, card.top + padding, size, size), + Radius.circular(spacing), + ), cornerPaint, ); canvas.drawRRect( - RRect.fromRectAndRadius(Rect.fromLTWH(card.right - size - padding, card.bottom - size - padding, size, size), Radius.circular(spacing)), + RRect.fromRectAndRadius( + Rect.fromLTWH(card.right - size - padding, card.bottom - size - padding, size, size), + Radius.circular(spacing), + ), cornerPaint, ); } @override - void render(Canvas canvas, Offset location, TabletopController controller) { + void render(Canvas canvas, Offset location) { final imageRect = Rect.fromLTWH(location.dx, location.dy, size.width, size.height); - renderCard(canvas, location, controller, imageRect, false); + renderCard(canvas, location, imageRect); } - void renderCard(Canvas canvas, Offset location, TabletopController controller, Rect imageRect, bool ui) { + void renderCard(Canvas canvas, Offset location, Rect imageRect) { if (error) { final paint = Paint()..color = Colors.red; canvas.drawRect(Rect.fromLTWH(location.dx, location.dy, size.width, size.height), paint); @@ -119,7 +129,8 @@ class CardObject extends TableObject { final paint = Paint()..color = Colors.white; // Show that the card is about to be dropped - if (controller.heldObject == this && controller.hoveringObjects.any((element) => element is DeckObject)) { + if (TabletopController.heldObject == this && + TabletopController.hoveringObjects.any((element) => element is DeckObject)) { paint.color = Colors.white.withAlpha(120); } @@ -127,8 +138,8 @@ class CardObject extends TableObject { if ((lastPosition != location || lastPosition == null) && !inventory) { final center = location + Offset(size.width / 2, size.height / 2); bool found = false; - for (var object in controller.objects.values) { - if (object is InventoryObject && controller.inventory != object) { + for (var object in TabletopController.objects.values) { + if (object is InventoryObject && TabletopController.inventory != object) { if (object.getInventoryRect(invisRangeX: size.width / 2, invisRangeY: size.height / 2).contains(center)) { found = true; } @@ -147,11 +158,8 @@ class CardObject extends TableObject { } if (image == null) { - canvas.clipRRect(RRect.fromRectAndRadius(imageRect, Radius.circular(ui ? sectionSpacing : sectionSpacing * 2))); - canvas.drawRect( - imageRect, - Paint()..color = Colors.red, - ); + canvas.clipRRect(RRect.fromRectAndRadius(imageRect, Radius.circular(sectionSpacing * 2))); + canvas.drawRect(imageRect, Paint()..color = Colors.red); } else { // Rotation for the flip animation canvas.save(); @@ -160,26 +168,29 @@ class CardObject extends TableObject { canvas.translate(focalX, focalY); final currentFlip = flipAnimation.value(DateTime.now()); - final Matrix4 matrix = Matrix4.identity() - ..setEntry(3, 2, 0) // perspective - ..rotateY(math.pi * currentFlip); + final Matrix4 matrix = + Matrix4.identity() + ..setEntry(3, 2, 0) // perspective + ..rotateY(math.pi * currentFlip); canvas.transform(matrix.storage); canvas.translate(-focalX, -focalY); - canvas.clipRRect(RRect.fromRectAndRadius(imageRect, Radius.circular(ui ? sectionSpacing : sectionSpacing * 2))); + canvas.clipRRect(RRect.fromRectAndRadius(imageRect, Radius.circular(sectionSpacing * 2))); // Check if the animation says it's flipped or not if (currentFlip > 0.5) { - canvas.drawRect( - imageRect, - Paint()..color = Get.theme.colorScheme.primaryContainer, - ); - renderFlippedDecorations(canvas, imageRect, ui: ui); + canvas.drawRect(imageRect, Paint()..color = Get.theme.colorScheme.primaryContainer); + renderFlippedDecorations(canvas, imageRect); } else { canvas.drawImageRect( image!, - Rect.fromLTWH(0, 0, size.width * (imageSize!.width / size.width), size.height * (imageSize!.height / size.height)), + Rect.fromLTWH( + 0, + 0, + size.width * (imageSize!.width / size.width), + size.height * (imageSize!.height / size.height), + ), imageRect, paint, ); @@ -207,8 +218,8 @@ class CardObject extends TableObject { // Download the new image final type = await AttachmentController.checkLocations(json["i"], StorageType.cache); - container = Get.find().fromJson(type, jsonDecode(data)); - final download = await Get.find().downloadAttachment(container!); + container = AttachmentController.fromJson(type, jsonDecode(data)); + final download = await AttachmentController.downloadAttachment(container!); if (!download) { error = true; sendLog("failed to download card"); @@ -232,7 +243,7 @@ class CardObject extends TableObject { } @override - void runAction(TabletopController controller) { + void runAction() { if (inventory) { setFlipped(!flipped); } else { @@ -257,15 +268,15 @@ class CardObject extends TableObject { ContextMenuAction( icon: Icons.login, label: 'Put into inventory', - onTap: (controller) { - intoInventory(controller); + onTap: () { + intoInventory(); }, ), ContextMenuAction( icon: Icons.fullscreen, goBack: false, label: 'View in image viewer', - onTap: (controller) { + onTap: () { sendLog("viewing.."); Get.back(); Get.dialog(ImagePreviewWindow(image: image)); @@ -279,21 +290,21 @@ class CardObject extends TableObject { ContextMenuAction( icon: Icons.fullscreen, label: 'View in image viewer', - onTap: (controller) { + onTap: () { Get.dialog(ImagePreviewWindow(image: image)); }, ), ]; } - Future intoInventory(TabletopController controller, {int? index}) async { + Future intoInventory({int? index}) async { positionX.setRealValue(location.dx); positionY.setRealValue(location.dy); sendRemove(); if (index != null) { - controller.inventory!.add(this, index: index); + TabletopController.inventory!.add(this, index: index); } else { - (await controller.getOrCreateInventory())?.add(this); + (await TabletopController.getOrCreateInventory())?.add(this); } } } diff --git a/lib/controller/spaces/tabletop/objects/tabletop_deck.dart b/lib/pages/spaces/tabletop/objects/tabletop_deck.dart similarity index 81% rename from lib/controller/spaces/tabletop/objects/tabletop_deck.dart rename to lib/pages/spaces/tabletop/objects/tabletop_deck.dart index 5ea8d173..f3813e86 100644 --- a/lib/controller/spaces/tabletop/objects/tabletop_deck.dart +++ b/lib/pages/spaces/tabletop/objects/tabletop_deck.dart @@ -2,15 +2,17 @@ import 'dart:convert'; import 'dart:isolate'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_card.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_card.dart'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_decks.dart'; import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class DeckObject extends TableObject { /// Card ID -> Card data @@ -28,9 +30,9 @@ class DeckObject extends TableObject { factory DeckObject.createFromDeck(Offset location, TabletopDeck deck) { final obj = DeckObject("", 0, location, const Size(500, 500)); - for (var card in deck.cards) { + for (var card in deck.cards.peek()) { obj.cards[card.id] = card; - for (int i = 0; i < (deck.amounts[card.id] ?? 1); i++) { + for (int i = 0; i < (deck.amounts.peek()[card.id] ?? 1); i++) { obj.cardOrder.add(card.id); } } @@ -41,7 +43,7 @@ class DeckObject extends TableObject { } @override - void render(Canvas canvas, Offset location, TabletopController controller) { + void render(Canvas canvas, Offset location) { // Draw a stack final now = DateTime.now(); final currentWidth = width.value(now); @@ -53,10 +55,7 @@ class DeckObject extends TableObject { location.dy + currentHeight, const Radius.circular(sectionSpacing * 2), ); - canvas.drawRRect( - cardRect, - Paint()..color = Get.theme.colorScheme.primaryContainer, - ); + canvas.drawRRect(cardRect, Paint()..color = Get.theme.colorScheme.primaryContainer); // Draw the flipped icon on the card CardObject.renderFlippedDecorations(canvas, cardRect.outerRect); @@ -70,10 +69,7 @@ class DeckObject extends TableObject { location.dy + currentHeight / 2 + counterSize / 2, const Radius.circular(sectionSpacing * 2), ); - canvas.drawRRect( - rect, - Paint()..color = Get.theme.colorScheme.inverseSurface, - ); + canvas.drawRRect(rect, Paint()..color = Get.theme.colorScheme.inverseSurface); var textSpan = TextSpan( text: cardOrder.length.toString(), style: TextStyle( @@ -83,10 +79,7 @@ class DeckObject extends TableObject { fontWeight: FontWeight.bold, ), ); - final textPainter = TextPainter( - text: textSpan, - textDirection: TextDirection.ltr, - ); + final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr); textPainter.layout(); textPainter.paint( canvas, @@ -111,10 +104,9 @@ class DeckObject extends TableObject { // Go through all cards and unpack them (only works in main thread cause sodium) final cardMap = json["cards"] as Map; - final controller = Get.find(); for (var card in cardMap.values) { final type = await AttachmentController.checkLocations(card["i"], StorageType.cache); - cards[card["i"]] = controller.fromJson(type, card); + cards[card["i"]] = AttachmentController.fromJson(type, card); } // Set the width and height from the order @@ -126,7 +118,10 @@ class DeckObject extends TableObject { final top = cardOrder.firstOrNull; if (top != null) { final card = cards[top]!; - final newSize = CardObject.normalizeSize(Size(card.width!.toDouble(), card.height!.toDouble()), CardObject.cardNormalizer); + final newSize = CardObject.normalizeSize( + Size(card.width!.toDouble(), card.height!.toDouble()), + CardObject.cardNormalizer, + ); if (replace) { width.setRealValue(newSize.width); height.setRealValue(newSize.height); @@ -145,19 +140,16 @@ class DeckObject extends TableObject { map[card.id] = card.toJson(); } - return jsonEncode({ - "cards": map, - "order": cardOrder.toList(), - }); + return jsonEncode({"cards": map, "order": cardOrder.toList()}); } @override - void runAction(TabletopController controller) { - drawCardIntoInventory(controller); + void runAction() { + drawCardIntoInventory(); } /// Draw a card from the deck into the inventory - Future drawCardIntoInventory(TabletopController controller) async { + Future drawCardIntoInventory() async { if (cardOrder.isEmpty) { return; } @@ -172,14 +164,14 @@ class DeckObject extends TableObject { } // Download the card and do all the other magic required for this - final obj = await CardObject.downloadCard(container, controller.mousePos); + final obj = await CardObject.downloadCard(container, TabletopController.mousePos); setWidthAndHeight(); final result = await modifyData(); if (!result) return; if (obj == null) return; obj.positionX.setRealValue(location.dx); obj.positionY.setRealValue(location.dy); - (await controller.getOrCreateInventory())?.add(obj); + (await TabletopController.getOrCreateInventory())?.add(obj); }); } @@ -214,7 +206,7 @@ class DeckObject extends TableObject { ContextMenuAction( icon: Icons.shuffle, label: 'Shuffle', - onTap: (controller) { + onTap: () { shuffle(); Get.back(); }, @@ -232,11 +224,11 @@ class DeckObjectCreationWindow extends StatefulWidget { State createState() => _DeckSelectionWindowState(); } -class _DeckSelectionWindowState extends State { +class _DeckSelectionWindowState extends State with SignalsMixin { // Deck list - final _decks = [].obs; - final _loading = true.obs; - final _error = false.obs; + final _decks = listSignal([]); + final _loading = signal(true); + final _error = signal(false); @override void initState() { @@ -244,6 +236,14 @@ class _DeckSelectionWindowState extends State { super.initState(); } + @override + void dispose() { + _decks.dispose(); + _loading.dispose(); + _error.dispose(); + super.dispose(); + } + Future getDecksFromServer() async { final decks = await TabletopDecks.listDecks(); if (decks == null) { @@ -258,25 +258,17 @@ class _DeckSelectionWindowState extends State { @override Widget build(BuildContext context) { return DialogBase( - child: Obx(() { + child: Watch((context) { if (_loading.value) { - return CircularProgressIndicator( - color: Get.theme.colorScheme.onPrimary, - ); + return CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary); } if (_error.value) { - return ErrorContainer( - message: "settings.tabletop.decks.error".tr, - expand: true, - ); + return ErrorContainer(message: "settings.tabletop.decks.error".tr, expand: true); } if (_decks.isEmpty) { - return ErrorContainer( - message: "tabletop.object.deck.choose_empty".tr, - expand: true, - ); + return ErrorContainer(message: "tabletop.object.deck.choose_empty".tr, expand: true); } return Column( @@ -297,7 +289,7 @@ class _DeckSelectionWindowState extends State { borderRadius: BorderRadius.circular(defaultSpacing), child: InkWell( onTap: () { - if (deck.cards.any((card) => card.width == null || card.height == null)) { + if (deck.cards.peek().any((card) => card.width == null || card.height == null)) { showErrorPopup("error", "tabletop.object.deck.incompatible".tr); return; } @@ -315,9 +307,9 @@ class _DeckSelectionWindowState extends State { children: [ Text(deck.name, style: Get.theme.textTheme.labelLarge), verticalSpacing(elementSpacing), - Obx( - () => Text( - "decks.cards".trParams({"count": deck.cards.length.toString()}), + Watch( + (context) => Text( + "decks.cards".trParams({"count": deck.cards.value.length.toString()}), style: Get.theme.textTheme.bodyMedium, ), ), @@ -330,7 +322,7 @@ class _DeckSelectionWindowState extends State { ), ); }, - ) + ), ], ); }), diff --git a/lib/controller/spaces/tabletop/objects/tabletop_inventory.dart b/lib/pages/spaces/tabletop/objects/tabletop_inventory.dart similarity index 76% rename from lib/controller/spaces/tabletop/objects/tabletop_inventory.dart rename to lib/pages/spaces/tabletop/objects/tabletop_inventory.dart index b91b866d..cd3b388f 100644 --- a/lib/controller/spaces/tabletop/objects/tabletop_inventory.dart +++ b/lib/pages/spaces/tabletop/objects/tabletop_inventory.dart @@ -2,9 +2,10 @@ import 'dart:convert'; import 'dart:isolate'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_card.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_card.dart'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; import 'package:chat_interface/pages/spaces/tabletop/tabletop_painter.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_switch.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; @@ -39,12 +40,13 @@ class InventoryObject extends TableObject { int inventoryHoverIndex = -1; - InventoryObject(String id, int order, Offset location, Size size) : super(id, order, location, size, TableObjectType.inventory); + InventoryObject(String id, int order, Offset location, Size size) + : super(id, order, location, size, TableObjectType.inventory); @override - void render(Canvas canvas, Offset location, TabletopController controller) { + void render(Canvas canvas, Offset location) { final now = DateTime.now(); - final ownInventory = controller.inventory == this; + final ownInventory = TabletopController.inventory == this; final color = ownInventory ? Get.theme.colorScheme.onPrimary : Get.theme.colorScheme.onSurface; // Draw a placeholder for the profile picture @@ -63,13 +65,13 @@ class InventoryObject extends TableObject { } // Add extra width if the inventory is hovered - if (controller.heldObject != null && ownInventory) { + if (TabletopController.heldObject != null && ownInventory) { if (inventoryHoverIndex != -1) { - totalWidth += controller.heldObject!.size.width + spacing; + totalWidth += TabletopController.heldObject!.size.width + spacing; } if (_cards.isEmpty) { - biggestHeight = controller.heldObject!.size.height; + biggestHeight = TabletopController.heldObject!.size.height; } } @@ -80,14 +82,18 @@ class InventoryObject extends TableObject { // Draw the background final backRect = getInventoryRect(now: now, base: location); - final backPaint = Paint() - ..color = color - ..style = PaintingStyle.stroke - ..strokeWidth = strokeWidth; + final backPaint = + Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth; canvas.drawRRect(RRect.fromRectAndRadius(backRect, Radius.circular(32)), backPaint); // Check if the general inventory is hovered - final bool inventoryHovered = backRect.contains(controller.mousePos) && controller.heldObject != null && controller.heldObject != this; + final bool inventoryHovered = + backRect.contains(TabletopController.mousePos) && + TabletopController.heldObject != null && + TabletopController.heldObject != this; if (inventoryHovered) { inventoryHoverIndex = 0; } else { @@ -101,15 +107,17 @@ class InventoryObject extends TableObject { final calcY = location.dy - 32 - object.size.height; // If the inventory is hovered, check if hover index should be incremented - if (calcX + object.size.width <= controller.mousePos.dx && inventoryHovered) { + if (calcX + object.size.width <= TabletopController.mousePos.dx && inventoryHovered) { inventoryHoverIndex++; } else if (inventoryHovered) { - calcX += controller.heldObject!.size.width + spacing; + calcX += TabletopController.heldObject!.size.width + spacing; } // Draw the card and update positions object.positionOverwrite = true; - if (controller.hoveringObjects.contains(this) || location != this.location || object.positionX.lastValue == 0) { + if (TabletopController.hoveringObjects.contains(this) || + location != this.location || + object.positionX.lastValue == 0) { object.positionX.setRealValue(calcX); object.positionY.setRealValue(calcY); } else { @@ -119,20 +127,15 @@ class InventoryObject extends TableObject { final x = object.positionX.value(now); final y = object.positionY.value(now); - final rect = Rect.fromLTWH( - x, - y, - object.size.width, - object.size.height, - ); + final rect = Rect.fromLTWH(x, y, object.size.width, object.size.height); // Tell the controller about the hover state if (ownInventory) { - final hovered = rect.contains(controller.mousePos) && controller.heldObject == null; - if (hovered && !controller.hoveringObjects.contains(object)) { - controller.hoveringObjects.insert(0, object); - } else if (!hovered && controller.hoveringObjects.contains(object)) { - controller.hoveringObjects.remove(object); + final hovered = rect.contains(TabletopController.mousePos) && TabletopController.heldObject == null; + if (hovered && !TabletopController.hoveringObjects.contains(object)) { + TabletopController.hoveringObjects.insert(0, object); + } else if (!hovered && TabletopController.hoveringObjects.contains(object)) { + TabletopController.hoveringObjects.remove(object); } object.inventory = true; if (!hovered) { @@ -142,8 +145,8 @@ class InventoryObject extends TableObject { object.setFlipped(!ownInventory); final cardLocation = Offset(x, y); - TabletopPainter.preDraw(canvas, cardLocation, object, now); - object.renderCard(canvas, Offset(x, y), controller, rect, false); + TabletopPainter.preDraw(canvas, cardLocation, object, now, rotation: false); + object.renderCard(canvas, Offset(x, y), rect); TabletopPainter.postDraw(canvas); counterWidth -= rect.width + spacing; @@ -191,7 +194,6 @@ class InventoryObject extends TableObject { _cards.removeWhere((c) => !cardList.any((o) => o["i"] == c.container?.id && o["u"] == c.container?.url)); // Go through all cards and unpack them (only works in main thread cause sodium) - final controller = Get.find(); int index = -1; for (var card in cardList) { index++; @@ -202,14 +204,14 @@ class InventoryObject extends TableObject { } final type = await AttachmentController.checkLocations(card["i"], StorageType.cache); - final container = controller.fromJson(type, card); + final container = AttachmentController.fromJson(type, card); // Create a card object from it final obj = await CardObject.downloadCard(container, location); if (obj == null) { continue; } - obj.setFlipped(Get.find().inventory != this, animation: false); + obj.setFlipped(TabletopController.inventory != this, animation: false); // Insert the card at the index where it was not found _cards.insert(index, obj); @@ -228,24 +230,23 @@ class InventoryObject extends TableObject { @override List getContextMenuAdditions() { - final controller = Get.find(); return [ - if (controller.inventory == this) + if (TabletopController.inventory == this) ContextMenuAction( icon: Icons.logout, label: "Disown", - onTap: (controller) { - controller.inventory = null; + onTap: () { + TabletopController.inventory = null; }, ), - if (controller.inventory != this) + if (TabletopController.inventory != this) ContextMenuAction( icon: Icons.login, label: "Make own", - onTap: (controller) { - controller.inventory = this; + onTap: () { + TabletopController.inventory = this; }, - ) + ), ]; } } @@ -254,11 +255,7 @@ class InventoryObjectWindow extends StatefulWidget { final Offset location; final InventoryObject? object; - const InventoryObjectWindow({ - super.key, - required this.location, - this.object, - }); + const InventoryObjectWindow({super.key, required this.location, this.object}); @override State createState() => _InventoryObjectWindowState(); @@ -279,9 +276,7 @@ class _InventoryObjectWindowState extends State { children: [ Text("Show cards to other players", style: theme.textTheme.bodyMedium), const Spacer(), - FJSwitch( - value: false, - ), + FJSwitch(value: false), ], ), verticalSpacing(defaultSpacing), @@ -297,7 +292,7 @@ class _InventoryObjectWindowState extends State { child: Center( child: Text((widget.object != null ? "edit" : "create").tr, style: Get.theme.textTheme.labelLarge), ), - ) + ), ], ), ); diff --git a/lib/controller/spaces/tabletop/objects/tabletop_text.dart b/lib/pages/spaces/tabletop/objects/tabletop_text.dart similarity index 71% rename from lib/controller/spaces/tabletop/objects/tabletop_text.dart rename to lib/pages/spaces/tabletop/objects/tabletop_text.dart index a1abd578..033564ef 100644 --- a/lib/controller/spaces/tabletop/objects/tabletop_text.dart +++ b/lib/pages/spaces/tabletop/objects/tabletop_text.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_slider.dart'; import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; @@ -8,6 +9,7 @@ import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class TextObject extends TableObject { static const fontSizeMultiplier = 2; @@ -25,16 +27,10 @@ class TextObject extends TableObject { } @override - void render(Canvas canvas, Offset location, TabletopController controller) { + void render(Canvas canvas, Offset location) { final realFontSize = fontSize.value(DateTime.now()) * fontSizeMultiplier; - var textSpan = TextSpan( - text: text, - style: Get.theme.textTheme.labelLarge!.copyWith(fontSize: realFontSize), - ); - final textPainter = TextPainter( - text: textSpan, - textDirection: TextDirection.ltr, - ); + var textSpan = TextSpan(text: text, style: Get.theme.textTheme.labelLarge!.copyWith(fontSize: realFontSize)); + final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr); textPainter.layout(); final hintRect = Rect.fromLTWH( location.dx - realFontSize / 2 / 2, @@ -42,7 +38,10 @@ class TextObject extends TableObject { textPainter.size.width + realFontSize / 2, textPainter.size.height + realFontSize / 2 / 2, ); - canvas.drawRRect(RRect.fromRectAndRadius(hintRect, Radius.circular(realFontSize / 4)), Paint()..color = Get.theme.colorScheme.primaryContainer); + canvas.drawRRect( + RRect.fromRectAndRadius(hintRect, Radius.circular(realFontSize / 4)), + Paint()..color = Get.theme.colorScheme.primaryContainer, + ); textPainter.paint(canvas, location); } @@ -55,10 +54,7 @@ class TextObject extends TableObject { @override String getData() { - return jsonEncode({ - "text": text, - "size": fontSize.realValue, - }); + return jsonEncode({"text": text, "size": fontSize.realValue}); } void evaluateSize() { @@ -66,10 +62,7 @@ class TextObject extends TableObject { text: text, style: Get.theme.textTheme.labelLarge!.copyWith(fontSize: fontSize.realValue * fontSizeMultiplier), ); - final textPainter = TextPainter( - text: textSpan, - textDirection: TextDirection.ltr, - ); + final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr); textPainter.layout(); size = textPainter.size; } @@ -80,7 +73,7 @@ class TextObject extends TableObject { ContextMenuAction( icon: Icons.edit, label: 'Edit', - onTap: (controller) { + onTap: () { Get.back(); Get.dialog(TextObjectCreationWindow(location: location, object: this)); Get.dialog(TextObjectCreationWindow(location: location, object: this)); @@ -94,26 +87,25 @@ class TextObjectCreationWindow extends StatefulWidget { final Offset location; final TextObject? object; - const TextObjectCreationWindow({ - super.key, - required this.location, - this.object, - }); + const TextObjectCreationWindow({super.key, required this.location, this.object}); @override State createState() => _TextObjectCreationWindowState(); } -class _TextObjectCreationWindowState extends State { - final fontSize = 16.0.obs; +class _TextObjectCreationWindowState extends State with SignalsMixin { + // Controller for the text input to power the preview final _textController = TextEditingController(); - final _text = "".obs; + + // State + late final _fontSize = createSignal(16.0); + late final _text = createSignal(""); @override void initState() { _textController.text = widget.object?.text ?? ""; _text.value = widget.object?.text ?? ""; - fontSize.value = widget.object?.fontSize.realValue ?? 16; + _fontSize.value = widget.object?.fontSize.realValue ?? 16; super.initState(); } @@ -132,17 +124,15 @@ class _TextObjectCreationWindowState extends State { onChange: (value) => _text.value = value, ), verticalSpacing(defaultSpacing), - Obx( - () => FJSliderWithInput( - min: 16, - max: 48, - value: fontSize.value, - label: fontSize.value.toStringAsFixed(0), - onChanged: (value) => fontSize.value = value, - ), + FJSliderWithInput( + min: 16, + max: 48, + value: _fontSize.value, + label: _fontSize.value.toStringAsFixed(0), + onChanged: (value) => _fontSize.value = value, ), verticalSpacing(defaultSpacing), - Obx(() => Text(_text.value, style: Get.theme.textTheme.labelLarge!.copyWith(fontSize: fontSize.value))), + Text(_text.value, style: Get.theme.textTheme.labelLarge!.copyWith(fontSize: _fontSize.value)), verticalSpacing(defaultSpacing), FJElevatedButton( onTap: () { @@ -150,19 +140,23 @@ class _TextObjectCreationWindowState extends State { if (widget.object != null) { widget.object!.queue(() { widget.object!.text = _textController.text; - widget.object!.fontSize.setValue(fontSize.value); + widget.object!.fontSize.setValue(_fontSize.value); widget.object!.evaluateSize(); widget.object!.modifyData(); }); return; } - final object = TextObject.createFromText(widget.location, _textController.text, fontSize.value.roundToDouble()); + final object = TextObject.createFromText( + widget.location, + _textController.text, + _fontSize.value.roundToDouble(), + ); object.sendAdd(); }, child: Center( child: Text((widget.object != null ? "edit" : "create").tr, style: Get.theme.textTheme.labelLarge), ), - ) + ), ], ), ); diff --git a/lib/pages/spaces/tabletop/tabletop_page.dart b/lib/pages/spaces/tabletop/tabletop_page.dart index 096dbcd9..e52dc8ef 100644 --- a/lib/pages/spaces/tabletop/tabletop_page.dart +++ b/lib/pages/spaces/tabletop/tabletop_page.dart @@ -1,9 +1,8 @@ import 'dart:async'; -import 'dart:ui'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_card.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_card.dart'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; -import 'package:chat_interface/controller/spaces/tabletop/objects/tabletop_deck.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_deck.dart'; import 'package:chat_interface/pages/settings/town/tabletop_settings.dart'; import 'package:chat_interface/pages/settings/data/entities.dart'; import 'package:chat_interface/pages/settings/data/settings_controller.dart'; @@ -17,15 +16,17 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'dart:math' as math; +import 'package:signals/signals_flutter.dart'; + class TabletopView extends StatefulWidget { const TabletopView({super.key}); @override State createState() => _TabletopViewState(); - static Offset localToWorldPos(Offset local, double scale, Offset movement, TabletopController controller) { + static Offset localToWorldPos(Offset local, double scale, Offset movement) { final angle = math.atan(local.dy / local.dx); - final mouseAngle = angle - controller.canvasRotation.value; + final mouseAngle = angle - TabletopController.canvasRotation.value; // Find position in circle final radius = local.distance; @@ -34,7 +35,7 @@ class TabletopView extends StatefulWidget { return Offset(x, y) / scale - movement; } - static Offset worldToLocalPos(Offset world, double scale, Offset movement, TabletopController controller) { + static Offset worldToLocalPos(Offset world, double scale, Offset movement) { // Undo the movement Offset withoutMovement = world + movement; @@ -45,7 +46,7 @@ class TabletopView extends StatefulWidget { final dx = unscaled.dx; final dy = unscaled.dy; final radius = math.sqrt(dx * dx + dy * dy); - final angle = math.atan2(dy, dx) + controller.canvasRotation.value; + final angle = math.atan2(dy, dx) + TabletopController.canvasRotation.value; // Convert back to local coordinates final x = radius * math.cos(angle); @@ -56,172 +57,213 @@ class TabletopView extends StatefulWidget { } class _TabletopViewState extends State with SingleTickerProviderStateMixin { - bool moved = false; + bool _moved = false; final GlobalKey _key = GlobalKey(); - final updater = false.obs; - Timer? timer; + final _updated = signal(false); + Timer? _timer; + Function()? _disposeSettingSub; + + @override + void dispose() { + _updated.dispose(); + _disposeSettingSub?.call(); + _timer?.cancel(); + super.dispose(); + } @override void initState() { super.initState(); - final setting = Get.find().settings[TabletopSettings.framerate]! as Setting; - setting.value.listenAndPump((value) => startFrameTimer(value!)); + final setting = SettingController.settings[TabletopSettings.framerate]! as Setting; + _disposeSettingSub = setting.value.subscribe((value) => startFrameTimer(value!)); + startFrameTimer(setting.value.value!); } void startFrameTimer(double value) { - if (timer != null) { - timer!.cancel(); + if (_timer != null) { + _timer!.cancel(); } - timer = Timer.periodic((1000 / value).ms, (timer) { - updater.value = !updater.value; + _timer = Timer.periodic((1000 / value).ms, (timer) { + _updated.value = !_updated.value; }); } @override Widget build(BuildContext context) { - final tableController = Get.find(); - // Add post frame callback to tell the controller the size of the painter WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final renderObj = _key.currentContext!.findRenderObject() as RenderBox; final widgetPosition = renderObj.localToGlobal(Offset.zero); - tableController.globalCanvasPosition = widgetPosition; + TabletopController.globalCanvasPosition = widgetPosition; }); return Scaffold( body: RepaintBoundary( - child: Obx( - () { - updater.value; - return Listener( - onPointerHover: (event) { - tableController.mousePosUnmodified = event.localPosition; - tableController.mousePos = - TabletopView.localToWorldPos(event.localPosition, tableController.canvasZoom, tableController.canvasOffset, tableController); - }, - onPointerDown: (event) { - if (event.buttons == 2) { - if (tableController.hoveringObjects.isNotEmpty) { - Get.dialog(ObjectContextMenu( + child: Watch((context) { + _updated.value; + return Listener( + onPointerHover: (event) { + TabletopController.mousePosUnmodified = event.localPosition; + TabletopController.mousePos = TabletopView.localToWorldPos( + event.localPosition, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); + }, + onPointerDown: (event) { + if (event.buttons == 2) { + if (TabletopController.hoveringObjects.isNotEmpty) { + Get.dialog( + ObjectContextMenu( data: ContextMenuData.fromPosition(Offset(event.position.dx, event.position.dy)), - object: tableController.hoveringObjects.first, - )); - moved = true; - return; - } - - Get.dialog(ObjectCreateMenu( - location: TabletopView.localToWorldPos( - event.localPosition, tableController.canvasZoom, tableController.canvasOffset, tableController))); - } else if (event.buttons == 1) { - moved = false; - } - }, - - //* Handle the mouse movements - onPointerMove: (event) { - // Calculate the new position of the mouse - final added = event.localPosition + event.delta; - - // Make sure the mouse isn't anywhere out of bounds - if (added.dx <= 0 || added.dy <= 0 || event.localPosition.dx <= 0 || event.localPosition.dy <= 0) return; - - // Move the canvas when the mouse wheel is pressed - if (event.buttons == 4) { - final old = - TabletopView.localToWorldPos(event.localPosition, tableController.canvasZoom, tableController.canvasOffset, tableController); - final newPos = TabletopView.localToWorldPos( - event.localPosition + event.delta, tableController.canvasZoom, tableController.canvasOffset, tableController); - tableController.canvasOffset += newPos - old; + object: TabletopController.hoveringObjects.first, + ), + ); + _moved = true; + return; } - // Move the currently held object when the mouse is clicked - if (event.buttons == 1) { - if (tableController.hoveringObjects.isNotEmpty && !tableController.cancelledHolding) { - // If there is a held object, move it, if not, add a new held object from the hovering objects list - if (tableController.heldObject != null) { - // Move the object - final old = TabletopView.localToWorldPos( - event.localPosition, tableController.canvasZoom, tableController.canvasOffset, tableController); - final newPos = TabletopView.localToWorldPos( - event.localPosition + event.delta, tableController.canvasZoom, tableController.canvasOffset, tableController); - tableController.heldObject!.location += newPos - old; - } else { - moved = true; - - // Start holding the object - tableController.startHoldingObject(tableController.hoveringObjects.last); - } - } - } + Get.dialog( + ObjectCreateMenu( + location: TabletopView.localToWorldPos( + event.localPosition, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ), + ), + ); + } else if (event.buttons == 1) { + _moved = false; + } + }, - // Update the mouse position in the controller - tableController.mousePosUnmodified = event.localPosition; - tableController.mousePos = - TabletopView.localToWorldPos(event.localPosition, tableController.canvasZoom, tableController.canvasOffset, tableController); - }, - - //* Handle when a mouse button is no longer pressed - onPointerUp: (event) { - tableController.cancelledHolding = false; - if (tableController.hoveringObjects.isNotEmpty && !moved && tableController.heldObject == null && event.buttons == 0) { - tableController.hoveringObjects.first.runAction(tableController); - return; - } + //* Handle the mouse movements + onPointerMove: (event) { + // Calculate the new position of the mouse + final added = event.localPosition + event.delta; + + // Make sure the mouse isn't anywhere out of bounds + if (added.dx <= 0 || added.dy <= 0 || event.localPosition.dx <= 0 || event.localPosition.dy <= 0) return; - final obj = tableController.heldObject; - if (obj != null && obj is CardObject) { - if (tableController.inventory != null && tableController.inventory?.inventoryHoverIndex != -1) { - obj.intoInventory(tableController, index: tableController.inventory?.inventoryHoverIndex); - } else if (tableController.hoveringObjects.any((element) => element is DeckObject)) { - final deck = tableController.hoveringObjects.firstWhere((element) => element is DeckObject) as DeckObject; - deck.addCard(obj); + // Move the canvas when the mouse wheel is pressed + if (event.buttons == 4) { + final old = TabletopView.localToWorldPos( + event.localPosition, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); + final newPos = TabletopView.localToWorldPos( + event.localPosition + event.delta, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); + TabletopController.canvasOffset += newPos - old; + } + + // Move the currently held object when the mouse is clicked + if (event.buttons == 1) { + if (TabletopController.hoveringObjects.isNotEmpty && !TabletopController.cancelledHolding) { + // If there is a held object, move it, if not, add a new held object from the hovering objects list + if (TabletopController.heldObject != null) { + // Move the object + final old = TabletopView.localToWorldPos( + event.localPosition, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); + final newPos = TabletopView.localToWorldPos( + event.localPosition + event.delta, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); + TabletopController.heldObject!.location += newPos - old; + } else { + _moved = true; + + // Start holding the object + TabletopController.startHoldingObject(TabletopController.hoveringObjects.last); } } + } - // Stop the selection - tableController.stopHoldingObject(error: tableController.cancelledHolding); - }, - onPointerSignal: (event) { - if (event is PointerScrollEvent) { - final scrollDelta = event.scrollDelta.dy / 500 * -1; - if (tableController.canvasZoom + scrollDelta < 0.1) { - return; - } - if (tableController.canvasZoom + scrollDelta > 5) return; + // Update the mouse position in the controller + TabletopController.mousePosUnmodified = event.localPosition; + TabletopController.mousePos = TabletopView.localToWorldPos( + event.localPosition, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); + }, - final zoomFactor = (tableController.canvasZoom + scrollDelta) / tableController.canvasZoom; - final focalPoint = - TabletopView.localToWorldPos(event.localPosition, tableController.canvasZoom, tableController.canvasOffset, tableController); - final newFocalPoint = TabletopView.localToWorldPos( - event.localPosition, tableController.canvasZoom + scrollDelta, tableController.canvasOffset, tableController); + //* Handle when a mouse button is no longer pressed + onPointerUp: (event) { + TabletopController.cancelledHolding = false; + if (TabletopController.hoveringObjects.isNotEmpty && + !_moved && + TabletopController.heldObject == null && + event.buttons == 0) { + TabletopController.hoveringObjects.first.runAction(); + return; + } - tableController.canvasOffset -= focalPoint - newFocalPoint; - tableController.canvasZoom *= zoomFactor; - tableController.mousePosUnmodified = event.localPosition; + final obj = TabletopController.heldObject; + if (obj != null && obj is CardObject) { + if (TabletopController.inventory != null && TabletopController.inventory?.inventoryHoverIndex != -1) { + obj.intoInventory(index: TabletopController.inventory?.inventoryHoverIndex); + } else if (TabletopController.hoveringObjects.any((element) => element is DeckObject)) { + final deck = + TabletopController.hoveringObjects.firstWhere((element) => element is DeckObject) as DeckObject; + deck.addCard(obj); } - }, - child: SizedBox.expand( - child: ClipRRect( - child: CustomPaint( - key: _key, - willChange: true, - isComplex: true, - painter: TabletopPainter( - controller: tableController, - mousePosition: tableController.mousePos, - mousePositionUnmodified: tableController.mousePosUnmodified, - offset: tableController.canvasOffset, - scale: tableController.canvasZoom, - rotation: tableController.canvasRotation.value, - ), + } + + // Stop the selection + TabletopController.stopHoldingObject(error: TabletopController.cancelledHolding); + }, + onPointerSignal: (event) { + if (event is PointerScrollEvent) { + final scrollDelta = event.scrollDelta.dy / 500 * -1; + if (TabletopController.canvasZoom + scrollDelta < 0.1) { + return; + } + if (TabletopController.canvasZoom + scrollDelta > 5) return; + + final zoomFactor = (TabletopController.canvasZoom + scrollDelta) / TabletopController.canvasZoom; + final focalPoint = TabletopView.localToWorldPos( + event.localPosition, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); + final newFocalPoint = TabletopView.localToWorldPos( + event.localPosition, + TabletopController.canvasZoom + scrollDelta, + TabletopController.canvasOffset, + ); + + TabletopController.canvasOffset -= focalPoint - newFocalPoint; + TabletopController.canvasZoom *= zoomFactor; + TabletopController.mousePosUnmodified = event.localPosition; + } + }, + child: SizedBox.expand( + child: ClipRRect( + child: CustomPaint( + key: _key, + willChange: true, + isComplex: true, + painter: TabletopPainter( + mousePosition: TabletopController.mousePos, + mousePositionUnmodified: TabletopController.mousePosUnmodified, + offset: TabletopController.canvasOffset, + scale: TabletopController.canvasZoom, + rotation: TabletopController.canvasRotation.value, ), ), ), - ); - }, - ), + ), + ); + }), ), ); } diff --git a/lib/pages/spaces/tabletop/tabletop_painter.dart b/lib/pages/spaces/tabletop/tabletop_painter.dart index 8588a442..d3027a17 100644 --- a/lib/pages/spaces/tabletop/tabletop_painter.dart +++ b/lib/pages/spaces/tabletop/tabletop_painter.dart @@ -1,9 +1,9 @@ import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; class TabletopPainter extends CustomPainter { - final TabletopController controller; final Offset offset; final Offset mousePosition; final Offset mousePositionUnmodified; @@ -11,7 +11,6 @@ class TabletopPainter extends CustomPainter { final double rotation; TabletopPainter({ - required this.controller, required this.offset, required this.mousePosition, required this.mousePositionUnmodified, @@ -112,28 +111,28 @@ class TabletopPainter extends CustomPainter { */ final now = DateTime.now(); - controller.hoveringObjects = controller.raycast(mousePosition); - for (var i in controller.orderSorted) { + TabletopController.hoveringObjects = TabletopController.raycast(mousePosition); + for (var i in TabletopController.orderSorted) { // Get the object at the current drawing layer - final objectId = controller.objectOrder[i]; + final objectId = TabletopController.objectOrder[i]; if (objectId == null) { continue; } // Render the object - final object = controller.objects[objectId]!; - if (controller.hoveringObjects.contains(object)) { + final object = TabletopController.objects[objectId]!; + if (TabletopController.hoveringObjects.contains(object)) { object.hoverRotation(-rotation); } else { object.scale.setValue(1.0); object.unhoverRotation(); } - final location = controller.heldObject == object ? object.location : object.interpolatedLocation(now); + final location = TabletopController.heldObject == object ? object.location : object.interpolatedLocation(now); drawObject(canvas, location, object, now); } // Render cursors - for (var cursor in controller.cursors.values) { + for (var cursor in TabletopController.cursors.value.values) { cursor.render(canvas); } @@ -146,13 +145,15 @@ class TabletopPainter extends CustomPainter { } /// Apply the nessecary scaling and rotation for object drawing (called before object rendering) - static void preDraw(Canvas canvas, Offset location, TableObject object, DateTime now) { + static void preDraw(Canvas canvas, Offset location, TableObject object, DateTime now, {bool rotation = true}) { final scale = object.scale.value(now); canvas.save(); final focalX = location.dx + object.size.width / 2; final focalY = location.dy + object.size.height / 2; canvas.translate(focalX, focalY); - canvas.rotate(object.rotation.value(now)); + if (rotation) { + canvas.rotate(object.rotation.value(now)); + } canvas.scale(scale); canvas.translate(-focalX, -focalY); } @@ -165,7 +166,7 @@ class TabletopPainter extends CustomPainter { /// Draw an object to the table void drawObject(Canvas canvas, Offset location, TableObject object, DateTime now) { preDraw(canvas, location, object, now); - object.render(canvas, location, controller); + object.render(canvas, location); postDraw(canvas); } } diff --git a/lib/pages/spaces/tabletop/tabletop_rotate_window.dart b/lib/pages/spaces/tabletop/tabletop_rotate_window.dart index 6e918e66..8f8f52f0 100644 --- a/lib/pages/spaces/tabletop/tabletop_rotate_window.dart +++ b/lib/pages/spaces/tabletop/tabletop_rotate_window.dart @@ -7,6 +7,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:math' as math; +import 'package:signals/signals_flutter.dart'; + class TabletopRotateWindow extends StatefulWidget { final ContextMenuData data; @@ -19,8 +21,6 @@ class TabletopRotateWindow extends StatefulWidget { class _TabletopRotateWindowState extends State { @override Widget build(BuildContext context) { - final tableController = Get.find(); - return SlidingWindowBase( title: const [], // Only for mobile (sort of) position: widget.data, @@ -29,11 +29,11 @@ class _TabletopRotateWindowState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Rotation", style: Get.theme.textTheme.labelLarge), - Obx( - () => FJSliderWithInput( - value: tableController.canvasRotation.value, + Watch( + (context) => FJSliderWithInput( + value: TabletopController.canvasRotation.value, onChanged: (value) { - rotateTable(value, tableController); + rotateTable(value); }, min: 0, max: 2 * math.pi, @@ -42,42 +42,51 @@ class _TabletopRotateWindowState extends State { ), ), Row( - children: List.generate(3, (index) { - return Padding( - padding: const EdgeInsets.only(right: defaultSpacing), - child: Material( - borderRadius: BorderRadius.circular(defaultSpacing), - color: Get.theme.colorScheme.inverseSurface, - child: InkWell( - onTap: () => rotateTable(math.pi / 2 * (index + 1), tableController), + children: List.generate(3, (index) { + return Padding( + padding: const EdgeInsets.only(right: defaultSpacing), + child: Material( borderRadius: BorderRadius.circular(defaultSpacing), - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Row( - children: [ - Icon(Icons.rotate_left, color: Get.theme.colorScheme.onPrimary), - horizontalSpacing(elementSpacing), - Text("${90 * (index + 1)}", style: Get.theme.textTheme.labelMedium), - ], + color: Get.theme.colorScheme.inverseSurface, + child: InkWell( + onTap: () => rotateTable(math.pi / 2 * (index + 1)), + borderRadius: BorderRadius.circular(defaultSpacing), + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + Icon(Icons.rotate_left, color: Get.theme.colorScheme.onPrimary), + horizontalSpacing(elementSpacing), + Text("${90 * (index + 1)}", style: Get.theme.textTheme.labelMedium), + ], + ), ), ), ), - ), - ); - })) + ); + }), + ), ], ), ); } - void rotateTable(double value, TabletopController tableController) { - final canvasWidth = Get.width - tableController.globalCanvasPosition.dx; - final canvasHeight = Get.height - tableController.globalCanvasPosition.dy; + void rotateTable(double value) { + final canvasWidth = Get.width - TabletopController.globalCanvasPosition.dx; + final canvasHeight = Get.height - TabletopController.globalCanvasPosition.dy; final center = Offset(canvasWidth / 2, canvasHeight / 2); - final focalPoint = TabletopView.localToWorldPos(center, tableController.canvasZoom, tableController.canvasOffset, tableController); - tableController.canvasRotation.value = value; - final newFocalPoint = TabletopView.localToWorldPos(center, tableController.canvasZoom, tableController.canvasOffset, tableController); + final focalPoint = TabletopView.localToWorldPos( + center, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); + TabletopController.canvasRotation.value = value; + final newFocalPoint = TabletopView.localToWorldPos( + center, + TabletopController.canvasZoom, + TabletopController.canvasOffset, + ); - tableController.canvasOffset -= focalPoint - newFocalPoint; + TabletopController.canvasOffset -= focalPoint - newFocalPoint; } } diff --git a/lib/pages/spaces/warp/warp_connected_list.dart b/lib/pages/spaces/warp/warp_connected_list.dart index 55ed8fdc..8e6bb823 100644 --- a/lib/pages/spaces/warp/warp_connected_list.dart +++ b/lib/pages/spaces/warp/warp_connected_list.dart @@ -4,19 +4,20 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class WarpConnectedList extends StatelessWidget { const WarpConnectedList({super.key}); @override Widget build(BuildContext context) { - final controller = Get.find(); - return Obx(() { - if (controller.activeWarps.isEmpty) { + return Watch((context) { + final activeWarps = WarpController.activeWarps.value; + if (activeWarps.isEmpty) { return SizedBox(); } - final values = controller.activeWarps.values.toList(); + final values = WarpController.activeWarps.value.values.toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -53,7 +54,7 @@ class WarpConnectedList extends StatelessWidget { ), const Spacer(), LoadingIconButton( - onTap: () => controller.disconnectWarp(warp), + onTap: () => WarpController.disconnectWarp(warp), extra: 5, icon: Icons.logout, ), diff --git a/lib/pages/spaces/warp/warp_create_window.dart b/lib/pages/spaces/warp/warp_create_window.dart index eedd63a9..31e84fa5 100644 --- a/lib/pages/spaces/warp/warp_create_window.dart +++ b/lib/pages/spaces/warp/warp_create_window.dart @@ -9,6 +9,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class WarpCreateWindow extends StatefulWidget { const WarpCreateWindow({super.key}); @@ -18,53 +19,47 @@ class WarpCreateWindow extends StatefulWidget { } class _WarpCreateWindowState extends State { + // Controller for the port input final TextEditingController _port = TextEditingController(); - final _error = "".obs; - final _loading = false.obs; + + // State + final _error = signal(""); + final _loading = signal(false); @override void dispose() { _port.dispose(); + _error.dispose(); + _loading.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text("warp.create.title".tr, style: Get.textTheme.labelLarge), - ], + title: [Text("warp.create.title".tr, style: Get.textTheme.labelLarge)], child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - "warp.create.desc".tr, - style: Get.theme.textTheme.bodyMedium, - textAlign: TextAlign.start, - ), + Text("warp.create.desc".tr, style: Get.theme.textTheme.bodyMedium, textAlign: TextAlign.start), verticalSpacing(defaultSpacing), FJTextField( maxLength: 5, controller: _port, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], + inputFormatters: [FilteringTextInputFormatter.digitsOnly], hintText: 'warp.port.placeholder'.tr, ), verticalSpacing(defaultSpacing), - AnimatedErrorContainer( - padding: const EdgeInsets.only(bottom: defaultSpacing), - message: _error, - ), + AnimatedErrorContainer(padding: const EdgeInsets.only(bottom: defaultSpacing), message: _error), FJElevatedLoadingButton( onTap: () async { _loading.value = true; _error.value = ""; // Convert the port to an actual port number - final port = int.parse(_port.text); - if (port < 1024 || port > 65535) { + final port = int.tryParse(_port.text); + if (port == null || port < 1024 || port > 65535) { _loading.value = false; _error.value = "warp.error.port_invalid".tr; return; @@ -80,7 +75,7 @@ class _WarpCreateWindowState extends State { } // Create the warp on the server - final error = await Get.find().createWarp(port); + final error = await WarpController.createWarp(port); _loading.value = false; _error.value = error ?? ""; if (error == null) { @@ -89,7 +84,7 @@ class _WarpCreateWindowState extends State { }, label: 'warp.create.button'.tr, loading: _loading, - ) + ), ], ), ); diff --git a/lib/pages/spaces/warp/warp_list.dart b/lib/pages/spaces/warp/warp_list.dart index 6fba49f6..0d3c3b4b 100644 --- a/lib/pages/spaces/warp/warp_list.dart +++ b/lib/pages/spaces/warp/warp_list.dart @@ -5,30 +5,27 @@ import 'package:chat_interface/theme/components/user_renderer.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class WarpList extends StatelessWidget { const WarpList({super.key}); @override Widget build(BuildContext context) { - final controller = Get.find(); - return Obx(() { - if (controller.warps.isEmpty) { + return Watch((context) { + if (WarpController.warps.isEmpty) { return Padding( - padding: const EdgeInsets.only( - top: sectionSpacing, - bottom: defaultSpacing, - ), + padding: const EdgeInsets.only(top: sectionSpacing, bottom: defaultSpacing), child: Text("warp.list.empty".tr, style: Get.textTheme.labelMedium), ); } return ListView.builder( shrinkWrap: true, - itemCount: controller.warps.length, + itemCount: WarpController.warps.length, itemBuilder: (context, index) { // Get the current warp - final warp = controller.warps[index]; + final warp = WarpController.warps.value[index]; // Render the warp port itself final warpRenderer = Padding( @@ -42,7 +39,7 @@ class WarpList extends StatelessWidget { if (warp.account.id == StatusController.ownAddress) { return; } - controller.connectToWarp(warp); + WarpController.connectToWarp(warp); }, child: Padding( padding: EdgeInsets.all(defaultSpacing), @@ -50,16 +47,13 @@ class WarpList extends StatelessWidget { children: [ Icon(Icons.cyclone, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(defaultSpacing), - Text( - warp.port.toString(), - style: Get.textTheme.labelMedium, - ), + Text(warp.port.toString(), style: Get.textTheme.labelMedium), const Spacer(), Visibility( visible: warp.account.id != StatusController.ownAddress, child: LoadingIconButton( loading: warp.loading, - onTap: () => controller.connectToWarp(warp), + onTap: () => WarpController.connectToWarp(warp), extra: 5, icon: Icons.add, ), @@ -72,7 +66,7 @@ class WarpList extends StatelessWidget { ); // Check if the account should be rendered - if (index > 0 && controller.warps[index - 1].account.id == warp.account.id) { + if (index > 0 && WarpController.warps.value[index - 1].account.id == warp.account.id) { return warpRenderer; } @@ -85,11 +79,9 @@ class WarpList extends StatelessWidget { children: [ UserAvatar(id: warp.account.id, size: 35), horizontalSpacing(defaultSpacing), - Obx( - () => Text( - "warp.list.sharing".trParams({ - "name": warp.account.displayName.value, - }), + Watch( + (context) => Text( + "warp.list.sharing".trParams({"name": warp.account.displayName.value}), style: Get.theme.textTheme.labelMedium, ), ), diff --git a/lib/pages/spaces/warp/warp_manager_window.dart b/lib/pages/spaces/warp/warp_manager_window.dart index d37eeaaf..56bc5265 100644 --- a/lib/pages/spaces/warp/warp_manager_window.dart +++ b/lib/pages/spaces/warp/warp_manager_window.dart @@ -20,13 +20,7 @@ class _WarpManagerWindowState extends State { Widget build(BuildContext context) { return DialogBase( title: [ - Expanded( - child: Text( - "warp.title".tr, - style: Get.theme.textTheme.labelLarge, - overflow: TextOverflow.ellipsis, - ), - ), + Expanded(child: Text("warp.title".tr, style: Get.theme.textTheme.labelLarge, overflow: TextOverflow.ellipsis)), ], child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/spaces/warp/warp_shared_list.dart b/lib/pages/spaces/warp/warp_shared_list.dart index 6de60e01..13fea6f5 100644 --- a/lib/pages/spaces/warp/warp_shared_list.dart +++ b/lib/pages/spaces/warp/warp_shared_list.dart @@ -3,19 +3,20 @@ import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class WarpSharedList extends StatelessWidget { const WarpSharedList({super.key}); @override Widget build(BuildContext context) { - final controller = Get.find(); - return Obx(() { - if (controller.sharedWarps.isEmpty) { + return Watch((context) { + final sharedWarps = WarpController.sharedWarps; + if (sharedWarps.isEmpty) { return SizedBox(); } - final values = controller.sharedWarps.values.toList(); + final values = sharedWarps.values.toList(); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -37,20 +38,17 @@ class WarpSharedList extends StatelessWidget { borderRadius: BorderRadius.circular(defaultSpacing), child: InkWell( borderRadius: BorderRadius.circular(defaultSpacing), - onTap: () => controller.stopWarp(warp), + onTap: () => WarpController.stopWarp(warp), child: Padding( padding: EdgeInsets.all(defaultSpacing), child: Row( children: [ Icon(Icons.cyclone, color: Get.theme.colorScheme.onPrimary), horizontalSpacing(defaultSpacing), - Text( - warp.port.toString(), - style: Get.textTheme.labelMedium, - ), + Text(warp.port.toString(), style: Get.textTheme.labelMedium), const Spacer(), LoadingIconButton( - onTap: () => controller.stopWarp(warp), + onTap: () => WarpController.stopWarp(warp), extra: 5, icon: Icons.stop_circle, ), diff --git a/lib/pages/spaces/widgets/space_controls.dart b/lib/pages/spaces/widgets/space_controls.dart index 9216e100..2de3dbbd 100644 --- a/lib/pages/spaces/widgets/space_controls.dart +++ b/lib/pages/spaces/widgets/space_controls.dart @@ -1,17 +1,20 @@ import 'dart:async'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/spaces/studio/studio_controller.dart'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; import 'package:chat_interface/controller/spaces/warp_controller.dart'; -import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; -import 'package:chat_interface/pages/spaces/call_page.dart'; +import 'package:chat_interface/pages/settings/app/audio_settings.dart'; import 'package:chat_interface/pages/spaces/tabletop/tabletop_rotate_window.dart'; +import 'package:chat_interface/pages/spaces/widgets/space_device_selection.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SpaceControls extends StatefulWidget { const SpaceControls({super.key}); @@ -21,8 +24,8 @@ class SpaceControls extends StatefulWidget { } class _SpaceControlsState extends State { - final GlobalKey tabletopKey = GlobalKey(); - StreamSubscription? subscription; + final GlobalKey _tabletopKey = GlobalKey(), _microphoneKey = GlobalKey(), _outputDeviceKey = GlobalKey(); + StreamSubscription? _subscription; @override void initState() { @@ -31,40 +34,27 @@ class _SpaceControlsState extends State { @override void dispose() { - subscription?.cancel(); + _subscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { ThemeData theme = Get.theme; - final controller = Get.find(); return Center( heightFactor: 1, child: Padding( - padding: const EdgeInsets.only( - right: sectionSpacing, - left: sectionSpacing, - bottom: sectionSpacing, - ), + padding: const EdgeInsets.only(right: sectionSpacing, left: sectionSpacing, bottom: sectionSpacing), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Obx( - () => LoadingIconButton( + Watch( + (context) => LoadingIconButton( background: true, - loading: false.obs, - onTap: () { - controller.hideSidebar.toggle(); - if (controller.hideSidebar.value) { - Get.offAll(const CallPage(), transition: Transition.fadeIn); - } else { - Get.offAll(const ChatPageDesktop(), transition: Transition.fadeIn); - } - }, - icon: controller.hideSidebar.value ? Icons.arrow_forward : Icons.arrow_back, + onTap: () => SidebarController.toggleSidebar(), + icon: SidebarController.hideSidebar.value ? Icons.arrow_forward : Icons.arrow_back, iconSize: 30, ), ), @@ -76,8 +66,8 @@ class _SpaceControlsState extends State { mainAxisSize: MainAxisSize.min, children: [ // Tabletop rotation button - Obx( - () => Animate( + Watch( + (context) => Animate( effects: [ ExpandEffect( customHeightFactor: 1, @@ -86,25 +76,22 @@ class _SpaceControlsState extends State { axis: Axis.horizontal, alignment: Alignment.center, ), - FadeEffect( - duration: 250.ms, - ), - ScaleEffect( - duration: 250.ms, - curve: Curves.ease, - ), + FadeEffect(duration: 250.ms), + ScaleEffect(duration: 250.ms, curve: Curves.ease), ], - onInit: (ac) => ac.value = controller.currentTab.value == SpaceTabType.table.index ? 1 : 0, - target: controller.currentTab.value == SpaceTabType.table.index ? 1 : 0, + onInit: (ac) => ac.value = SpaceController.currentTab.value == SpaceTabType.table.index ? 1 : 0, + target: SpaceController.currentTab.value == SpaceTabType.table.index ? 1 : 0, child: Padding( padding: const EdgeInsets.only(right: defaultSpacing), child: LoadingIconButton( - key: tabletopKey, + key: _tabletopKey, background: true, padding: defaultSpacing, - loading: Get.find().loading, + loading: TabletopController.loading, onTap: () { - Get.dialog(TabletopRotateWindow(data: ContextMenuData.fromKey(tabletopKey, above: true))); + Get.dialog( + TabletopRotateWindow(data: ContextMenuData.fromKey(_tabletopKey, above: true)), + ); }, icon: Icons.crop_rotate, iconSize: 28, @@ -113,24 +100,93 @@ class _SpaceControlsState extends State { ), ), - // Full screen button + // Full screen button (no reactivity needed cause refresh of the screen happens anyway) LoadingIconButton( - loading: false.obs, background: true, padding: defaultSpacing, - onTap: () => controller.toggleFullScreen(), - icon: controller.fullScreen.value ? Icons.fullscreen_exit : Icons.fullscreen, + onTap: () => SpaceController.toggleFullScreen(), + icon: SpaceController.fullScreen.value ? Icons.fullscreen_exit : Icons.fullscreen, iconSize: 28, ), horizontalSpacing(defaultSpacing), + // Render mute and deafen buttons in case in Studio + Watch((ctx) { + if (!StudioController.connected.value) { + return SizedBox(); + } + + return Row( + children: [ + // Render the mute button + Watch( + (ctx) => LoadingIconButton( + key: _microphoneKey, + loading: StudioController.audioStateLoading, + background: true, + padding: defaultSpacing, + onTap: () => StudioController.toggleMute(), + onSecondaryTap: () { + if (StudioController.getConnection() == null || + StudioController.getConnection()!.getEngine() == null) { + return; + } + Get.dialog( + SpaceDeviceSelection( + title: "settings.audio.microphone".tr, + data: ContextMenuData.fromKey(_microphoneKey, above: true), + child: MicrophoneSelection( + engine: StudioController.getConnection()!.getEngine()!, + secondary: true, + ), + ), + ); + }, + icon: StudioController.audioMuted.value ? Icons.mic_off : Icons.mic, + iconSize: 28, + ), + ), + horizontalSpacing(defaultSpacing), + + // Render the deafen button + Watch( + (ctx) => LoadingIconButton( + key: _outputDeviceKey, + loading: StudioController.audioStateLoading, + background: true, + padding: defaultSpacing, + onTap: () => StudioController.toggleDeafened(), + onSecondaryTap: () { + if (StudioController.getConnection() == null || + StudioController.getConnection()!.getEngine() == null) { + return; + } + Get.dialog( + SpaceDeviceSelection( + title: "settings.audio.output_device".tr, + data: ContextMenuData.fromKey(_outputDeviceKey, above: true), + child: OutputDeviceSelection( + engine: StudioController.getConnection()!.getEngine()!, + secondary: true, + ), + ), + ); + }, + icon: StudioController.audioDeafened.value ? Icons.headset_off : Icons.headset, + iconSize: 28, + ), + ), + horizontalSpacing(defaultSpacing), + ], + ); + }), + // Warp manager button LoadingIconButton( - loading: false.obs, background: true, padding: defaultSpacing, - onTap: () => Get.find().open(), + onTap: () => WarpController.open(), icon: Icons.cyclone, iconSize: 28, ), @@ -141,8 +197,7 @@ class _SpaceControlsState extends State { LoadingIconButton( background: true, padding: defaultSpacing, - loading: false.obs, - onTap: () => controller.leaveCall(), + onTap: () => SpaceController.leaveSpace(), icon: Icons.call_end, color: theme.colorScheme.error, iconSize: 28, @@ -152,17 +207,14 @@ class _SpaceControlsState extends State { ], ), const Spacer(), - Obx( - () { - return LoadingIconButton( - loading: false.obs, - background: true, - onTap: () => controller.chatOpen.toggle(), - icon: controller.chatOpen.value ? Icons.arrow_forward : Icons.arrow_back, - iconSize: 30, - ); - }, - ), + Watch((context) { + return LoadingIconButton( + background: true, + onTap: () => SpaceController.chatOpen.value = !SpaceController.chatOpen.peek(), + icon: SpaceController.chatOpen.value ? Icons.arrow_forward : Icons.arrow_back, + iconSize: 30, + ); + }), ], ), ), diff --git a/lib/pages/spaces/widgets/space_device_selection.dart b/lib/pages/spaces/widgets/space_device_selection.dart new file mode 100644 index 00000000..b6be2cdc --- /dev/null +++ b/lib/pages/spaces/widgets/space_device_selection.dart @@ -0,0 +1,25 @@ +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; + +class SpaceDeviceSelection extends StatelessWidget { + final String title; + final Widget child; + final ContextMenuData data; + + const SpaceDeviceSelection({super.key, required this.title, required this.child, required this.data}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SlidingWindowBase( + title: const [], + position: data, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(title, style: theme.textTheme.labelLarge), verticalSpacing(defaultSpacing), child], + ), + ); + } +} diff --git a/lib/pages/spaces/widgets/space_grid_renderer.dart b/lib/pages/spaces/widgets/space_grid_renderer.dart new file mode 100644 index 00000000..b4bcb31a --- /dev/null +++ b/lib/pages/spaces/widgets/space_grid_renderer.dart @@ -0,0 +1,64 @@ +import 'dart:math'; + +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; + +class SpaceGridRenderer extends StatelessWidget { + /// The amount of widgets that should fit into the grid + final int amount; + + /// Renders the individual widgets (size will be applied) + final Widget Function(int) renderer; + + /// The padding between the rectangles + final double padding; + + const SpaceGridRenderer({super.key, required this.amount, this.padding = defaultSpacing, required this.renderer}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final ratio = 16 / 9; + final maxWidth = min(constraints.maxWidth - padding, 500); + final maxHeight = constraints.maxHeight - padding; + double bestSize = 0; + + // Compute the optimal width for the children + for (int c = 1; c <= amount; c++) { + final r = (amount + c - 1) ~/ c; + final maxChildWidth = maxWidth / c; + final maxChildHeight = maxHeight / r; + final childWidth = maxChildWidth < maxChildHeight * ratio ? maxChildWidth : maxChildHeight * ratio; + if (childWidth > bestSize) { + bestSize = childWidth; + } + } + + final childWidth = max(bestSize, 300); + + return SingleChildScrollView( + child: Center( + child: Padding( + padding: EdgeInsets.all(padding / 2), + child: Wrap( + alignment: WrapAlignment.center, + children: List.generate(amount, (index) { + if (index >= amount) return const SizedBox(); + return Padding( + padding: EdgeInsets.all(padding / 2), + child: SizedBox( + width: childWidth - padding * 2, + height: (childWidth - padding * 2) / ratio, + child: renderer.call(index), + ), + ); + }), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/spaces/widgets/space_info_tab.dart b/lib/pages/spaces/widgets/space_info_tab.dart index af6a2b45..48e92657 100644 --- a/lib/pages/spaces/widgets/space_info_tab.dart +++ b/lib/pages/spaces/widgets/space_info_tab.dart @@ -1,30 +1,75 @@ +import 'package:chat_interface/controller/spaces/studio/studio_controller.dart'; +import 'package:chat_interface/pages/spaces/widgets/space_studio_grid_tab.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class SpaceInfoTab extends StatelessWidget { +class SpaceInfoTab extends StatefulWidget { const SpaceInfoTab({super.key}); + @override + State createState() => _SpaceInfoTabState(); +} + +class _SpaceInfoTabState extends State { @override Widget build(BuildContext context) { - return Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 700 + sectionSpacing * 2, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: sectionSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - verticalSpacing(75), - Text("spaces.welcome".tr, style: Get.textTheme.headlineMedium), - verticalSpacing(sectionSpacing), - Text("spaces.welcome.desc".tr, style: Get.textTheme.bodyMedium), - ], + final theme = Theme.of(context); + + return Watch((context) { + // Render the member list when connected to Studio + if (StudioController.connected.value) { + return SpaceStudioGridTab(); + } + + // Return a basic description of Spaces instead + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 700 + sectionSpacing * 2), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: sectionSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + verticalSpacing(75), + Text("spaces.welcome".tr, style: Get.textTheme.headlineMedium), + verticalSpacing(sectionSpacing), + Text("spaces.welcome.desc".tr, style: Get.textTheme.bodyMedium), + + // Render the status of the studio connection + Watch((ctx) { + if (StudioController.connectionError.value != "") { + return Padding( + padding: const EdgeInsets.only(top: sectionSpacing), + child: Text(StudioController.connectionError.value, style: theme.textTheme.labelMedium), + ); + } + + // Render a loading indicator + return Visibility( + visible: StudioController.connecting.value, + child: Padding( + padding: EdgeInsets.only(top: sectionSpacing), + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(color: theme.colorScheme.onPrimary), + ), + horizontalSpacing(defaultSpacing), + Text("spaces.studio.connecting".tr, style: Get.textTheme.bodyMedium), + ], + ), + ), + ); + }), + ], + ), ), ), - ), - ); + ); + }); } } diff --git a/lib/pages/spaces/widgets/space_info_window.dart b/lib/pages/spaces/widgets/space_info_window.dart index 2df33745..5fbb0eee 100644 --- a/lib/pages/spaces/widgets/space_info_window.dart +++ b/lib/pages/spaces/widgets/space_info_window.dart @@ -1,62 +1,87 @@ -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/spaces/studio/studio_controller.dart'; import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; import 'package:chat_interface/theme/components/forms/fj_switch.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/theme/ui/profile/profile_button.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SpaceInfoWindow extends StatelessWidget { const SpaceInfoWindow({super.key}); @override Widget build(BuildContext context) { - final controller = Get.find(); - final memberController = Get.find(); - return DialogBase( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Text("Space #${controller.id.value}", style: Get.theme.textTheme.titleMedium), + Text("Space on ${SpaceController.domain!}", style: Get.theme.textTheme.titleMedium), + verticalSpacing(defaultSpacing), + ProfileButton( + icon: Icons.content_copy, + label: "Copy Space ID", + onTap: () => Clipboard.setData(ClipboardData(text: SpaceController.id.value!)), ), verticalSpacing(defaultSpacing), Row( children: [ Text("Disable Tabletop cursors"), const Spacer(), - Obx( - () => FJSwitch( - value: Get.find().disableCursorSending.value, - onChanged: (b) => Get.find().disableCursorSending.value = b, + Watch( + (context) => FJSwitch( + value: TabletopController.disableCursorSending.value, + onChanged: (b) => TabletopController.disableCursorSending.value = b, ), ), ], ), + verticalSpacing(sectionSpacing), + Text("Studio (experimental)", style: Get.theme.textTheme.labelMedium), verticalSpacing(defaultSpacing), - Text("Members", style: Get.theme.textTheme.labelMedium), + ProfileButton( + icon: Icons.launch, + label: "Connect to Studio", + onTap: () { + StudioController.connectToStudio(); + Get.back(); + }, + ), + /* verticalSpacing(elementSpacing), - Obx( - () { - return Column( - children: memberController.members.values.map((member) { - return Padding( - padding: const EdgeInsets.only(bottom: elementSpacing), - child: Row( - children: [ - Text(member.friend.displayName.value, style: Get.theme.textTheme.bodyMedium), - horizontalSpacing(defaultSpacing), - Text("#${member.id}", style: Get.theme.textTheme.bodyMedium), - ], - ), - ); - }).toList(), - ); + ProfileButton( + icon: Icons.play_arrow, + label: "Try video track", + onTap: () { + StudioController.getConnection()!.getPublisher().createCameraTrack(); }, ), + */ + verticalSpacing(sectionSpacing), + Text("Members", style: Get.theme.textTheme.labelMedium), + verticalSpacing(defaultSpacing), + Watch((context) { + return Column( + children: + SpaceMemberController.members.value.values.map((member) { + return Padding( + padding: const EdgeInsets.only(bottom: elementSpacing), + child: Row( + children: [ + Text(member.friend.displayName.value, style: Get.theme.textTheme.bodyMedium), + horizontalSpacing(defaultSpacing), + Text("#${member.id}", style: Get.theme.textTheme.bodyMedium), + ], + ), + ); + }).toList(), + ); + }), ], ), ); diff --git a/lib/pages/spaces/widgets/space_members_tab.dart b/lib/pages/spaces/widgets/space_members_tab.dart index 8f791658..3b63b4b0 100644 --- a/lib/pages/spaces/widgets/space_members_tab.dart +++ b/lib/pages/spaces/widgets/space_members_tab.dart @@ -3,10 +3,12 @@ import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.da import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/settings/town/tabletop_settings.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/profile.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SpaceMembersTab extends StatefulWidget { const SpaceMembersTab({super.key}); @@ -18,20 +20,17 @@ class SpaceMembersTab extends StatefulWidget { class _SpaceMembersTabState extends State { @override Widget build(BuildContext context) { - final controller = Get.find(); - final tableController = Get.find(); - return Padding( padding: const EdgeInsets.symmetric(horizontal: elementSpacing, vertical: defaultSpacing), child: Padding( padding: const EdgeInsets.symmetric(horizontal: elementSpacing), - child: Obx( - () => ListView.builder( + child: Watch( + (context) => ListView.builder( shrinkWrap: true, - itemCount: controller.members.length, + itemCount: SpaceMemberController.members.length, itemBuilder: (context, index) { final GlobalKey listKey = GlobalKey(); - final member = controller.members.values.elementAt(index); + final member = SpaceMemberController.members.values.elementAt(index); return Padding( key: listKey, @@ -44,7 +43,7 @@ class _SpaceMembersTabState extends State { final RenderBox box = listKey.currentContext?.findRenderObject() as RenderBox; Get.dialog( Profile( - position: box.localToGlobal(box.size.bottomLeft(Offset.zero)), + data: ContextMenuData.fromKey(listKey, below: true), friend: member.friend, size: box.size.width.toInt(), ), @@ -56,14 +55,10 @@ class _SpaceMembersTabState extends State { padding: const EdgeInsets.all(elementSpacing), child: Row( children: [ - Flexible( - child: UserRenderer( - id: member.friend.id, - ), - ), + Flexible(child: UserRenderer(id: member.friend.id)), horizontalSpacing(defaultSpacing), - Obx( - () => Visibility( + Watch( + (context) => Visibility( visible: !member.verified.value, child: Padding( padding: const EdgeInsets.only(right: defaultSpacing), @@ -74,8 +69,8 @@ class _SpaceMembersTabState extends State { ), ), ), - Obx(() { - var hue = tableController.cursors[member.id]?.hue; + Watch((context) { + var hue = TabletopController.cursors[member.id]?.hue; // Don't render a color in case there isn't one if (hue == null && StatusController.ownAddress != member.friend.id) { diff --git a/lib/pages/spaces/widgets/space_studio_grid_tab.dart b/lib/pages/spaces/widgets/space_studio_grid_tab.dart new file mode 100644 index 00000000..f57c11d2 --- /dev/null +++ b/lib/pages/spaces/widgets/space_studio_grid_tab.dart @@ -0,0 +1,95 @@ +import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; +import 'package:chat_interface/pages/spaces/widgets/space_grid_renderer.dart'; +import 'package:chat_interface/theme/components/user_renderer.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:signals/signals_flutter.dart'; + +class SpaceStudioGridTab extends StatefulWidget { + const SpaceStudioGridTab({super.key}); + + @override + State createState() => _SpaceStudioGridTabState(); +} + +class _SpaceStudioGridTabState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Center( + child: Watch((ctx) { + return SpaceGridRenderer( + amount: SpaceMemberController.members.value.length, + padding: sectionSpacing, + renderer: (index) { + final member = SpaceMemberController.members.values.elementAt(index); + + // Render the actual user + return renderMember(member, theme); + }, + ); + }), + ); + } + + /// Render a member of the space + Widget renderMember(SpaceMember member, ThemeData theme) { + return Watch((context) { + // Compute the border of the container from the talking state of the member + Border? border; + if (member.talking.value) { + border = Border.all(color: theme.colorScheme.onPrimary, width: 2.0); + } + + return Stack( + children: [ + // Base background layer with the profile picture + Container( + decoration: BoxDecoration( + color: theme.colorScheme.onInverseSurface, + border: border, + borderRadius: BorderRadius.circular(sectionSpacing), + ), + child: Center(child: UserAvatar(id: member.friend.id, size: 64)), + ), + + // Mute/deafen/connection indicator + Watch((ctx) { + // Determine the icon + IconData? icon; + if (member.isMuted.value) { + icon = Icons.mic_off; + } + if (member.isDeafened.value) { + icon = Icons.headset_off; + } + if (!member.connectedToStudio.value) { + icon = Icons.power_off; + } + + // Render nothing if there is no icon + if (icon == null) { + return SizedBox(); + } + + return Positioned( + right: defaultSpacing, + bottom: defaultSpacing, + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.errorContainer, + borderRadius: BorderRadius.circular(200), + boxShadow: [BoxShadow(color: theme.colorScheme.primaryContainer, blurRadius: 10)], + ), + width: 32, + height: 32, + child: Center(child: Icon(icon, color: theme.colorScheme.error)), + ), + ); + }), + ], + ); + }); + } +} diff --git a/lib/pages/spaces/widgets/spaces_message_feed.dart b/lib/pages/spaces/widgets/spaces_message_feed.dart index a8f55cd7..1da8860e 100644 --- a/lib/pages/spaces/widgets/spaces_message_feed.dart +++ b/lib/pages/spaces/widgets/spaces_message_feed.dart @@ -1,9 +1,8 @@ -import 'package:chat_interface/controller/spaces/spaces_message_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/pages/chat/components/message/message_list.dart'; import 'package:chat_interface/pages/chat/messages/message_input.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; class SpacesMessageFeed extends StatefulWidget { const SpacesMessageFeed({super.key}); @@ -17,17 +16,8 @@ class SpacesMessageFeedState extends State { Widget build(BuildContext context) { return Column( children: [ - Expanded( - child: MessageList( - provider: Get.find().provider, - overwritePadding: sectionSpacing, - ), - ), - MessageInput( - draft: "spaces_input", - provider: Get.find().provider, - secondary: true, - ), + Expanded(child: MessageList(provider: SpaceController.provider, overwritePadding: sectionSpacing)), + MessageInput(draft: "spaces_input", provider: SpaceController.provider, secondary: true), ], ); } diff --git a/lib/pages/spaces/widgets/video_preview.dart b/lib/pages/spaces/widgets/video_preview.dart new file mode 100644 index 00000000..9e7ee34d --- /dev/null +++ b/lib/pages/spaces/widgets/video_preview.dart @@ -0,0 +1,89 @@ +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:signals/signals_flutter.dart'; + +class VideoPreview extends StatefulWidget { + const VideoPreview({super.key}); + + @override + State createState() => _VideoPreviewState(); +} + +class _VideoPreviewState extends State with SignalsMixin { + final initialized = signal(false); + MediaStream? _mediaStream; + final renderer = RTCVideoRenderer(); + + @override + void initState() { + super.initState(); + start(); + } + + Future start() async { + await renderer.initialize(); + _mediaStream = await mediaDevices.getUserMedia(_getMediaConstraints(audio: false, video: true)); + if (_mediaStream!.getVideoTracks().isEmpty) { + sendLog("warning: no video tracks"); + return; + } + + // Start the tracks + for (var track in _mediaStream!.getVideoTracks()) { + track.enabled = true; + } + + // Add it to the renderer + renderer.srcObject = _mediaStream; + initialized.value = true; + } + + /// Media constraints for video and audio tracks + Map _getMediaConstraints({bool audio = true, bool video = true}) { + return { + 'audio': audio ? true : false, + 'video': + video + ? { + 'mandatory': { + 'minWidth': '1920', + 'minHeight': '1080', + 'width': '1920', + 'height': '1080', + 'minFrameRate': '24', + 'frameRate': '30', + }, + 'facingMode': 'user', + 'optional': [], + } + : false, + }; + } + + @override + void dispose() { + _mediaStream!.getTracks().forEach((element) { + element.stop(); + }); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DialogBase( + child: SizedBox( + width: 300, + height: 300, + child: Watch((context) { + if (!initialized.value) { + return const SizedBox(); + } + + return RTCVideoView(renderer); + }), + ), + ); + } +} diff --git a/lib/pages/status/error/error_container.dart b/lib/pages/status/error/error_container.dart index e6f677b4..a8ed2adb 100644 --- a/lib/pages/status/error/error_container.dart +++ b/lib/pages/status/error/error_container.dart @@ -1,20 +1,15 @@ -import 'dart:async'; - import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ErrorContainer extends StatelessWidget { /// Translation required final String message; final bool expand; - const ErrorContainer({ - super.key, - required this.message, - this.expand = false, - }); + const ErrorContainer({super.key, required this.message, this.expand = false}); @override Widget build(BuildContext context) { @@ -22,7 +17,10 @@ class ErrorContainer extends StatelessWidget { return Container( padding: const EdgeInsets.all(defaultSpacing), - decoration: BoxDecoration(color: theme.colorScheme.errorContainer, borderRadius: BorderRadius.circular(defaultSpacing)), + decoration: BoxDecoration( + color: theme.colorScheme.errorContainer, + borderRadius: BorderRadius.circular(defaultSpacing), + ), child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, @@ -45,11 +43,7 @@ class InfoContainer extends StatelessWidget { final String message; final bool expand; - const InfoContainer({ - super.key, - required this.message, - this.expand = false, - }); + const InfoContainer({super.key, required this.message, this.expand = false}); @override Widget build(BuildContext context) { @@ -75,91 +69,80 @@ class InfoContainer extends StatelessWidget { } } -class AnimatedErrorContainer extends StatefulWidget { +class _AnimatedContainerBase extends StatefulWidget { /// Translation required - final RxString message; + final Signal message; final EdgeInsets padding; final bool expand; + final Color iconColor; + final Color backgroundColor; - const AnimatedErrorContainer({ + const _AnimatedContainerBase({ super.key, required this.padding, required this.message, + required this.iconColor, + required this.backgroundColor, this.expand = false, }); @override - State createState() => _AnimatedErrorContainerState(); + State<_AnimatedContainerBase> createState() => _AnimatedContainerBaseState(); } -class _AnimatedErrorContainerState extends State { - final message = "".obs; - final showing = false.obs; +class _AnimatedContainerBaseState extends State<_AnimatedContainerBase> with SignalsMixin { + // For handling the animation AnimationController? controller; - StreamSubscription? _sub; String? prev; + // State + late final _message = createSignal(""); + late final _showing = createSignal(false); + @override void initState() { - _sub = widget.message.listen((p0) { - if (prev != null && prev != "" && p0 != "") { + createEffect(() { + final msg = widget.message.value; + if (prev != null && prev != "" && msg != "") { controller?.loop(count: 1, reverse: true); } - if (p0 != "") { - message.value = p0; - showing.value = true; - prev = p0; + if (msg != "") { + _message.value = msg; + _showing.value = true; + prev = msg; } else { - showing.value = false; + _showing.value = false; } }); super.initState(); } - @override - void dispose() { - _sub?.cancel(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return Obx( - () => Animate( - effects: [ - ScaleEffect( - duration: 250.ms, - curve: Curves.ease, - begin: const Offset(1.1, 1.1), - end: const Offset(1.0, 1.0), - ), - ], - onInit: (controller) => this.controller = controller, - child: Animate( - effects: [ - ExpandEffect( - axis: Axis.vertical, - curve: Curves.ease, - duration: 250.ms, + return Animate( + effects: [ + ScaleEffect(duration: 250.ms, curve: Curves.ease, begin: const Offset(1.1, 1.1), end: const Offset(1.0, 1.0)), + ], + onInit: (controller) => this.controller = controller, + child: Animate( + effects: [ExpandEffect(axis: Axis.vertical, curve: Curves.ease, duration: 250.ms)], + target: _showing.value ? 1 : 0, + child: Padding( + padding: widget.padding, + child: Container( + padding: const EdgeInsets.all(defaultSpacing), + decoration: BoxDecoration( + color: Get.theme.colorScheme.errorContainer, + borderRadius: BorderRadius.circular(defaultSpacing), ), - ], - target: showing.value ? 1 : 0, - child: Padding( - padding: widget.padding, - child: Container( - padding: const EdgeInsets.all(defaultSpacing), - decoration: BoxDecoration(color: Get.theme.colorScheme.errorContainer, borderRadius: BorderRadius.circular(defaultSpacing)), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: widget.expand ? MainAxisSize.max : MainAxisSize.min, - children: [ - Icon(Icons.error, color: Theme.of(context).colorScheme.error), - horizontalSpacing(defaultSpacing), - Flexible( - child: Text(message.value, style: Theme.of(context).textTheme.labelMedium), - ), - ], - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: widget.expand ? MainAxisSize.max : MainAxisSize.min, + children: [ + Icon(Icons.error, color: Theme.of(context).colorScheme.error), + horizontalSpacing(defaultSpacing), + Flexible(child: Text(_message.value, style: Theme.of(context).textTheme.labelMedium)), + ], ), ), ), @@ -168,95 +151,46 @@ class _AnimatedErrorContainerState extends State { } } -class AnimatedInfoContainer extends StatefulWidget { +class AnimatedErrorContainer extends StatelessWidget { /// Translation required - final RxString message; + final Signal message; final EdgeInsets padding; final bool expand; - const AnimatedInfoContainer({ - super.key, - required this.padding, - required this.message, - this.expand = false, - }); - @override - State createState() => _AnimatedInfoContainerState(); -} - -class _AnimatedInfoContainerState extends State { - final message = "".obs; - final showing = false.obs; - AnimationController? controller; - StreamSubscription? _sub; - String? prev; + const AnimatedErrorContainer({super.key, required this.padding, required this.message, this.expand = false}); @override - void initState() { - _sub = widget.message.listen((p0) { - if (prev != null && prev != "" && p0 != "") { - controller?.loop(count: 1, reverse: true); - } - if (p0 != "") { - message.value = p0; - showing.value = true; - prev = p0; - } else { - showing.value = false; - } - }); - super.initState(); + Widget build(BuildContext context) { + final theme = Theme.of(context); + return _AnimatedContainerBase( + key: key, + padding: padding, + message: message, + iconColor: theme.colorScheme.error, + backgroundColor: theme.colorScheme.errorContainer, + expand: expand, + ); } +} - @override - void dispose() { - _sub?.cancel(); - super.dispose(); - } +class AnimatedInfoContainer extends StatelessWidget { + /// Translation required + final Signal message; + final EdgeInsets padding; + final bool expand; + + const AnimatedInfoContainer({super.key, required this.padding, required this.message, this.expand = false}); @override Widget build(BuildContext context) { - return Obx( - () => Animate( - effects: [ - ScaleEffect( - duration: 250.ms, - curve: Curves.ease, - begin: const Offset(1.1, 1.1), - end: const Offset(1.0, 1.0), - ), - ], - onInit: (controller) => this.controller = controller, - child: Animate( - effects: [ - ExpandEffect( - axis: Axis.vertical, - curve: Curves.ease, - duration: 250.ms, - ), - ], - target: showing.value ? 1 : 0, - child: Padding( - padding: widget.padding, - child: Container( - padding: const EdgeInsets.all(defaultSpacing), - decoration: BoxDecoration(color: Get.theme.colorScheme.primary, borderRadius: BorderRadius.circular(defaultSpacing)), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: widget.expand ? MainAxisSize.max : MainAxisSize.min, - children: [ - Icon(Icons.error, color: Theme.of(context).colorScheme.onPrimary), - horizontalSpacing(defaultSpacing), - Flexible( - child: Text(message.value, style: Theme.of(context).textTheme.labelMedium), - ), - ], - ), - ), - ), - ), - ), + final theme = Theme.of(context); + return _AnimatedContainerBase( + key: key, + padding: padding, + message: message, + iconColor: theme.colorScheme.onPrimary, + backgroundColor: theme.colorScheme.primary, + expand: expand, ); } } diff --git a/lib/pages/status/error/error_page.dart b/lib/pages/status/error/error_page.dart index ccd422fc..6de8260f 100644 --- a/lib/pages/status/error/error_page.dart +++ b/lib/pages/status/error/error_page.dart @@ -6,6 +6,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ErrorPage extends StatefulWidget { final String title; @@ -16,27 +17,24 @@ class ErrorPage extends StatefulWidget { State createState() => _ErrorPageState(); } -class _ErrorPageState extends State { +class _ErrorPageState extends State with SignalsMixin { Timer? _timer; var _start = 30.0; - final _progress = 0.0.obs; + late final _progress = createSignal(0.0); @override void initState() { int duration = 10; - _timer = Timer.periodic( - duration.ms, - (timer) { - if (_start <= 0) { - timer.cancel(); - setupManager.retry(); - } else { - _start -= duration / 1000; - _progress.value = _start / 30; - } - }, - ); + _timer = Timer.periodic(duration.ms, (timer) { + if (_start <= 0) { + timer.cancel(); + setupManager.retry(); + } else { + _start -= duration / 1000; + _progress.value = _start / 30; + } + }); super.initState(); } @@ -54,32 +52,29 @@ class _ErrorPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text( - widget.title.tr, - style: Get.textTheme.headlineMedium, - ), + Text(widget.title.tr, style: Get.textTheme.headlineMedium), verticalSpacing(sectionSpacing), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Obx(() => Row( - children: [ - SizedBox( - width: 20.0, - height: 20.0, - child: CircularProgressIndicator( - backgroundColor: Get.theme.colorScheme.primary, - color: Get.theme.colorScheme.onPrimary, - value: _progress.value, - strokeWidth: 2, - ), - ), - horizontalSpacing(defaultSpacing * 2), - Text("${'retry.text.1'.tr} "), - Text('${_start.toInt()}'), - Text(" ${'retry.text.2'.tr}"), - ], - )), + Row( + children: [ + SizedBox( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator( + backgroundColor: Get.theme.colorScheme.primary, + color: Get.theme.colorScheme.onPrimary, + value: _progress.value, + strokeWidth: 2, + ), + ), + horizontalSpacing(defaultSpacing * 2), + Text("${'retry.text.1'.tr} "), + Text('${_start.toInt()}'), + Text(" ${'retry.text.2'.tr}"), + ], + ), ], ), verticalSpacing(defaultSpacing), diff --git a/lib/pages/status/error/offline_hider.dart b/lib/pages/status/error/offline_hider.dart index a7430f75..bc6cea08 100644 --- a/lib/pages/status/error/offline_hider.dart +++ b/lib/pages/status/error/offline_hider.dart @@ -2,7 +2,7 @@ import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; -import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class OfflineHider extends StatelessWidget { final EdgeInsets? padding; @@ -10,35 +10,19 @@ class OfflineHider extends StatelessWidget { final Widget child; final Alignment alignment; - const OfflineHider({ - super.key, - required this.axis, - required this.child, - required this.alignment, - this.padding, - }); + const OfflineHider({super.key, required this.axis, required this.child, required this.alignment, this.padding}); @override Widget build(BuildContext context) { - return Obx( - () => Animate( + return Watch( + (ctx) => Animate( effects: [ - ExpandEffect( - axis: axis, - curve: Curves.ease, - duration: 250.ms, - alignment: alignment, - ), - FadeEffect( - duration: 250.ms, - ), + ExpandEffect(axis: axis, curve: Curves.ease, duration: 250.ms, alignment: alignment), + FadeEffect(duration: 250.ms), ], - target: Get.find().connected.value ? 1 : 0, - onInit: (controller) => controller.value = Get.find().connected.value ? 1 : 0, - child: Padding( - padding: padding ?? EdgeInsets.all(0), - child: child, - ), + target: ConnectionController.connected.value ? 1 : 0, + onInit: (controller) => controller.value = ConnectionController.connected.value ? 1 : 0, + child: Padding(padding: padding ?? EdgeInsets.all(0), child: child), ), ); } diff --git a/lib/pages/status/setup/database/database_init_native.dart b/lib/pages/status/setup/database/database_init_native.dart index 33bb60fd..731a2c7f 100644 --- a/lib/pages/status/setup/database/database_init_native.dart +++ b/lib/pages/status/setup/database/database_init_native.dart @@ -28,10 +28,7 @@ Future loadInstance(String name) async { } // Open the encrypted database (code was taken from the drift encrypted example) - db = Database(NativeDatabase.createInBackground( - file, - logStatements: driftLogger, - )); + db = Database(NativeDatabase.createInBackground(file, logStatements: driftLogger)); currentInstance = name; return null; diff --git a/lib/pages/status/setup/instance_setup.dart b/lib/pages/status/setup/instance_setup.dart index ed5b5fef..f7c358b6 100644 --- a/lib/pages/status/setup/instance_setup.dart +++ b/lib/pages/status/setup/instance_setup.dart @@ -1,6 +1,7 @@ import 'dart:async'; +import 'dart:io'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; import 'package:chat_interface/pages/settings/app/log_settings.dart'; import 'package:chat_interface/pages/status/error/error_page.dart'; import 'package:chat_interface/pages/status/setup/database/database_init_stub.dart' @@ -8,7 +9,7 @@ import 'package:chat_interface/pages/status/setup/database/database_init_stub.da if (dart.library.js) 'package:chat_interface/pages/status/setup/database/database_init_web.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; -import 'package:chat_interface/util/logging_framework.dart'; +import 'package:dbus_secrets/dbus_secrets.dart'; import 'package:drift/drift.dart' as drift; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -20,9 +21,7 @@ import '../../../main.dart'; import '../../../util/vertical_spacing.dart'; import 'setup_manager.dart'; -const secureStorage = FlutterSecureStorage( - aOptions: AndroidOptions(encryptedSharedPreferences: true), -); +const secureStorage = FlutterSecureStorage(aOptions: AndroidOptions(encryptedSharedPreferences: true)); class InstanceSetup extends Setup { InstanceSetup() : super('loading.instance', false); @@ -55,8 +54,11 @@ class InstanceSetup extends Setup { } } -String dbEncrypted(String data) { - return encryptSymmetric(data, databaseKey); +/// Convert data to a local database encrypted string. +/// +/// Encrypts using the database key stored in secure storage. +String dbEncrypted(String data, [Sodium? sodium, SecureKey? key]) { + return encryptSymmetric(data, key ?? databaseKey, sodium); } String fromDbEncrypted(String cipher) { @@ -81,8 +83,40 @@ Future setupInstance(String name, {bool next = false}) async { var _ = await (db.select(db.setting)).get(); // Get the encryption password from secure storage - final databaseKeyField = "db_key_$name"; - var encryptionKey = await secureStorage.read(key: databaseKeyField); + String? error; + if (Platform.isLinux) { + error = await loadEncryptionKeyDbusSecrets(name); + } else { + error = await loadEncryptionKeySecureStorage(name); + } + if (error != null) { + return error; + } + + // Enable logging for the current instance + await LogManager.enableLogging(); + + // Open the next setup page + if (next) { + unawaited(setupManager.next(open: true)); + } + + return null; +} + +/// Load the encryption key from flutter_secure_storage. +/// +/// Returns an error if there was one. +Future loadEncryptionKeySecureStorage(String instance) async { + final databaseKeyField = "db_key_$instance"; + String? encryptionKey; + try { + encryptionKey = await secureStorage.read(key: databaseKeyField); + } catch (_) { + return "secure_storage.not_supported".tr; + } + + // Generate a new encryption key in case there isn't one yet if (encryptionKey == null) { // Create a new random encryption key (with sodium so it's secure) databaseKey = randomSymmetricKey(); @@ -91,55 +125,54 @@ Future setupInstance(String name, {bool next = false}) async { await secureStorage.write(key: databaseKeyField, value: packageSymmetricKey(databaseKey)); encryptionKey = await secureStorage.read(key: databaseKeyField); if (encryptionKey == null) { - sendLog("couldn't write encryption key to secure storage"); - return "Your browser doesn't support secure storage."; - } - - // Migrate all fields that need to be encrypted from now on - for (var toMigrate in [ - "tokens", - "private_key", - "public_key", - "signature_public_key", - "signature_private_key", - "key_sync_pub", - "key_sync_priv", - "key_sync_sig_pub", - "key_sync_sig_priv", - "key_sync_sig", - ]) { - final success = await _migrateFieldToEncryption(toMigrate); - if (!success) { - sendLog("field to migrate ($toMigrate) not found.."); - } + return "secure_storage.not_supported".tr; } } else { databaseKey = unpackageSymmetricKey(encryptionKey); } - // Enable logging for the current instance - await LogManager.enableLogging(); + return null; +} - // Open the next setup page - if (next) { - unawaited(setupManager.next(open: true)); +/// Load the encryption key from dbus_secrets (specifically for Linux). +/// +/// Returns an error if there was one. +Future loadEncryptionKeyDbusSecrets(String instance) async { + // Connect to org.freedesktop.secrets using dbus + final secrets = DBusSecrets(appName: linuxDbusAppName); + var result = await secrets.initialize(); + if (!result) { + return "secure_storage.not_supported".tr; } - return null; -} + // Try to unlock the vault + result = await secrets.unlock(); + if (!result) { + await secrets.close(); + return "secure_storage.unlock_failed".tr; + } -/// Encrypts the specified field in the settings table of the database (to migrate it from being unencrypted before) -Future _migrateFieldToEncryption(String field) async { - final value = await (db.setting.select()..where((tbl) => tbl.key.equals(field))).getSingleOrNull(); - if (value == null) { - return false; + // Get the encryption key from the vault + final databaseKeyField = "db_key_$instance"; + var encryptionKey = await secrets.get(databaseKeyField); + + // Create a new database encryption key in case it isn't there yet + if (encryptionKey == null) { + encryptionKey = packageSymmetricKey(randomSymmetricKey()); + result = await secrets.set(databaseKeyField, encryptionKey); + if (!result) { + await secrets.close(); + return "secure_storage.not_supported".tr; + } } - // Encrypt the value and put it back into the database - final encrypted = encryptSymmetric(value.value, databaseKey); - await db.setting.insertOnConflictUpdate(SettingData(key: field, value: encrypted)); + // Set the database key + databaseKey = unpackageSymmetricKey(encryptionKey); - return true; + // Close the dbus session + await secrets.close(); + + return null; } /// Get the value of a specified field store in the settings table @@ -183,14 +216,13 @@ class _InstanceSelectionPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ + Text('setup.choose.instance'.tr, style: Get.textTheme.headlineMedium, textAlign: TextAlign.center), + verticalSpacing(sectionSpacing), Text( - 'setup.choose.instance'.tr, - style: Get.textTheme.headlineMedium, - textAlign: TextAlign.center, + "If you don't know what this is, just click on default and you'll be fine.", + style: Get.textTheme.bodyMedium, ), verticalSpacing(sectionSpacing), - Text("If you don't know what this is, just click on default and you'll be fine.", style: Get.textTheme.bodyMedium), - verticalSpacing(sectionSpacing), ListView.builder( shrinkWrap: true, itemCount: widget.instances.length, @@ -228,10 +260,7 @@ class _InstanceSelectionPageState extends State { }, ), verticalSpacing(sectionSpacing), - FJTextField( - controller: _controller, - hintText: 'setup.instance.name'.tr, - ), + FJTextField(controller: _controller, hintText: 'setup.instance.name'.tr), verticalSpacing(defaultSpacing), FJElevatedButton( onTap: () => setupInstance(_controller.text, next: true), diff --git a/lib/pages/status/setup/policy_setup.dart b/lib/pages/status/setup/policy_setup.dart index fed01c25..23d3de5d 100644 --- a/lib/pages/status/setup/policy_setup.dart +++ b/lib/pages/status/setup/policy_setup.dart @@ -9,6 +9,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../util/vertical_spacing.dart'; @@ -54,15 +55,11 @@ class PolicySetup extends Setup { // Check if the agreements file has already been created and contains the latest date file = File(path.join(supportDir.path, agreeFile)); if (!file.existsSync()) { - return PolicyAcceptPage( - versionToWrite: uncoveredDate, - ); + return PolicyAcceptPage(versionToWrite: uncoveredDate); } final content = await file.readAsString(); if (content.trim() != uncoveredDate) { - return PolicyAcceptPage( - versionToWrite: uncoveredDate, - ); + return PolicyAcceptPage(versionToWrite: uncoveredDate); } return null; @@ -78,16 +75,9 @@ class PolicyAcceptPage extends StatefulWidget { State createState() => _PolicyAcceptPageState(); } -class _PolicyAcceptPageState extends State { - final error = "".obs; - final clicked = false.obs; - final TextEditingController _controller = TextEditingController(); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } +class _PolicyAcceptPageState extends State with SignalsMixin { + late final _error = createSignal(""); + late final _clicked = createSignal(false); @override Widget build(BuildContext context) { @@ -96,64 +86,43 @@ class _PolicyAcceptPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text( - 'setup.policy'.tr, - style: Get.textTheme.headlineMedium, - ), + Text('setup.policy'.tr, style: Get.textTheme.headlineMedium), verticalSpacing(sectionSpacing), Text("setup.policy.text".tr, style: Get.textTheme.bodyMedium), verticalSpacing(sectionSpacing), - AnimatedErrorContainer( - padding: const EdgeInsets.only(bottom: defaultSpacing), - message: error, - expand: true, - ), + AnimatedErrorContainer(padding: const EdgeInsets.only(bottom: defaultSpacing), message: _error, expand: true), FJElevatedButton( onTap: () async { const url = "https://liphium.com/legal/terms"; if (await canLaunchUrl(Uri.parse(url))) { final result = await launchUrl(Uri.parse(url)); if (result) { - clicked.value = true; + _clicked.value = true; return; } } - error.value = "setup.policy.error".tr; + _error.value = "setup.policy.error".tr; }, - child: Center( - child: Text( - "View agreements", - style: Get.textTheme.labelLarge, - textAlign: TextAlign.center, - ), - ), + child: Center(child: Text("View agreements", style: Get.textTheme.labelLarge, textAlign: TextAlign.center)), ), - Obx( - () => Animate( - effects: [ - ExpandEffect( - axis: Axis.vertical, - curve: scaleAnimationCurve, - duration: 500.ms, - ), - FadeEffect( - duration: 500.ms, - ) - ], - target: clicked.value ? 1 : 0, - child: Padding( - padding: const EdgeInsets.only(top: defaultSpacing), - child: FJElevatedButton( - onTap: () async { - final supportDir = await getApplicationSupportDirectory(); - - // Add a file to document that the privacy policy has been accepted - final file = await File(path.join(supportDir.path, agreeFile)).create(); - await file.writeAsString(widget.versionToWrite); - await setupManager.next(); - }, - child: Center(child: Text("accept".tr, style: Get.textTheme.labelLarge)), - ), + Animate( + effects: [ + ExpandEffect(axis: Axis.vertical, curve: scaleAnimationCurve, duration: 500.ms), + FadeEffect(duration: 500.ms), + ], + target: _clicked.value ? 1 : 0, + child: Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: FJElevatedButton( + onTap: () async { + final supportDir = await getApplicationSupportDirectory(); + + // Add a file to document that the privacy policy has been accepted + final file = await File(path.join(supportDir.path, agreeFile)).create(); + await file.writeAsString(widget.versionToWrite); + await setupManager.next(); + }, + child: Center(child: Text("accept".tr, style: Get.textTheme.labelLarge)), ), ), ), diff --git a/lib/pages/status/setup/server_setup.dart b/lib/pages/status/setup/server_setup.dart index 8337d69e..663f96d9 100644 --- a/lib/pages/status/setup/server_setup.dart +++ b/lib/pages/status/setup/server_setup.dart @@ -10,6 +10,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; const apiVersion = "v1"; @@ -41,10 +42,20 @@ class ServerSelectorPage extends StatefulWidget { } class _ServerSelectorPageState extends State { - final _error = "".obs; - final _loading = false.obs; + // Controller for the server name final TextEditingController _name = TextEditingController(); + // State + final _error = signal(""); + final _loading = signal(false); + + @override + void dispose() { + _error.dispose(); + _loading.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( @@ -52,19 +63,11 @@ class _ServerSelectorPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text( - "setup.choose.town".tr, - style: Get.textTheme.headlineMedium, - textAlign: TextAlign.center, - ), + Text("setup.choose.town".tr, style: Get.textTheme.headlineMedium, textAlign: TextAlign.center), verticalSpacing(sectionSpacing), - Text( - "setup.choose.town.desc".tr, - style: Get.textTheme.bodyMedium, - ), + Text("setup.choose.town.desc".tr, style: Get.textTheme.bodyMedium), verticalSpacing(defaultSpacing), FJElevatedLoadingButton( - loading: false.obs, onTap: () async { await launchUrl(Uri.parse("https://liphium.com/docs/concepts/towns")); }, @@ -73,27 +76,20 @@ class _ServerSelectorPageState extends State { verticalSpacing(sectionSpacing), Align( alignment: Alignment.centerLeft, - child: Text( - "setup.choose.town.selector".tr, - style: Get.textTheme.titleMedium, - ), + child: Text("setup.choose.town.selector".tr, style: Get.textTheme.titleMedium), ), verticalSpacing(defaultSpacing), - FJTextField( - controller: _name, - hintText: "placeholder.domain".tr, - ), + FJTextField(controller: _name, hintText: "placeholder.domain".tr), verticalSpacing(defaultSpacing), - AnimatedErrorContainer( - padding: const EdgeInsets.only(bottom: defaultSpacing), - message: _error, - expand: true, - ), + AnimatedErrorContainer(padding: const EdgeInsets.only(bottom: defaultSpacing), message: _error, expand: true), FJElevatedLoadingButton( loading: _loading, onTap: () async { _loading.value = true; - final json = await postAny("${formatPath(_name.text)}/pub", {}); // Send a request to get the public key (good test ig) + final json = await postAny( + "${formatPath(_name.text)}/pub", + {}, + ); // Send a request to get the public key (good test ig) _loading.value = false; if (json["pub"] == null) { _error.value = "server.not_found".tr; diff --git a/lib/pages/status/setup/settings_setup.dart b/lib/pages/status/setup/settings_setup.dart index c37b1988..8d26276f 100644 --- a/lib/pages/status/setup/settings_setup.dart +++ b/lib/pages/status/setup/settings_setup.dart @@ -15,18 +15,18 @@ class SettingsSetup extends Setup { @override Future load() async { - SettingController controller = Get.find(); - // Load all settings - for (var setting in controller.settings.values) { + for (var setting in SettingController.settings.values) { await setting.grabFromDb(); } // Set current language - await Get.updateLocale(GeneralSettings.languages[controller.settings[GeneralSettings.language]!.getValue()].locale); + await Get.updateLocale( + GeneralSettings.languages[SettingController.settings[GeneralSettings.language]!.getValue()].locale, + ); // Changes the color theme - Get.find().changeTheme(getThemeData()); + ThemeManager.changeTheme(getThemeData()); // Initialize the tabletop settings await TabletopSettings.initSettings(); @@ -35,7 +35,7 @@ class SettingsSetup extends Setup { if (!isWeb) { final list = await LogManager.loggingDirectory!.list().toList(); list.sort((a, b) => a.statSync().modified.compareTo(b.statSync().modified)); - var index = Get.find().settings[LogSettings.amountOfLogs]!.getValue() as double; + var index = SettingController.settings[LogSettings.amountOfLogs]!.getValue() as double; for (final file in list) { if (index <= 0) { await file.delete(); diff --git a/lib/pages/status/setup/setup_manager.dart b/lib/pages/status/setup/setup_manager.dart index 5ab81574..689949c7 100644 --- a/lib/pages/status/setup/setup_manager.dart +++ b/lib/pages/status/setup/setup_manager.dart @@ -11,7 +11,6 @@ import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; import 'package:chat_interface/pages/status/setup/instance_setup.dart'; import 'package:chat_interface/pages/status/setup/settings_setup.dart'; import 'package:chat_interface/pages/status/setup/server_setup.dart'; -import 'package:chat_interface/pages/status/setup/updates_setup.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -34,7 +33,6 @@ class SetupManager { static bool setupFinished = false; final _steps = []; int current = -1; - final message = 'setup.loading'.obs; SmoothDialogController? controller; SetupManager() { @@ -44,9 +42,6 @@ class SetupManager { if (!isWeb) { _steps.add(PolicySetup()); } - if (GetPlatform.isWindows) { - _steps.add(UpdateSetup()); - } _steps.add(InstanceSetup()); _steps.add(ServerSetup()); _steps.add(SettingsSetup()); @@ -85,7 +80,6 @@ class SetupManager { } sendLog(setup.name); - message.value = setup.name; Widget? ready; if (isDebug) { @@ -115,7 +109,7 @@ class SetupManager { } controller = null; unawaited(Get.offAll(getChatPage(), transition: Transition.fade, duration: const Duration(milliseconds: 500))); - unawaited(Get.find().tryConnection()); + unawaited(ConnectionController.tryConnection()); } } diff --git a/lib/pages/status/setup/setup_page.dart b/lib/pages/status/setup/setup_page.dart index 02d169b1..181e9e18 100644 --- a/lib/pages/status/setup/setup_page.dart +++ b/lib/pages/status/setup/setup_page.dart @@ -26,16 +26,13 @@ class _SetupPageState extends State { @override void dispose() { setupManager.controller = null; + _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return Center( - child: SizedBox( - child: SmoothDialog(controller: _controller), - ), - ); + return Center(child: SizedBox(child: SmoothDialog(controller: _controller))); } } @@ -47,8 +44,6 @@ class SetupLoadingWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - child: Text(text.tr, style: Get.textTheme.headlineMedium), - ); + return Center(child: Text(text.tr, style: Get.textTheme.headlineMedium)); } } diff --git a/lib/pages/status/setup/smooth_dialog.dart b/lib/pages/status/setup/smooth_dialog.dart index 85b19d6e..cbb8512f 100644 --- a/lib/pages/status/setup/smooth_dialog.dart +++ b/lib/pages/status/setup/smooth_dialog.dart @@ -7,17 +7,19 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SmoothDialogController { - final widgetOne = Rx(null); - final widgetTwo = Rx(null); + final Duration duration; + final widgetOne = signal(null); + final widgetTwo = signal(null); late AnimationController _one, _two; Future? transitionComplete; var keyOne = Random().nextDouble(); var keyTwo = Random().nextDouble(); bool direction = true; - SmoothDialogController(Widget child) { + SmoothDialogController(Widget child, {this.duration = const Duration(milliseconds: 750)}) { widgetTwo.value = child; } @@ -26,24 +28,30 @@ class SmoothDialogController { _two = two; } + /// Dispose the signals powering the smooth dialog controller. + void dispose() { + widgetOne.dispose(); + widgetTwo.dispose(); + } + static const curve = Curves.easeInOutQuart; Future transitionTo(Widget widget) async { await transitionComplete; direction = !direction; if (direction) { _two.value = 0; - unawaited(_two.animateTo(1, duration: 750.ms, curve: curve)); + unawaited(_two.animateTo(1, duration: duration, curve: curve)); _one.value = 1; - unawaited(_one.animateBack(0.0, duration: 750.ms, curve: curve)); + unawaited(_one.animateBack(0.0, duration: duration, curve: curve)); widgetTwo.value = widget; } else { _one.value = 0; - unawaited(_one.animateTo(1, duration: 750.ms, curve: curve)); + unawaited(_one.animateTo(1, duration: duration, curve: curve)); _two.value = 1; - unawaited(_two.animateBack(0.0, duration: 750.ms, curve: curve)); + unawaited(_two.animateBack(0.0, duration: duration, curve: curve)); widgetOne.value = widget; } - transitionComplete = Future.delayed(750.ms); + transitionComplete = Future.delayed(duration); } Future transitionToContinuos(Widget widget) async { @@ -53,10 +61,10 @@ class SmoothDialogController { widgetOne.value = widgetToClone; widgetTwo.value = widget; _two.value = 0; - unawaited(_two.animateTo(1, duration: 750.ms, curve: curve)); + unawaited(_two.animateTo(1, duration: duration, curve: curve)); _one.value = 1; - unawaited(_one.animateBack(0.0, duration: 750.ms, curve: curve)); - transitionComplete = Future.delayed(750.ms); + unawaited(_one.animateBack(0.0, duration: duration, curve: curve)); + transitionComplete = Future.delayed(duration); } } @@ -111,23 +119,14 @@ class _SmoothDialogState extends State with TickerProviderStateMix controller: _one, autoPlay: false, effects: [ - ExpandEffect( - axis: Axis.vertical, - alignment: Alignment.bottomCenter, - ), - const BlurEffect( - begin: Offset(5, 5), - end: Offset(0, 0), - ), - const ScaleEffect( - begin: Offset(0.5, 0.5), - end: Offset(1, 1), - ), + ExpandEffect(axis: Axis.vertical, alignment: Alignment.bottomCenter), + const BlurEffect(begin: Offset(5, 5), end: Offset(0, 0)), + const ScaleEffect(begin: Offset(0.5, 0.5), end: Offset(1, 1)), ], child: Padding( padding: const EdgeInsets.all(sectionSpacing), - child: Obx( - () => SizedBox( + child: Watch( + (ctx) => SizedBox( key: ValueKey(widget.controller.keyOne), child: widget.controller.widgetOne.value ?? const SizedBox(), ), @@ -138,26 +137,15 @@ class _SmoothDialogState extends State with TickerProviderStateMix controller: _two, autoPlay: false, effects: [ - ExpandEffect( - axis: Axis.vertical, - alignment: Alignment.topCenter, - ), - const BlurEffect( - begin: Offset(5, 5), - end: Offset(0, 0), - ), - const ScaleEffect( - begin: Offset(0.5, 0.5), - end: Offset(1, 1), - ), + ExpandEffect(axis: Axis.vertical, alignment: Alignment.topCenter), + const BlurEffect(begin: Offset(5, 5), end: Offset(0, 0)), + const ScaleEffect(begin: Offset(0.5, 0.5), end: Offset(1, 1)), ], child: Padding( padding: const EdgeInsets.all(sectionSpacing), - child: Obx( - () => SizedBox( - key: ValueKey(widget.controller.keyTwo), - child: widget.controller.widgetTwo.value!, - ), + child: Watch( + (ctx) => + SizedBox(key: ValueKey(widget.controller.keyTwo), child: widget.controller.widgetTwo.value!), ), ), ), @@ -173,10 +161,7 @@ class _SmoothDialogState extends State with TickerProviderStateMix class SmoothDialogWindow extends StatefulWidget { final SmoothDialogController controller; - const SmoothDialogWindow({ - super.key, - required this.controller, - }); + const SmoothDialogWindow({super.key, required this.controller}); @override State createState() => _SmoothDialogWindowState(); @@ -222,23 +207,14 @@ class _SmoothDialogWindowState extends State with TickerProv controller: _one, autoPlay: false, effects: [ - ExpandEffect( - axis: Axis.vertical, - alignment: Alignment.bottomCenter, - ), - const BlurEffect( - begin: Offset(5, 5), - end: Offset(0, 0), - ), - const ScaleEffect( - begin: Offset(0.5, 0.5), - end: Offset(1, 1), - ), + ExpandEffect(axis: Axis.vertical, alignment: Alignment.bottomCenter), + const BlurEffect(begin: Offset(5, 5), end: Offset(0, 0)), + const ScaleEffect(begin: Offset(0.5, 0.5), end: Offset(1, 1)), ], child: Padding( padding: const EdgeInsets.all(sectionSpacing), - child: Obx( - () => SizedBox( + child: Watch( + (ctx) => SizedBox( key: ValueKey(widget.controller.keyOne), child: widget.controller.widgetOne.value ?? const SizedBox(), ), @@ -249,26 +225,15 @@ class _SmoothDialogWindowState extends State with TickerProv controller: _two, autoPlay: false, effects: [ - ExpandEffect( - axis: Axis.vertical, - alignment: Alignment.topCenter, - ), - const BlurEffect( - begin: Offset(5, 5), - end: Offset(0, 0), - ), - const ScaleEffect( - begin: Offset(0.5, 0.5), - end: Offset(1, 1), - ), + ExpandEffect(axis: Axis.vertical, alignment: Alignment.topCenter), + const BlurEffect(begin: Offset(5, 5), end: Offset(0, 0)), + const ScaleEffect(begin: Offset(0.5, 0.5), end: Offset(1, 1)), ], child: Padding( padding: const EdgeInsets.all(sectionSpacing), - child: Obx( - () => SizedBox( - key: ValueKey(widget.controller.keyTwo), - child: widget.controller.widgetTwo.value!, - ), + child: Watch( + (ctx) => + SizedBox(key: ValueKey(widget.controller.keyTwo), child: widget.controller.widgetTwo.value!), ), ), ), @@ -283,10 +248,7 @@ class _SmoothDialogWindowState extends State with TickerProv class SmoothBox extends StatefulWidget { final SmoothDialogController controller; - const SmoothBox({ - super.key, - required this.controller, - }); + const SmoothBox({super.key, required this.controller}); @override State createState() => _SmoothBoxState(); @@ -324,18 +286,11 @@ class _SmoothBoxState extends State with TickerProviderStateMixin { controller: _one, autoPlay: false, effects: [ - ExpandEffect( - axis: Axis.vertical, - alignment: Alignment.bottomCenter, - ), - const FadeEffect( - begin: 0, - end: 1, - curve: Curves.linear, - ), + ExpandEffect(axis: Axis.vertical, alignment: Alignment.bottomCenter), + const FadeEffect(begin: 0, end: 1, curve: Curves.linear), ], - child: Obx( - () => SizedBox( + child: Watch( + (ctx) => SizedBox( key: ValueKey(widget.controller.keyOne), child: widget.controller.widgetOne.value ?? const SizedBox(), ), @@ -345,20 +300,11 @@ class _SmoothBoxState extends State with TickerProviderStateMixin { controller: _two, autoPlay: false, effects: [ - ExpandEffect( - axis: Axis.vertical, - alignment: Alignment.topCenter, - ), - const FadeEffect( - begin: 0, - end: 1, - ), + ExpandEffect(axis: Axis.vertical, alignment: Alignment.topCenter), + const FadeEffect(begin: 0, end: 1), ], - child: Obx( - () => SizedBox( - key: ValueKey(widget.controller.keyTwo), - child: widget.controller.widgetTwo.value!, - ), + child: Watch( + (ctx) => SizedBox(key: ValueKey(widget.controller.keyTwo), child: widget.controller.widgetTwo.value!), ), ), ], @@ -382,9 +328,7 @@ class _SmoothDialogTestState extends State { controller.transitionTo( Builder( builder: (context) { - return SetupLoadingWidget( - text: "Hi ${Random().nextDouble().toStringAsFixed(2)}", - ); + return SetupLoadingWidget(text: "Hi ${Random().nextDouble().toStringAsFixed(2)}"); }, ), ); @@ -419,9 +363,7 @@ class _SmoothDialogBoxTestState extends State { controller.transitionTo( Builder( builder: (context) { - return SetupLoadingWidget( - text: "Hi ${Random().nextDouble().toStringAsFixed(2)}", - ); + return SetupLoadingWidget(text: "Hi ${Random().nextDouble().toStringAsFixed(2)}"); }, ), ); diff --git a/lib/pages/status/setup/tokens_setup.dart b/lib/pages/status/setup/tokens_setup.dart index 67694bc3..abaff06d 100644 --- a/lib/pages/status/setup/tokens_setup.dart +++ b/lib/pages/status/setup/tokens_setup.dart @@ -11,7 +11,6 @@ import 'package:chat_interface/theme/components/ssr/ssr.dart'; import 'package:chat_interface/util/web.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; class TokensSetup extends Setup { TokensSetup() : super("loading.tokens", false); @@ -41,31 +40,35 @@ class TokensSetup extends Setup { ); // Start the SSR process - unawaited(ssr.start( - extra: { - "/account/auth/form": ServerSelectorContainer( - onSelected: () { - setupManager.retry(); - }, - ), - }, - ).then((error) async { - // You shall waste 750ms of your life to witness this amazing animation better - await Future.delayed(const Duration(milliseconds: 750)); + unawaited( + ssr + .start( + extra: { + "/account/auth/form": ServerSelectorContainer( + onSelected: () { + setupManager.retry(); + }, + ), + }, + ) + .then((error) async { + // You shall waste 750ms of your life to witness this amazing animation better + await Future.delayed(const Duration(milliseconds: 750)); - // Return error (in here cause cool animation) - if (error != null) { - setupManager.error(error); - } - })); + // Return error (in here cause cool animation) + if (error != null) { + setupManager.error(error); + } + }), + ); return const SetupLoadingWidget(text: "rendering"); } // Load account stuff from settings StatusController.ownAccountId = await retrieveEncryptedValue("cache_account_id") ?? ""; - Get.find().name.value = await retrieveEncryptedValue("cache_account_uname") ?? ""; - Get.find().displayName.value = await retrieveEncryptedValue("cache_account_dname") ?? ""; + StatusController.name.value = await retrieveEncryptedValue("cache_account_uname") ?? ""; + StatusController.displayName.value = await retrieveEncryptedValue("cache_account_dname") ?? ""; // Init file paths with account id await AttachmentController.initFilePath(StatusController.ownAccountId); diff --git a/lib/pages/status/setup/updates_setup.dart b/lib/pages/status/setup/updates_setup.dart deleted file mode 100644 index 265f8a0c..00000000 --- a/lib/pages/status/setup/updates_setup.dart +++ /dev/null @@ -1,345 +0,0 @@ -import 'dart:io'; - -import 'package:archive/archive_io.dart'; -import 'package:chat_interface/main.dart'; -import 'package:chat_interface/pages/status/setup/setup_manager.dart'; -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; - -class UpdateSetup extends Setup { - UpdateSetup() : super('loading.update', false); - - @override - Future load() async { - if (!checkVersion || isDebug) { - return null; - } - - var location = await getApplicationSupportDirectory(); - location = Directory(path.join(location.path, "versions/")); - await location.create(); - - final release = await fetchReleaseDataFor("Liphium", "chat_interface"); - if (release == null) { - sendLog("Error with GitHub API, using current version"); - return null; - } - - // Check current version - final entities = await location.list().toList(); - if (entities.isEmpty) { - // Update - sendLog("install required"); - return _updatePage(release); - } - - // Delete any leftover stuff - bool downloadFile = false; - if (entities.length >= 2) { - for (var entity in entities) { - final name = path.basename(entity.path); - if (name.endsWith(".zip")) { - downloadFile = true; - } - } - } - - // Find the current version - String? foundPath; - int versionsFound = 0; - for (var entity in entities) { - final name = path.basename(entity.path); - final type = await FileSystemEntity.type(entity.path); - if (type == FileSystemEntityType.directory) { - versionsFound++; - } - if (name == release.version) { - foundPath = entity.path; - } - } - - // If the newest version wasn't found, download it - if (foundPath == null) { - return _updatePage(release); - } - - // If the most recent version is running, install or run normally - if (Platform.resolvedExecutable.contains(release.version)) { - // Check if installation is needed - if (versionsFound > 1 || downloadFile) { - return _installPage(release); - } - - return null; - } - - // If not, run the latest version instead of the current one - await Future.delayed(Duration(milliseconds: 500)); // Just in case this becomes an infinite loop - await restartProcessAsAdmin(path: path.join(foundPath, "chat_interface.exe")); - await SystemNavigator.pop(); - return null; - } -} - -Widget _updatePage(ReleaseData data) { - return ShouldUpdateSetupPage( - title: "Updating..", - callback: (value, data) { - updateApp(value, data); - }, - data: data, - ); -} - -Widget _installPage(ReleaseData data) { - return ShouldUpdateSetupPage( - title: "Installing..", - callback: (value, data) { - installApp(value, data); - }, - data: data, - ); -} - -class ReleaseData { - final String version; - final String body; - final String downloadUrl; - - ReleaseData(this.version, this.body, this.downloadUrl); -} - -/// Install the current version of the app -Future installApp(RxString status, ReleaseData data) async { - String step = "admin"; - try { - // Run with admin privilege on windows - if (Platform.isWindows && !executableArguments.contains("--update")) { - await restartProcessAsAdmin(); - await Future.delayed(3.seconds); - exit(0); - } - - // Wait for the other process to exit (potentially) - status.value = "Preparing.."; - await Future.delayed(const Duration(seconds: 5)); - - // Get the path for the general support folder - final location = await getApplicationSupportDirectory(); - - // Delete older versions - status.value = "Cleaning up.."; - step = "del old versions"; - final versionsDir = Directory(path.join(location.path, "versions")); - for (var version in (await versionsDir.list().toList())) { - final type = await FileSystemEntity.type(version.path); - if (type == FileSystemEntityType.directory) { - if (path.basename(version.path) != data.version) { - step = "del ${path.basename(version.path)}"; - // Try to delete, otherwise return an error - await version.delete(recursive: true); - } - } - - // Delete the download.zip file (and any others cause doesn't matter) - if (version.path.endsWith(".zip")) { - await version.delete(); - } - } - - // Create an application shortcut (for the start menu) - status.value = "Adding app.."; - step = "add to start"; - if (GetPlatform.isWindows) { - final shortcutPath = path.join("C:/ProgramData/Microsoft/Windows/Start Menu/Programs", "Liphium.lnk"); - - // Execute a powershell command to create a shortcut (dart doesn't have support for this..) - step = "execute powershell command"; - final powerShellCommand = ''' - \$targetPath = "${path.join(location.path, "versions", data.version, "chat_interface.exe")}" - \$shortcutPath = "$shortcutPath" - \$wshShell = New-Object -ComObject WScript.Shell - \$shortcut = \$wshShell.CreateShortcut(\$shortcutPath) - \$shortcut.TargetPath = \$targetPath - \$shortcut.Save() - '''; - - final result = await Process.run( - 'powershell', - ['-Command', powerShellCommand], - ); - - if (result.exitCode != 0) { - status.value = "Shortcut couldn't be created (${result.exitCode})"; - } - } - - // Restart the setup - await Future.delayed(const Duration(seconds: 3)); - setupManager.retry(); - } catch (e) { - status.value = "Error during installation ($step): $e"; - } -} - -/// Get the release data for a project from GitHub -Future fetchReleaseDataFor(String owner, String repo) async { - final res = await dio.get("https://api.github.com/repos/$owner/$repo/releases/latest", options: Options(validateStatus: (s) => true)); - if (res.statusCode != 200) { - return null; - } - - var searchTerm = "linux.zip"; - if (Platform.isWindows) { - searchTerm = "windows.zip"; - } - - for (var asset in res.data["assets"]) { - if (asset["name"] == searchTerm) { - sendLog(asset); - return ReleaseData(res.data["tag_name"], res.data["body"], asset["browser_download_url"]); - } - } - - return null; -} - -/// Download and extract the executable as well as add it to the versions folder -Future updateApp(RxString status, ReleaseData data) async { - try { - // Run with admin privilege on windows - if (Platform.isWindows && !executableArguments.contains("--update")) { - status.value = "Re-running with admin privilege.."; - await restartProcessAsAdmin(); - exit(0); - } - - // Wait quickly - status.value = "Preparing.."; - await Future.delayed(const Duration(seconds: 5)); - - // Get the path for the versions folder - var location = await getApplicationSupportDirectory(); - location = Directory(path.join(location.path, "versions")); - - // Download the version - final res = await dio.download( - data.downloadUrl, - path.join(location.path, "download.zip"), - onReceiveProgress: (count, total) { - status.value = "Downloading ${((count / total) * 100.0).toStringAsFixed(1)}%.."; - }, - options: Options( - validateStatus: (status) => true, - ), - ); - - // Check if download was successful - if (res.statusCode != 200) { - status.value = "Couldn't download from GitHub"; - return false; - } - - // Extract the downloaded archive into the versions folder - status.value = "Extracting.."; - final dir = await Directory(path.join(location.path, data.version)).create(); - await extractFileToDisk(path.join(location.path, "download.zip"), dir.path, asyncWrite: true); - - // Restart the app - status.value = "Restarting.."; - - if (GetPlatform.isWindows) { - // Search for the executable - final directory = Directory(path.join(location.path, data.version)); - final entities = await directory.list().toList(); - late final FileSystemEntity executable; - for (var entity in entities) { - if (path.basename(entity.path).endsWith(".exe")) { - executable = entity; - } - } - - sendLog("restarting with path: ${executable.path}"); - await restartProcessAsAdmin(path: executable.path); - } - - exit(0); - } catch (e) { - status.value = "There was an error during the update: $e"; - return false; - } -} - -Future restartProcessAsAdmin({String? path}) async { - sendLog(Platform.resolvedExecutable); - await Process.run( - 'powershell', - ['Start-Process "${path ?? Platform.resolvedExecutable}" -ArgumentList "--update" -Verb RunAs'], - ); - return true; -} - -Directory getDesktopDirectory() { - String home = ""; - Map envVars = Platform.environment; - if (Platform.isMacOS) { - home = envVars['HOME']!; - } else if (Platform.isLinux) { - home = envVars['HOME']!; - } else if (Platform.isWindows) { - home = envVars["UserProfile"]!; - final exists = Directory(path.join(home, "Desktop")).existsSync(); - if (!exists) { - home = path.join(home, "OneDrive"); - } - } - - return Directory(path.join(home, "Desktop")); -} - -class ShouldUpdateSetupPage extends StatefulWidget { - final String title; - final Function(RxString, ReleaseData data) callback; - final ReleaseData data; - - const ShouldUpdateSetupPage({ - super.key, - required this.title, - required this.callback, - required this.data, - }); - - @override - State createState() => _ShouldUpdateSetupPageState(); -} - -class _ShouldUpdateSetupPageState extends State { - @override - void initState() { - super.initState(); - widget.callback.call(_status, widget.data); - } - - final _status = "".obs; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 1000), - Text(widget.title, style: Get.theme.textTheme.headlineMedium), - verticalSpacing(sectionSpacing), - Obx( - () => Text(_status.value, style: Get.theme.textTheme.labelLarge), - ), - ], - ); - } -} diff --git a/lib/services/chat/conversation_member.dart b/lib/services/chat/conversation_member.dart new file mode 100644 index 00000000..836da28c --- /dev/null +++ b/lib/services/chat/conversation_member.dart @@ -0,0 +1,125 @@ +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; +import 'package:chat_interface/util/web.dart'; + +class Member { + final LPHAddress tokenId; // Token id + final LPHAddress address; // Account id + final MemberRole role; + + Member(this.tokenId, this.address, this.role); + Member.unknown(this.address) : tokenId = LPHAddress.error(), role = MemberRole.user; + Member.fromJson(Map json) + : tokenId = LPHAddress.from(json['id']), + address = LPHAddress.from(json['address']), + role = MemberRole.fromValue(json['role']); + + Member.fromData(MemberData data) + : this( + LPHAddress.from(data.id), + LPHAddress.from(fromDbEncrypted(data.accountId)), + MemberRole.fromValue(data.roleId), + ); + + MemberData toData(LPHAddress conversation) => MemberData( + id: tokenId.encode(), + accountId: dbEncrypted(address.encode()), + roleId: role.value, + conversationId: conversation.encode(), + ); + + Friend getFriend() { + if (StatusController.ownAddress == address) return Friend.me(); + return FriendController.friends[address] ?? Friend.unknown(address); + } + + /// Promote a member to the next available role. + /// + /// Returns an error if there was one. + Future promote(Conversation conversation) async { + final json = await postNodeJSON("/conversations/promote_token", { + "token": conversation.token.toMap(conversation.id), + "data": tokenId.encode(), + }); + + // Check if there was an error + if (!json["success"]) { + return json["error"]; + } + return null; + } + + /// Demote a member to the next available role. + /// + /// Returns an error if there was one. + Future demote(Conversation conversation) async { + final json = await postNodeJSON("/conversations/demote_token", { + "token": conversation.token.toMap(conversation.id), + "data": tokenId.encode(), + }); + + // Check if there was an error + if (!json["success"]) { + return json["error"]; + } + return null; + } + + /// Kick a member from the conversation. + /// + /// Returns an error if there was one. + Future remove(Conversation conversation) async { + // Kick the member's token out of the conversation + final json = await postNodeJSON("/conversations/kick_member", { + "token": conversation.token.toMap(conversation.id), + "data": tokenId.encode(), + }); + + // Check if there was an error + if (!json["success"]) { + return json["error"]; + } + return null; + } +} + +enum MemberRole { + admin(2), + moderator(1), + user(0); + + final int value; + + const MemberRole(this.value); + + bool lowerOrEqual(MemberRole role) { + return value <= role.value; + } + + bool higherOrEqual(MemberRole role) { + return value >= role.value; + } + + bool higherThan(MemberRole role) { + return value > role.value; + } + + bool lowerThan(MemberRole role) { + return value < role.value; + } + + static MemberRole fromValue(int value) { + switch (value) { + case 2: + return MemberRole.admin; + case 1: + return MemberRole.moderator; + case 0: + return MemberRole.user; + } + return MemberRole.user; + } +} diff --git a/lib/services/chat/conversation_message_provider.dart b/lib/services/chat/conversation_message_provider.dart new file mode 100644 index 00000000..295ef839 --- /dev/null +++ b/lib/services/chat/conversation_message_provider.dart @@ -0,0 +1,296 @@ +import 'dart:async'; + +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/conversation/message_controller.dart'; +import 'package:chat_interface/controller/conversation/message_provider.dart'; +import 'package:chat_interface/controller/conversation/system_messages.dart'; +import 'package:chat_interface/controller/current/connection_controller.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/connection/chat/message_listener.dart'; +import 'package:chat_interface/theme/ui/conversation_util.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sodium_libs/sodium_libs.dart'; + +/// A message provider that loads messages from a conversation. +class ConversationMessageProvider extends MessageProvider { + final Conversation conversation; + final String extra; + ConversationMessageProvider(this.conversation, {this.extra = ""}); + + /// Generate a new value key related to this conversation and extra. + String getKey(String identifier) => "${conversation.id.encode()}-$extra-$identifier"; + + @override + Future<(List?, bool)> loadMessagesBefore(int time) async { + // Load messages from the local database + final messageQuery = + db.select(db.message) + ..where((tbl) => tbl.conversation.equals(ConversationService.withExtra(conversation.id.encode(), extra))) + ..where((tbl) => tbl.createdAt.isSmallerThanValue(BigInt.from(time))) + ..orderBy([(u) => OrderingTerm.desc(u.createdAt), (u) => OrderingTerm.asc(u.id)]) + ..limit(30); + final loadedMessages = await messageQuery.get(); + var processed = await _processMessages(loadedMessages); + + // If there aren't enough messages, load more from the server + if (loadedMessages.length != 30) { + // Check if the user is even connected to the server (to make sure offline retrieval works) + if (!ConnectionController.connected.value) { + // Act like the top has been reached + return (processed, false); + } + + // Load messages from the server + final json = await postNodeJSON("/conversations/message/list_before", { + "token": conversation.token.toMap(conversation.id), + "data": {"extra": extra, "before": loadedMessages.isEmpty ? time : loadedMessages.last.createdAt.toInt()}, + }); + + // Check if there was an error + if (!json["success"]) { + conversation.error.value = json["error"]; + return (null, true); + } + + // Check if the top has been reached + if (json["messages"] == null || json["messages"].isEmpty) { + return (processed, false); + } + // Unpack the messages in an isolate + final messagesFromServer = + (await MessageListener.unpackMessagesInIsolate(conversation, json["messages"])).map((msg) => msg.$1).toList(); + + sendLog(messagesFromServer.length); + + // Prepare and add to the messages from the client + await initAttachmentsForMessages(messagesFromServer); + processed += messagesFromServer; + } + + // Process the messages in a seperate isolate + return (processed, false); + } + + @override + Future<(List?, bool)> loadMessagesAfter(int time) async { + // Load messages from the local database + final messageQuery = + db.select(db.message) + ..where((tbl) => tbl.conversation.equals(ConversationService.withExtra(conversation.id.encode(), extra))) + ..where((tbl) => tbl.createdAt.isBiggerThanValue(BigInt.from(time))) + ..orderBy([(u) => OrderingTerm.asc(u.createdAt), (u) => OrderingTerm.asc(u.id)]) + ..limit(10); + final messages = await messageQuery.get(); + + // Process the messages in a seperate isolate + return (await _processMessages(messages), false); + } + + @override + Future loadMessageFromServer(String id, {bool init = true}) async { + // Get the message from the local database + // Load messages from the local database + final messageQuery = + db.select(db.message) + ..where((tbl) => tbl.conversation.equals(ConversationService.withExtra(conversation.id.encode(), extra))) + ..where((tbl) => tbl.id.equals(id)) + ..limit(1); + final message = await messageQuery.getSingleOrNull(); + if (message == null) { + // Check if the user is even connected to the server (to make sure offline retrieval works) + if (!ConnectionController.connected.value) { + // Act like the message doesn't exist + return null; + } + + // Get the message from the server + final json = await postNodeJSON("/conversations/message/get", { + "token": conversation.token.toMap(conversation.id), + "data": id, + }); + + // Check if there is an error + if (!json["success"]) { + sendLog("error fetching message $id: ${json["error"]}"); + return null; + } + + // Parse message and init attachments (if desired) + final message = await MessageListener.unpackMessageInIsolate(conversation, json["message"]); + if (init) { + await message.initAttachments(this); + } + + return message; + } + + // Process message as a new list and grab it from the list when finished + return (await _processMessages([message], init: init))[0]; + } + + /// Process a message payload from the local database. + /// TODO: Possibly run this in an isolate in the future (needs really advanced code) + /// + /// For the future: TODO: Also process the signatures in the isolate by preloading profiles + Future> _processMessages(List messages, {bool init = true}) async { + if (messages.isEmpty) { + return []; + } + + // Process all messages + final list = []; + for (var data in messages) { + final (message, _) = decryptFromLocalDatabase(data, databaseKey); + + // Don't render system messages that shouldn't be rendered (this is only for safety, should never actually happen) + if (message.type == MessageType.system && SystemMessages.messages[message.content]?.render == false) { + continue; + } + + list.add(message); + } + + // Init the attachments to prepare the messages for rendering (if desired) + if (init) { + await initAttachmentsForMessages(list); + } + + return list; + } + + /// Init the attachments for all passed in messages. + Future initAttachmentsForMessages(List messages) async { + await Future.wait(messages.map((msg) => msg.initAttachments(this))); + return true; + } + + /// Decrypt a message from the local database. + /// + /// Returns message and conversation found in the local database. + static (Message, String) decryptFromLocalDatabase(MessageData data, SecureKey key, {Sodium? sodium}) { + // Create a new base message + final message = Message( + id: data.id, + type: MessageType.text, + content: decryptSymmetric(data.content, key, sodium), + answer: "", + attachments: [], + senderToken: LPHAddress.from(data.senderToken), + senderAddress: LPHAddress.from(decryptSymmetric(data.senderAddress, key, sodium)), + createdAt: DateTime.fromMillisecondsSinceEpoch(data.createdAt.toInt()), + edited: data.edited, + verified: data.verified, + ); + + // Set the type to system in case it is a system message + if (message.senderToken == MessageController.systemSender) { + message.type = MessageType.system; + message.loadContent(); + return (message, data.conversation); + } + + // Load the type, attachments, answer, .. from the content json + message.loadContent(); + + return (message, data.conversation); + } + + @override + Future deleteMessage(Message message) async { + // Check if the message is sent by the user + final token = ConversationController.conversations[conversation.id]!.token; + if (message.senderToken != token.id) { + return "no.permission".tr; + } + + // Send a request to the server + final json = await postNodeJSON("/conversations/message/delete", { + "token": token.toMap(conversation.id), + "data": message.id, + }); + + if (!json["success"]) { + return json["error"]; + } + + return null; + } + + @override + Future deleteMessageFromClient(String id) async { + messages.remove(id); + await db.message.deleteWhere((tbl) => tbl.id.equals(id)); + return true; + } + + @override + SecureKey encryptionKey() { + return conversation.key; + } + + @override + Future<(String, int)?> getTimestamp() async { + // Grab a new timestamp from the server + var json = await postNodeJSON("/conversations/timestamp", {"token": conversation.token.toMap(conversation.id)}); + if (!json["success"]) { + return null; + } + + // The stamp is first casted to a num to prevent an error (don't remove) + return (json["token"] as String, (json["stamp"] as num).toInt()); + } + + @override + Future handleMessageSend(String timeToken, String data, int stamp) async { + // Send message to the server with conversation token as authentication + final json = await postNodeJSON("/conversations/message/send", { + "token": conversation.token.toMap(conversation.id), + "data": {"token": timeToken, "data": data, "extra": extra}, + }); + if (!json["success"]) { + return json["error"]; + } + + return null; + } + + bool sendingRead = false; + + @override + Future handleBottomReached() async { + // Update read state when scrolled to the bottom + if (ConversationController.notificationMap[ConversationService.withExtra(conversation.id.encode(), extra)] != 0 && + !sendingRead) { + final newest = getNewestMessage(); + if (newest == null) { + return; + } + sendingRead = true; + await ConversationService.overwriteRead(conversation, messages[newest]!.createdAt.millisecondsSinceEpoch); + sendingRead = false; + } + } + + /// Get an appropriate icon for the current conversation. + IconData getIconForConversation() { + return ConversationUtil.getIconForConversation(conversation, extra: extra); + } + + /// Get an appropriate name for the current conversation. + String getNameForConversation() { + return ConversationUtil.getNameForConversation(conversation, extra: extra); + } + + /// Open the appropriate dialog for the conversation. + void openDialogForConversation(ContextMenuData data) { + return ConversationUtil.openDialogForConversation(conversation, data, extra: extra); + } +} diff --git a/lib/services/chat/conversation_service.dart b/lib/services/chat/conversation_service.dart new file mode 100644 index 00000000..81f64571 --- /dev/null +++ b/lib/services/chat/conversation_service.dart @@ -0,0 +1,704 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/controller/current/steps/account_step.dart'; +import 'package:chat_interface/controller/current/steps/key_step.dart'; +import 'package:chat_interface/controller/current/tasks/vault_sync_task.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/database/database_entities.dart' as model; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; +import 'package:chat_interface/services/chat/conversation_member.dart'; +import 'package:chat_interface/services/connection/chat/stored_actions_listener.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/util/constants.dart'; +import 'package:chat_interface/util/encryption/hash.dart'; +import 'package:chat_interface/util/encryption/signatures.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:drift/drift.dart' as drift; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; +import 'package:sodium_libs/sodium_libs.dart'; + +/// The container used for storing members of conversations on the server +class MemberContainer { + late final LPHAddress id; + + MemberContainer(this.id); + MemberContainer.fromJson(Map json) : id = LPHAddress.from(json["id"]); + + /// Decrypts a [MemberContainer] received from the server using the conversation key. + /// + /// [key] is the encryption key of the conversation. + MemberContainer.decrypt(String cipherText, SecureKey key) { + final json = jsonDecode(decryptSymmetric(cipherText, key)); + id = LPHAddress.from(json["id"]); + } + + /// Encrypt the member container for sending it to the server + String encrypted(SecureKey key) => encryptSymmetric(jsonEncode({"id": id.encode()}), key); +} + +/// The token for accessing a conversation and acting as a specific conversation member +class ConversationToken { + final LPHAddress id; + final String token; + + ConversationToken(this.id, this.token); + ConversationToken.fromJson(Map json) : id = LPHAddress.from(json["id"]), token = json["token"]; + + String toJson(LPHAddress conv) => jsonEncode(toMap(conv)); + Map toMap(LPHAddress conv) => { + "id": id.encode(), + "conv": conv.encode(), + "token": token, + "time": -1, + }; +} + +/// The container used for storing conversation data on the server. +/// +/// For now it only stores the [name] of the conversation. +class ConversationContainer { + late final String name; + + ConversationContainer(this.name); + ConversationContainer.fromJson(Map json) : name = json["name"]; + + factory ConversationContainer.decrypt(String cipherText, SecureKey key) { + return ConversationContainer.fromJson(jsonDecode(decryptSymmetric(cipherText, key))); + } + String encrypted(SecureKey key) => encryptSymmetric(jsonEncode(toJson()), key); + + Map toJson() => {"name": name}; +} + +/// The prefix for the conversation name used for all direct messages +const directMessagePrefix = "DM_"; + +class ConversationService extends VaultTarget { + ConversationService() : super(Constants.vaultConversationTag); + + @override + Future init() async { + final conversations = + await (db.select(db.conversation)..orderBy([(u) => drift.OrderingTerm.desc(u.updatedAt)])).get(); + final futures = >[]; + final order = List.filled(conversations.length, LPHAddress.error(), growable: true); + final map = {}; + int index = 0; + for (var conversation in conversations) { + // Make sure to handle the different types properly + Conversation conv; + switch (conversation.type) { + case model.ConversationType.square: + conv = Square.fromData(conversation); + default: + conv = Conversation.fromData(conversation); + } + + // Load the members and add to the order and map + futures.add(ConversationService.loadMembers(conv)); + map[conv.id] = conv; + order[index] = conv.id; + index++; + } + + // Wait for the members to be loaded and then update the UI + unawaited( + futures.wait.then((_) { + batch(() { + ConversationController.order.value = order; + ConversationController.conversations.value = map; + }); + }), + ); + } + + @override + Future processEntries(List deleted, List newEntries) async { + // Add all the new conversations to the vault + for (var entry in newEntries) { + // Make sure to handle both squares and conversations properly + final json = jsonDecode(entry.payload); + Conversation conv; + if (json["type"] == model.ConversationType.square.index) { + conv = Square.fromJson(json, entry.id); + } else { + conv = Conversation.fromJson(json, entry.id); + } + + // Add to the controller in case not there yet + if (ConversationController.conversations[conv.id] == null) { + await ConversationService.insertFromVault(conv); + } + } + + // Delete everything that's been deleted from the vault on the server + ConversationController.conversations.removeWhere((id, conv) { + if (deleted.contains(conv.vaultId)) { + ConversationService.delete(id, vaultId: conv.vaultId, deleteCache: false); + ConversationController.order.remove(id); + SidebarController.unselectConversation(id); + return true; + } + return false; + }); + + // Subscribe to conversations again + sendLog("vault sync completed"); + ConversationService.subscribeToConversations(); + } + + /// Open a direct message with a friend. + /// + /// The conversation is not null if there is one already. + /// The string is an error if there was one. + static Future<(Conversation?, String?)> openDirectMessage(Friend friend) async { + // Check if the conversation already exists + final conversation = ConversationController.conversations.values.firstWhere( + (element) => + element.type == model.ConversationType.directMessage && + element.members.values.any((element) => element.address == friend.id), + orElse: + () => Conversation( + LPHAddress.error(), + "", + model.ConversationType.directMessage, + ConversationToken(LPHAddress.error(), ""), + ConversationContainer(""), + "", + 0, + 0, + ConversationReads.fromContainer(""), + ), + ); + if (!conversation.id.isError()) { + return (conversation, null); + } + + // Open a new conversation with the friend + return ( + null, + await openConversation(model.ConversationType.directMessage, [ + friend, + ], ConversationContainer(directMessagePrefix + friend.id.id)), + ); + } + + /// Open a new group conversation. + /// + /// If the returned string is not null, it is an error message. + static Future openGroupConversation(List friends, String name) { + return openConversation(model.ConversationType.group, friends, ConversationContainer(name)); + } + + /// The underlying method for creating a conversation on the server. + static Future openConversation( + model.ConversationType type, + List friends, + ConversationContainer container, + ) async { + // Prepare the conversation + final conversationKey = randomSymmetricKey(); + final ownMemberContainer = MemberContainer(StatusController.ownAddress).encrypted(conversationKey); + final memberContainers = {}; + for (final friend in friends) { + final container = MemberContainer(friend.id); + memberContainers[friend.id] = container.encrypted(conversationKey); + } + final encryptedData = container.encrypted(conversationKey); + + // Create the conversation on the server + final body = await postNodeJSON("/conversations/open", { + "accountData": ownMemberContainer, + "members": memberContainers.values.toList(), + "type": type.index, + "data": encryptedData, + }); + if (!body["success"]) { + return body["error"]; + } + + // Put together the information all other members need + final packagedKey = packageSymmetricKey(conversationKey); + final convId = LPHAddress.from(body["conversation"]); + final conversation = Conversation( + convId, + "", + model.ConversationType.values[body["type"]], + ConversationToken.fromJson(body["admin_token"]), + container, + packagedKey, + 0, + DateTime.now().millisecondsSinceEpoch, + ConversationReads.fromContainer(""), + ); + + // Send all other members their information and credentials + for (var friend in friends) { + // Get the token for the member + final token = ConversationToken.fromJson(body["tokens"][hashSha(memberContainers[friend.id]!)]); + + // Send them an authenticated stored action and add the member to the list + final error = await sendAuthenticatedStoredAction( + friend, + _conversationPayload(convId, token, packagedKey, friend), + ); + if (error != null) { + // Handle invitation failure gracefully + if (conversation.type == model.ConversationType.directMessage) { + // In case of a direct message, delete it because we don't wanna be in there alone + unawaited(delete(convId, token: conversation.token)); + return error; + } else { + // In case of a group conversation or square, simply remove the person + final member = Member(token.id, friend.id, MemberRole.user); + unawaited(member.remove(conversation)); + } + } + } + + // Add to vault + final (error, _) = await addToVault(Constants.vaultConversationTag, conversation.toJson()); + if (error != null) { + sendLog("WARNING: Conversation couldn't be added to vault: $error"); + } + + return null; + } + + /// Delete/leave a conversation. + /// + /// If you want to delete it from the vault, specify [vaultId]. + /// If you want to also leave the conversation, specify [token]. + /// Deletion from the local database will always happen. + /// Control deletion from the cache using [deleteCache]. + /// + /// Returns an error if there was one. + static Future delete( + LPHAddress id, { + String? vaultId, + ConversationToken? token, + bool deleteCache = true, + }) async { + // Remove the conversation from the vault (if desired) + if (vaultId != null) { + final err = await removeFromVault(vaultId); + if (err != null) { + return err; + } + } + + // Send a removal request to the server (if desired) + if (token != null) { + final json = await postNodeJSON("/conversations/leave", {"token": token.toMap(id)}); + + if (!json["success"]) { + sendLog("Error deleting conversation on the server, ignoring though: ${json["error"]}"); + // Don't return here, should remove from the local vault regardless + } + } + + // Remove the conversation from the local database + await db.conversation.deleteWhere((tbl) => tbl.id.equals(id.encode())); + await db.member.deleteWhere((tbl) => tbl.conversationId.equals(id.encode())); + if (deleteCache) { + SidebarController.unselectConversation(id); + ConversationController.removeConversation(id); + } + return null; + } + + /// Add a friend to a conversation by generating them a new token and sending it. + /// + /// Returns an error if there was one. + static Future addToConversation(Conversation conv, Friend friend) async { + // Generate a new conversation token for the friend + final json = await postNodeJSON("/conversations/generate_token", { + "token": conv.token.toMap(conv.id), + "data": MemberContainer(friend.id).encrypted(conv.key), + }); + if (!json["success"]) { + return json["error"]; + } + + // Send the friend the invite to the conversation + final result = await sendAuthenticatedStoredAction( + friend, + _conversationPayload(conv.id, ConversationToken.fromJson(json), packageSymmetricKey(conv.key), friend), + ); + return result; + } + + /// Create the stored action for a conversation invite. + static Map _conversationPayload( + LPHAddress id, + ConversationToken token, + String packagedKey, + Friend friend, + ) { + final signature = signMessage(signatureKeyPair.secretKey, "$id${friend.id}"); + return authenticatedStoredAction("conv", { + "id": id.encode(), + "sg": signature, + "token": token.toJson(id), + "key": packagedKey, + }); + } + + /// Ask the server to subscribe to all conversations. + /// + /// Also sends out status packets. + static void subscribeToConversations({StatusController? controller}) { + // Collect all thet tokens for the conversations currently in cache + final tokens = >[]; + for (var conversation in ConversationController.conversations.values) { + tokens.add(conversation.token.toMap(conversation.id)); + } + + // Subscribe to all conversations + unawaited(_sub(StatusController.statusPacket(), StatusController.sharedContentPacket(), tokens, deletions: true)); + } + + /// Ask the server to subscribe to a singular conversation. + /// + /// Also sends out a status packet to this conversation (if it's a direct message). + static void subscribeToConversation( + LPHAddress id, + ConversationToken token, { + StatusController? controller, + deletions = true, + }) { + // Subscribe to all conversations + final tokens = >[token.toMap(id)]; + + // Subscribe + unawaited( + _sub(StatusController.statusPacket(), StatusController.sharedContentPacket(), tokens, deletions: deletions), + ); + } + + /// Returns an error if there was one. + static Future _sub( + String status, + String statusData, + List> tokens, { + deletions = false, + }) async { + // Get the sync dates for every conversation + for (var token in tokens) { + // Get the maximum value of the conversation update timestamps + token["time"] = ConversationController.conversations[LPHAddress.from(token["conv"])]?.updatedAt ?? 0; + } + + // Send the subscription request + final event = await connector.sendActionAndWait( + ServerAction("conv_sub", {"tokens": tokens, "status": status, "data": statusData}), + ); + if (event == null) { + return "server.error".tr; + } + if (!event.data["success"]) { + sendLog("ERROR WHILE SUBSCRIBING: ${event.data["message"]}"); + return event.data["message"]; + } + await ConversationController.finishedLoading( + basePath, + event.data["info"], + deletions ? (event.data["missing"] ?? []) : [], + false, + ); + + return null; + } + + /// Add a new conversation to the cache from the vault. + /// + /// Inserts it into the database or updates it. + /// Subscribes to the conversation. + static Future insertFromVault(Conversation conversation) async { + sendLog("new vault insertion"); + + // Insert it into cache + ConversationController.add(conversation); + + // Insert into database + saveToDatabase(conversation, saveMembers: false); + + // Subscribe to conversation + ConversationService.subscribeToConversation(conversation.id, conversation.token); + + return true; + } + + /// Fetch all data about a conversation from the server and update it in the local database. + /// + /// Also compares the current version with the new version that was sent and doesn't refresh + /// in case it's not nessecary. + static Future fetchNewestVersion(Conversation conversation) async { + if (conversation.membersLoading.value) { + return false; + } + + // Get the data from the server + conversation.membersLoading.value = true; + final json = await postNodeJSON("/conversations/data", {"token": conversation.token.toMap(conversation.id)}); + if (!json["success"]) { + sendLog("SOMETHING WENT WRONG KINDA WITH MEMBER FETCHING ${json["error"]}"); + conversation.membersLoading.value = false; + return false; + } + + // Make sure there are changes worth pulling + if (conversation.lastVersion == json["version"]) { + conversation.membersLoading.value = false; + return true; + } + + // Update to the latest version + conversation.lastVersion = json["version"]; + + // Update the container + sendLog("conversation fetch with ${json["data"]}: ${conversation.container.name}"); + if (conversation.type == model.ConversationType.square) { + conversation.container = SquareContainer.decrypt(json["data"], conversation.key); + } else { + conversation.container = ConversationContainer.decrypt(json["data"], conversation.key); + } + + // Update the members + final members = {}; + for (var memberData in json["members"]) { + final memberContainer = MemberContainer.decrypt(memberData["data"], conversation.key); + final address = LPHAddress.from(memberData["id"]); + members[address] = Member(address, memberContainer.id, MemberRole.fromValue(memberData["rank"])); + } + + // Load the members into the database + for (var currentMember in conversation.members.values) { + if (!members.containsKey(currentMember.tokenId)) { + await db.member.deleteWhere((tbl) => tbl.id.equals(currentMember.tokenId.encode())); + } + } + + // Set the members and save the conversation + batch(() { + conversation.members.value = members; + conversation.membersLoading.value = false; + conversation.containerSub.value = conversation.container; + }); + saveToDatabase(conversation); + + return true; + } + + /// Save a conversation to the local database. + /// + /// By default members are also overwritten. Can be disabled by setting `saveMembers` to `false`. + static void saveToDatabase(Conversation conversation, {saveMembers = true}) { + db.conversation.insertOnConflictUpdate(conversation.entity); + if (saveMembers) { + for (var member in conversation.members.values) { + db.member.insertOnConflictUpdate(member.toData(conversation.id)); + } + } + } + + /// Update when the last message was read in a conversation. + /// + /// [messageSendTime] is when the message was sent. + /// + /// Also calls the same method in the controller. + static void updateLastMessage(Conversation conversation, int stamp) { + // Save the new update time in the local database + final updatedTime = BigInt.from(stamp); + final query = + db.conversation.update() + ..where((tbl) => tbl.id.equals(conversation.id.encode()) & tbl.updatedAt.isSmallerThanValue(updatedTime)); + unawaited(query.write(ConversationCompanion(updatedAt: drift.Value(updatedTime)))); + + // Re-evaluate order in the sidebar + ConversationController.reorder(conversation); + } + + /// Get the notification count of a conversation (straight from the database). + static Future getNotificationCount(LPHAddress conversationId, int readAt, {String extra = ""}) async { + final query = + await db.message + .count( + where: + (row) => + row.conversation.equals(withExtra(conversationId.encode(), extra)) & + row.createdAt.isBiggerThanValue(BigInt.from(readAt)), + ) + .getSingle(); + return query; + } + + /// Get the notification count of a conversation (straight from the database). + static Future getUpdatedAt(LPHAddress conversationId) async { + // Get the maximum value of the conversation update timestamps + final max = db.message.createdAt.max(filter: db.message.conversation.like("%${conversationId.encode()}%")); + final query = db.selectOnly(db.message)..addColumns([max]); + + return await query.map((row) => row.read(max)).getSingleOrNull() ?? BigInt.zero; + } + + /// Mark the conversation as read for the current time. + static Future overwriteRead(Conversation conversation, int stamp, {String extra = ""}) async { + // Build new reads + final reads = ConversationReads.copy(conversation.reads); + reads.map[ConversationReads.getContainerKey(extra)] = stamp; + + // Send new read state to the server + final json = await postNodeJSON("/conversations/read", { + "token": conversation.token.toMap(conversation.id), + "data": reads.toContainer(), + }); + + if (json["success"]) { + conversation.reads = reads; + unawaited(evaluateNotificationCount(conversation)); + } + } + + /// Process new reads when they come from the server (re-evaluate conversation count and more) + static Future evaluateNotificationCount(Conversation conversation) async { + if (conversation is Square) { + final squareContainer = conversation.container as SquareContainer; + + // Update for all topics + for (var topic in [Topic("", "")] + squareContainer.topics) { + final count = await getNotificationCount(conversation.id, conversation.reads.get(topic.id), extra: topic.id); + ConversationController.updateNotificationCount(conversation.id, count, extra: topic.id); + } + } else { + final count = await getNotificationCount(conversation.id, conversation.reads.getMain()); + ConversationController.updateNotificationCount(conversation.id, count); + } + } + + /// Load all members of a conversation into it from the local database. + static Future loadMembers(Conversation conv) async { + // Get all the members from the local database + final members = await (db.select(db.member)..where((tbl) => tbl.conversationId.equals(conv.id.encode()))).get(); + if (members.isEmpty) { + sendLog("WARNING: a conversation doesn't have any members associated with it"); + return; + } + + // Parse all of them from the database + final map = {}; + for (var dbMember in members) { + final member = Member.fromData(dbMember); + map[member.tokenId] = member; + } + + // Set the members in the conversation + conv.members.value = map; + } + + /// Set the data of a conversation on the server. + static Future setData(Conversation conv, ConversationContainer container) async { + final data = container.encrypted(conv.key); + + // Update the conversation on the server + final json = await postNodeJSON("/conversations/set_data", { + "token": conv.token.toMap(conv.id), + "data": {"version": conv.lastVersion, "data": data}, + }); + if (!json["success"]) { + return json["error"]; + } + + // Update locally + conv.lastVersion += 1; + conv.container = container; + conv.containerSub.value = container; + + return null; + } + + /// Append an extra id to the conversation id (for message retrieval in sub channels) + static String withExtra(String convId, String extra) { + if (extra == "") { + return convId; + } + return "${convId}_$extra"; + } +} + +/// A simple helper class to store conversation reads +class ConversationReads { + final map = {}; + + /// Parse as the format received from the server (use "" for empty conversation reads) + ConversationReads.fromContainer(String container) { + // Make sure to not parse no reads at all + if (container == "") { + return; + } + + // Parse the reads from the container to the map + final decrypted = decryptSymmetric(container, vaultKey); + for (var entry in jsonDecode(decrypted).entries) { + map[entry.key] = entry.value; + } + } + + /// Parse as the format received from the local database (use "" for empty conversation reads) + ConversationReads.fromLocalContainer(String container) { + // Make sure to not parse no reads at all + if (container == "") { + return; + } + + // Parse the reads from the container to the map + try { + final decrypted = decryptSymmetric(container, databaseKey); + for (var entry in jsonDecode(decrypted).entries) { + map[entry.key] = entry.value; + } + } catch (_) { + sendLog("ERROR: Local conversation read decryption failure"); + return; + } + } + + /// Copy another instance of [ConversationReads] + ConversationReads.copy(ConversationReads reads) { + for (var entry in reads.map.entries) { + map[entry.key] = entry.value; + } + } + + /// Get all the reads in the form they're stored on the server. + String toContainer() => encryptSymmetric(jsonEncode(map), vaultKey); + + /// Get all the reads in the form they're stored in the local database. + String toLocalContainer() => encryptSymmetric(jsonEncode(map), databaseKey); + + /// Get the read time for an extra id. + int get(String extra) { + return map[getContainerKey(extra)] ?? 0; + } + + /// Get the read time for the main conversation. + int getMain() { + return map[getContainerKey("")] ?? 0; + } + + /// Get the key of a read time in any instance of [ConversationReads] + static String getContainerKey(String extra) { + return extra == "" ? "_" : extra; + } +} diff --git a/lib/services/chat/friends_service.dart b/lib/services/chat/friends_service.dart new file mode 100644 index 00000000..bf1ada16 --- /dev/null +++ b/lib/services/chat/friends_service.dart @@ -0,0 +1,52 @@ +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/database/database_entities.dart' as dbe; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/connection/chat/stored_actions_listener.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:drift/drift.dart'; + +class FriendsService { + /// Called when the friend is updated in the vault + static Future onVaultUpdate(Friend friend) async { + if (friend.id != StatusController.ownAddress) { + await db.friend.insertOnConflictUpdate(await friend.entity()); + } + FriendController.addOrUpdate(friend); + } + + /// Remove a friend. + /// + /// Returns an error if there was one. + static Future remove(Friend friend, {bool removeAction = false}) async { + // Remove the friends vault + final error = await FriendsVault.remove(friend.vaultId); + if (error != null) { + return error; + } + + // Send the deletion stored action in case necessary + if (removeAction) { + final error = await sendAuthenticatedStoredAction(friend, authenticatedStoredAction("fr_rem", {})); + if (error != null) { + return error; + } + } + + // Leave direct message conversations with the guy in them + var toRemove = []; + for (var conversation in ConversationController.conversations.values) { + if (conversation.members.values.any((mem) => mem.address == friend.id) && + conversation.type == dbe.ConversationType.directMessage) { + toRemove.add(conversation.id); + } + } + for (var key in toRemove) { + await ConversationService.delete(key); + } + + return null; + } +} diff --git a/lib/services/chat/friends_vault.dart b/lib/services/chat/friends_vault.dart new file mode 100644 index 00000000..a08fa4be --- /dev/null +++ b/lib/services/chat/friends_vault.dart @@ -0,0 +1,338 @@ +part of '../../controller/account/friend_controller.dart'; + +class FriendsVault { + /// Store a received request in the vault. + /// + /// Returns an error if there was one. + static Future storeReceivedRequest(Request request) async { + // Store the request in the vault + final (error, entry) = await _store(request.toStoredPayload(false)); + if (error != null) { + return error; + } + + // Call the related vault update event + request.vaultId = entry!.$1; + await updateFromVaultUpdate(FriendVaultUpdate(entry.$2, [], [], [request], [], [])); + return null; + } + + /// Store a sent request in the vault. + /// + /// Returns an error if there was one. + static Future storeSentRequest(Request request) async { + // Store the request in the vault + final (error, entry) = await _store(request.toStoredPayload(true)); + if (error != null) { + return error; + } + + // Call the related vault update event + request.vaultId = entry!.$1; + await updateFromVaultUpdate(FriendVaultUpdate(entry.$2, [], [], [], [request], [])); + return null; + } + + /// Helper method for storing things in the friends vault. + /// + /// The first element is an error if there was one. + /// The second element is a tuple of vault id and version in case successful. + static Future<(String?, (String, int)?)> _store(String data) async { + // Encrypt the friend for the vault + final payload = encryptSymmetric(data, vaultKey); + + // Add the friend to the vault + final json = await postAuthorizedJSON("/account/friends/add", { + "payload": payload, + "receive_date": encryptDate(DateTime.fromMillisecondsSinceEpoch(0)), + }); + + // Check if there was an error + if (!json["success"]) { + return (json["error"] as String, null); + } + return (null, (json["id"] as String, (json["version"] as num).toInt())); + } + + /// Update a friend in the vault. + /// + /// Returns an error if there was one. + static Future updateFriend(Friend friend) async { + // Store the request in the vault + final (error, version) = await _update(friend.vaultId, await friend.toStoredPayload()); + if (error != null) { + return error; + } + + // Call the related vault update event + await updateFromVaultUpdate(FriendVaultUpdate(version!, [], [friend.vaultId], [], [], [friend])); + return null; + } + + /// Helper method for updating things in the friends vault. + /// + /// The first element is an error if there was one. + /// The second element is the new version in case successsful. + static Future<(String?, int?)> _update(String id, String data) async { + // Encrypt the friend for the vault + final payload = encryptSymmetric(data, vaultKey); + + // Add the friend to the vault + final json = await postAuthorizedJSON("/account/friends/update", { + "entry": id, + "payload": payload, + }); + + // Check if there was an error + if (!json["success"]) { + return (json["error"] as String, null); + } + return (null, (json["version"] as num).toInt()); + } + + /// Remove friend from vault. + /// + /// Returns an error if there was one. + static Future remove(String vaultId) async { + // Remove the friend from the server vault + final json = await postAuthorizedJSON("/account/friends/remove", {"id": vaultId}); + if (!json["success"]) { + return json["error"]; + } + + // Update the local vault + await updateFromVaultUpdate(FriendVaultUpdate(json["version"], [vaultId], [], [], [], [])); + return null; + } + + /// Encrypt a date with server-side information + static String encryptDate(DateTime time) { + return ServerStoredInfo(time.millisecondsSinceEpoch.toString()).transform(); + } + + /// Decrypt a date with server-side information + static DateTime decryptDate(String text) { + final info = ServerStoredInfo.untransform(text); + return DateTime.fromMillisecondsSinceEpoch(int.parse(info.text)); + } + + /// Get the last date a new message was sent to the friend (for replay attack prevention) + static Future lastReceiveDate(String id) async { + final json = await postAuthorizedJSON("/account/friends/get_receive_date", {"id": id}); + + if (!json["success"]) { + sendLog("COULDN'T GET THE RECEIVE DATE FOR $id: ${json["error"]}"); + return null; + } + + try { + return decryptDate(json["date"]); + } catch (e) { + return null; + } + } + + /// Set a new receive date (for replay attack prevention) + static Future setReceiveDate(String id, DateTime received) async { + final json = await postAuthorizedJSON("/account/friends/update_receive_date", { + "id": id, + "date": encryptDate(received), + }); + + if (!json["success"]) { + sendLog("COULDN'T SAVE THE NEW RECEIVE DATE ${json["error"]}"); + return false; + } + + return true; + } + + /// A global boolean that tells you whether the friends vault is currently refreshing or not + static final friendsVaultRefreshing = signal(false); + + /// Refresh all friends and load them from the vault (also removes what's not on the server) + static Future refreshFriendsVault() async { + if (friendsVaultRefreshing.value) { + sendLog("COLLISION: Friends vault is already refreshing, this should be something worth looking into"); + return null; + } + + // Get the latest version + final version = await VaultVersioningService.retrieveVersion(VaultVersioningService.vaultTypeFriend, ""); + + friendsVaultRefreshing.value = true; + // Load friends from vault + final json = await postAuthorizedJSON("/account/friends/sync", {"version": version}); + if (!json["success"]) { + friendsVaultRefreshing.value = false; + return "friends.error".tr; + } + + // Parse the JSON (in different isolate) + final res = await sodiumLib.runIsolated( + (sodium, keys, pairs) => _parseFriends(version, json, sodium, keys[0]), + secureKeys: [vaultKey], + ); + + // Update the local vault + await updateFromVaultUpdate(res); + + friendsVaultRefreshing.value = false; + return null; + } + + /// Parse a response from the server vault sync to a friend vault update + static Future _parseFriends( + int currentVersion, + Map json, + Sodium sodium, + SecureKey key, + ) async { + final deleted = []; + final friendVaultIds = []; + final friends = []; + final requests = []; + final requestsSent = []; + for (var friend in json["friends"]) { + final decrypted = decryptSymmetric(friend["friend"], key, sodium); + final data = jsonDecode(decrypted); + + // Set the new version + if (friend["version"] > currentVersion) { + currentVersion = friend["version"]; + } + + // Add to the list of deleted ids when it was deleted + if (friend["deleted"]) { + deleted.add(friend["id"]); + continue; + } + + // Check if request or friend + if (data["rq"]) { + if (data["self"]) { + final rq = Request.fromStoredPayload(friend["id"], friend["updated_at"], data); + requestsSent.add(rq); + } else { + final rq = Request.fromStoredPayload(friend["id"], friend["updated_at"], data); + requests.add(rq); + } + } else { + final fr = Friend.fromStoredPayload(friend["id"], friend["updated_at"], data); + friends.add(fr); + friendVaultIds.add(friend["id"]); + } + } + + return FriendVaultUpdate(currentVersion, deleted, friendVaultIds, requests, requestsSent, friends); + } + + /// Update the local vault using a server friends vault update + static Future updateFromVaultUpdate(FriendVaultUpdate update) async { + // Change the version to the one in the update + await VaultVersioningService.storeOrUpdateVersion(VaultVersioningService.vaultTypeFriend, "", update.newVersion); + + // Update the requests + for (var request in update.requests) { + if (RequestController.requests[request.id] == null) { + RequestsService.onVaultUpdate(request); + } + } + + // Remove all requests and also the ones that aren't requests anymore (a friend could've been upgraded) + if (update.deleted.isNotEmpty || update.friendVaultIds.isNotEmpty) { + RequestController.requests.removeWhere((item, rq) { + if (update.deleted.contains(rq.vaultId) || update.friendVaultIds.contains(rq.vaultId)) { + unawaited(db.request.deleteWhere((t) => t.id.equals(rq.id.encode()))); + return true; + } + return false; + }); + } + + for (var request in update.requestsSent) { + if (RequestController.requestsSent[request.id] == null) { + RequestsService.onVaultUpdateSent(request); + } + } + + // Remove all requests and also the ones that aren't requests anymore (a friend could've been upgraded) + if (update.deleted.isNotEmpty || update.friendVaultIds.isNotEmpty) { + RequestController.requestsSent.removeWhere((item, rq) { + if (update.deleted.contains(rq.vaultId) || update.friendVaultIds.contains(rq.vaultId)) { + unawaited(db.request.deleteWhere((t) => t.id.equals(rq.id.encode()))); + return true; + } + return false; + }); + } + + // Push friends + for (var friend in update.friends) { + if (FriendController.friends[friend.id] == null) { + await FriendsService.onVaultUpdate(friend); + } + } + if (update.deleted.isNotEmpty) { + FriendController.friends.removeWhere((id, fr) { + if (update.deleted.contains(fr.vaultId) && id != StatusController.ownAddress) { + unawaited(db.friend.deleteWhere((t) => t.id.equals(fr.id.encode()))); + return true; + } + return false; + }); + } + } +} + +class FriendVaultUpdate { + final int newVersion; + + final List deleted; + final List friendVaultIds; + + final List requests; + final List requestsSent; + final List friends; + + FriendVaultUpdate(this.newVersion, this.deleted, this.friendVaultIds, this.requests, this.requestsSent, this.friends); +} + +/// Class for storing all keys for a friend +class KeyStorage { + late String profileKeyPacked; + String storedActionKey; + Uint8List publicKey; + Uint8List signatureKey; + + KeyStorage.empty() + : publicKey = Uint8List(0), + signatureKey = Uint8List(0), + profileKeyPacked = "unbreathable_was_here_but_2024", + storedActionKey = "unbreathable_was_here"; + KeyStorage(this.publicKey, this.signatureKey, SecureKey profileKey, this.storedActionKey) { + profileKeyPacked = packageSymmetricKey(profileKey); + unpackedProfileKey = profileKey; + } + KeyStorage.fromJson(Map json) + : publicKey = unpackagePublicKey(json["pub"]), + profileKeyPacked = json["pf"] ?? "", + signatureKey = unpackagePublicKey(json["sg"]), + storedActionKey = json["sa"] ?? ""; + + Map toJson() { + return { + "pub": packagePublicKey(publicKey), + "pf": profileKeyPacked, + "sg": packagePublicKey(signatureKey), + "sa": storedActionKey, + }; + } + + // Just so we don't break the API anywhere yk + SecureKey? unpackedProfileKey; + SecureKey get profileKey { + unpackedProfileKey ??= unpackageSymmetricKey(profileKeyPacked); + return unpackedProfileKey!; + } +} diff --git a/lib/pages/chat/components/library/library_manager.dart b/lib/services/chat/library_manager.dart similarity index 52% rename from lib/pages/chat/components/library/library_manager.dart rename to lib/services/chat/library_manager.dart index 46599125..ce61c911 100644 --- a/lib/pages/chat/components/library/library_manager.dart +++ b/lib/services/chat/library_manager.dart @@ -4,61 +4,32 @@ import 'dart:convert'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; import 'package:chat_interface/database/database.dart'; import 'package:chat_interface/database/database_entities.dart'; -import 'package:chat_interface/main.dart'; -import 'package:chat_interface/controller/current/steps/account_step.dart'; import 'package:chat_interface/controller/current/tasks/vault_sync_task.dart'; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; import 'package:chat_interface/util/constants.dart'; +import 'package:chat_interface/util/encryption/hash.dart'; import 'package:chat_interface/util/popups.dart'; -import 'package:chat_interface/util/web.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -class LibraryManager { - /// Load all new entries from the server - static Future refreshEntries() async { - // Get everything from the server after the date - final json = await postAuthorizedJSON("/account/vault/list", { - "after": 0, - "tag": Constants.vaultLibraryTag, - }); - - // Check if there is an error - if (!json["success"]) { - return json["error"]; +class LibraryManager extends VaultTarget { + LibraryManager() : super(Constants.vaultLibraryTag); + + @override + Future processEntries(List deleted, List newEntries) async { + // Add all new entries + final list = []; + for (var entry in newEntries) { + final libraryEntry = LibraryEntry.fromJson(entry.id, jsonDecode(entry.payload)); + list.add(libraryEntry); + await db.libraryEntry.insertOnConflictUpdate(await libraryEntry.entity); } - // Parse all the vault entries in an isolate - final (parsed, ids) = await sodiumLib.runIsolated( - (sodium, keys, pairs) { - final list = []; - final ids = []; - for (var entryJson in json["entries"]) { - final entry = VaultEntry.fromJson(entryJson); - final libraryEntry = LibraryEntry.fromJson(entry.id, jsonDecode(entry.decryptedPayload(keys[0], sodium))); - list.add(libraryEntry); - ids.add(entry.id); - } - - return (list, ids); - }, - secureKeys: [vaultKey], - ); - // Delete all library entries that aren't in the local database anymore - await db.libraryEntry.deleteWhere((tbl) => tbl.id.isNotIn(ids)); - - // Check if there are any - if (parsed.isEmpty || ids.isEmpty) { - return null; - } - - // Add all of them to the database - for (var entry in parsed) { - await db.libraryEntry.insertOnConflictUpdate(entry.entity); - } + await db.libraryEntry.deleteWhere((tbl) => tbl.id.isIn(deleted)); - return null; + return; } /// Remove a library entry from the library @@ -70,9 +41,6 @@ class LibraryManager { return false; } - // Remove from the local database - await db.libraryEntry.deleteWhere((tbl) => tbl.id.equals(entry.id)); - return true; } @@ -117,15 +85,11 @@ class LibraryManager { } // Add entry to server vault - final id = await addToVault(Constants.vaultLibraryTag, jsonEncode(entry.toJson())); - if (id == null) { - showErrorPopup("error", "server.error".tr); + final (error, _) = await addToVault(Constants.vaultLibraryTag, jsonEncode(entry.toJson())); + if (error != null) { + showErrorPopup("error", error); return false; } - - // Add to local database as well - entry.id = id; - await db.libraryEntry.insertOnConflictUpdate(entry.entity); return true; } @@ -135,14 +99,12 @@ class LibraryManager { final stream = image.image.resolve(const ImageConfiguration()); ImageStreamListener? listener; stream.addListener( - listener = ImageStreamListener( - (ImageInfo image, bool synchronousCall) { - var myImage = image.image; - Size size = Size(myImage.width.toDouble(), myImage.height.toDouble()); - completer.complete(size); - stream.removeListener(listener!); - }, - ), + listener = ImageStreamListener((ImageInfo image, bool synchronousCall) { + var myImage = image.image; + Size size = Size(myImage.width.toDouble(), myImage.height.toDouble()); + completer.complete(size); + stream.removeListener(listener!); + }), ); return completer.future; } @@ -155,28 +117,55 @@ class LibraryEntry { final DateTime createdAt; final int width; final int height; + String? identifier; + AttachmentContainer? container; LibraryEntry(this.id, this.type, this.data, this.createdAt, this.width, this.height); /// Get a library entry from the local database object - LibraryEntry.fromData(LibraryEntryData data) - : this( - data.id, - data.type, - data.data, - DateTime.fromMillisecondsSinceEpoch(data.createdAt.toInt()), - data.width, - data.height, - ); - - get entity => LibraryEntryData( - id: id, - type: type, - createdAt: BigInt.from(createdAt.millisecondsSinceEpoch), - data: data, - width: width, - height: height, + static Future fromData(LibraryEntryData data) async { + // Migrate to new system in case still not database encrypted + if (data.identifierHash == "to-migrate") { + // Get the new identifier + final container = await AttachmentController.fromString(data.data); + final identifier = LibraryEntry.entryIdentifier(container); + + // Fix the entry + data = LibraryEntryData( + id: data.id, + type: data.type, + createdAt: data.createdAt, + identifierHash: identifier, + data: dbEncrypted(data.data), + width: data.width, + height: data.height, ); + unawaited(db.libraryEntry.insertOnConflictUpdate(data)); + } + + // Create the actual library entry + final entry = LibraryEntry( + data.id, + data.type, + fromDbEncrypted(data.data), + DateTime.fromMillisecondsSinceEpoch(data.createdAt.toInt()), + data.width, + data.height, + ); + entry.identifier = data.identifierHash; + + return entry; + } + + Future get entity async => LibraryEntryData( + id: id, + type: type, + createdAt: BigInt.from(createdAt.millisecondsSinceEpoch), + identifierHash: identifier ?? (await getIdentifier()), + data: dbEncrypted(data), + width: width, + height: height, + ); /// Convert a LibraryEntry to a JSON map Map toJson() { @@ -200,4 +189,27 @@ class LibraryEntry { json['height'], ); } + + /// Get the identifier of the library entry. + Future getIdentifier() async { + final container = await AttachmentController.fromString(data); + return LibraryEntry.entryIdentifier(container); + } + + /// Load all the things needed for displaying the entry. + Future initForUI() async { + container = await AttachmentController.fromString(data); + identifier = LibraryEntry.entryIdentifier(container!); + } + + /// Get the identifier of a Library entry from an AttachmentContainer. + static String entryIdentifier(AttachmentContainer container) { + // If it's a file hash the file id + if (container.attachmentType == AttachmentContainerType.file) { + return hashSha(container.id); + } + + // Otherwise hash the URL of the remote container + return hashSha(container.url); + } } diff --git a/lib/services/chat/message_search_query.dart b/lib/services/chat/message_search_query.dart new file mode 100644 index 00000000..2f9fff00 --- /dev/null +++ b/lib/services/chat/message_search_query.dart @@ -0,0 +1,185 @@ +import 'dart:async'; + +import 'package:chat_interface/controller/conversation/message_provider.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:drift/drift.dart'; +import 'package:signals/signals_flutter.dart'; + +class MessageSearchQuery { + final filters = listSignal([]); + final results = listSignal([]); + FocusNode? currentFocus; + + // Data for the message search algorithm + bool _finished = false; + bool _restart = false; + int _lastTime = 0; + Timer? _searchTimer; + int neededMessages = 10; + + void search({bool increment = false}) { + _searchTimer?.cancel(); + if (increment) { + if (_finished) { + return; + } + neededMessages += 10; + _restart = false; + } else { + neededMessages = 10; + _restart = true; + } + bool working = false; + _searchTimer = Timer.periodic(Duration(milliseconds: 50), (timer) async { + final wasRestart = _restart; + if (_restart) { + _restart = false; + _lastTime = DateTime.now().millisecondsSinceEpoch; + } + if (working) { + return; + } + working = true; + + // Check if there is a conversation filter + final convFilter = filters.firstWhereOrNull((f) => f is ConversationFilter) as ConversationFilter?; + + // Grab all the messages from the list using the offset + // Make sure to only search in the current conversation in case there is a conversation filter + final SimpleSelectStatement<$MessageTable, MessageData> messageQuery; + if (convFilter != null) { + messageQuery = + db.select(db.message) + ..where((tbl) => tbl.createdAt.isSmallerThanValue(BigInt.from(_lastTime))) + ..where((tbl) => tbl.conversation.equals(convFilter.conversationId)) + ..orderBy([(u) => OrderingTerm.desc(u.createdAt)]) + ..limit(100); + } else { + messageQuery = + db.select(db.message) + ..where((tbl) => tbl.createdAt.isSmallerThanValue(BigInt.from(_lastTime))) + ..orderBy([(u) => OrderingTerm.desc(u.createdAt)]) + ..limit(100); + } + + // Make sure to only search in the current conversation in case there is a conversation filter + if (convFilter != null) {} + final messages = await messageQuery.get(); + + // If there are no messages, cancel the timer + if (messages.isEmpty) { + timer.cancel(); + return; + } + + // Set the last message time to make sure messages aren't loaded twice + _lastTime = messages.last.createdAt.toInt(); + + // Check all the filters for the current messages (maybe put in an isolate in the future?) + final found = []; + for (var message in messages) { + final (processed, conversation) = ConversationMessageProvider.decryptFromLocalDatabase(message, databaseKey); + + // Maybe remove this limitation in the future? + if (processed.type != MessageType.text) { + continue; + } + + bool fail = false; + for (var filter in filters) { + if (!filter.matches(processed, conversation: conversation)) { + fail = true; + break; + } + } + + if (!fail) { + found.add(processed); + } + } + + // Initialize all the attachments on the messages + for (var msg in found) { + await msg.initAttachments(null); + } + + // Add all found results to the list + if (wasRestart) { + results.value = found; + } else { + results.addAll(found); + + // Check if the algorithm should be stopped for now + if (results.length >= neededMessages) { + timer.cancel(); + } + } + + // Check if fetching can be stopped + if (messages.length < 100) { + _finished = true; + timer.cancel(); + } + + working = false; + }); + } +} + +/// Abstract class for all filters related to messages +abstract class MessageFilter { + /// This function is called for every message in the database. + /// + /// If it returns true, the message will be loaded as part of the search results. + bool matches(Message message, {String? conversation}); +} + +/// Filter for all messages in a conversation +class ConversationFilter extends MessageFilter { + final String conversationId; + + ConversationFilter(this.conversationId); + + @override + bool matches(Message message, {String? conversation}) { + if (conversation == null) { + return false; + } + + return conversationId == conversation; + } +} + +/// Filter that checks if a certain piece of content is in a message +class ContentFilter extends MessageFilter { + final String content; + + ContentFilter(this.content); + + @override + bool matches(Message message, {String? conversation}) { + // Split the search query into words + final searchWords = content.toLowerCase().split(RegExp(r'\s+')); + + // Check if all words in the search query are found in the content + final contentWords = message.content.toLowerCase().split(RegExp(r'\s+')); + if (searchWords.every((word) => contentWords.any((contentWord) => contentWord.contains(word)))) { + return true; + } + + // Check if all words in the search query are found in any attachment + for (var attachment in message.attachments) { + final attachmentWords = attachment.toLowerCase().split(RegExp(r'\s+')); + if (searchWords.every((word) => attachmentWords.any((attachmentWord) => attachmentWord.contains(word)))) { + return true; + } + } + + // No matches found + return false; + } +} diff --git a/lib/services/chat/message_service.dart b/lib/services/chat/message_service.dart new file mode 100644 index 00000000..b1adab5f --- /dev/null +++ b/lib/services/chat/message_service.dart @@ -0,0 +1,141 @@ +import 'dart:async'; + +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/conversation/message_controller.dart'; +import 'package:chat_interface/controller/conversation/message_provider.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/conversation/system_messages.dart'; +import 'package:chat_interface/controller/spaces/ringing_manager.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/main.dart'; +import 'package:chat_interface/pages/status/setup/instance_setup.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; + +class MessageService { + /// Store all of the messages in the list in the local database and cache. + /// The string next to the message in the list is its extra id. + /// + /// This method doesn't play a sound because it's only used for synchronization. + static Future storeMessages(List<(Message, String)> messages, Conversation conversation) async { + if (messages.isEmpty) { + return false; + } + + // Sort all the messages to prevent failing system messages + messages.sort((a, b) { + return a.$1.createdAt.compareTo(b.$1.createdAt); + }); + + // Encrypt everything for local database storage + final parts = await sodiumLib.runIsolated((sodium, keys, pairs) async { + final list = <(String, String)>[]; + for (var (message, _) in messages) { + list.add(( + dbEncrypted(message.toContentJson(), sodium, keys[0]), + dbEncrypted(message.senderAddress.encode(), sodium, keys[0]), + )); + } + + return list; + }, secureKeys: [databaseKey]); + + // Store all the messages in the local database + int index = 0; + for (var (message, extra) in messages) { + await storeMessage(message, conversation, extra: extra, simple: true, part: parts[index]); + index++; + } + + // Update last message in the conversation + ConversationService.updateLastMessage(conversation, messages.last.$1.createdAt.millisecondsSinceEpoch); + + return true; + } + + /// Store a message in the local database and in the cache (if the conversation is selected). + /// + /// Also handles system messages. + /// Set [simple] to [true] in case you want to avoid any extra stuff other than adding to cache and database. + static Future storeMessage( + Message message, + Conversation conversation, { + String extra = "", + bool simple = false, + (String, String)? part, + }) async { + // Get the current provider + final provider = SidebarController.getCurrentProvider(); + + if (!simple) { + // Update message read time for conversations (nessecary for notification count) + ConversationService.updateLastMessage(conversation, message.createdAt.millisecondsSinceEpoch); + + // Play a notification sound when a new message arrives + unawaited(RingingManager.playNotificationSound()); + } + + // Handle system messages + if (message.type == MessageType.system) { + if ((provider?.conversation.id ?? "hi") == conversation.id && extra == (provider?.extra ?? "-")) { + SystemMessages.messages[message.content]?.handle(message, provider!); + } else { + SystemMessages.messages[message.content]?.handle( + message, + ConversationMessageProvider(conversation, extra: extra), + ); + } + + // Check if message should be stored + if (SystemMessages.messages[message.content]?.store ?? false) { + // Store message in local database + _storeInLocalDatabase(conversation, message, extra: extra, part: part); + } + } else { + // Store message in local database + _storeInLocalDatabase(conversation, message, extra: extra, part: part); + } + + // On call message type, ring using the message TODO: Reintroduce the ringtone in Spaces + /* + if (message.type == MessageType.call && message.senderAddress != StatusController.ownAddress) { + final container = SpaceConnectionContainer.fromJson(jsonDecode(message.content)); + RingingManager.startRinging(conversation, container); + } + */ + + // Add to the cache + return MessageController.addMessage(message, conversation, extra: extra, part: part, simple: simple); + } + + /// Store a message in the database. + /// + /// The part tuple is provided by [storeMessages] to not encrypt the data twice. + static void _storeInLocalDatabase( + Conversation conversation, + Message message, { + required String extra, + (String, String)? part, + }) { + db + .into(db.message) + .insertOnConflictUpdate( + MessageData( + id: message.id, + content: part?.$1 ?? dbEncrypted(message.toContentJson()), + senderToken: message.senderToken.encode(), + senderAddress: part?.$2 ?? dbEncrypted(message.senderAddress.encode()), + createdAt: BigInt.from(message.createdAt.millisecondsSinceEpoch), + conversation: ConversationService.withExtra(conversation.id.encode(), extra), + edited: message.edited, + verified: message.verified.value, + ), + ); + } + + /// Split a conversation id into the id of the conversation and the extra identifier + static (String, String) intoIdAndExtra(String convId) { + final args = convId.split("_"); + return (args[0], args.length == 1 ? "" : args[1]); + } +} diff --git a/lib/controller/account/profile_picture_helper.dart b/lib/services/chat/profile_picture_helper.dart similarity index 80% rename from lib/controller/account/profile_picture_helper.dart rename to lib/services/chat/profile_picture_helper.dart index 3c6e30d0..1c220311 100644 --- a/lib/controller/account/profile_picture_helper.dart +++ b/lib/services/chat/profile_picture_helper.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:ui' as ui; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; @@ -27,28 +27,21 @@ class ProfileHelper { // Get old profile picture final oldProfile = await (db.profile.select()..where((tbl) => tbl.id.equals(friend.id.encode()))).getSingleOrNull(); - final json = await postAddress(friend.id.server, "/account/profile/get", { - "id": friend.id.id, - }); + final json = await postAddress(friend.id.server, "/account/profile/get", {"id": friend.id.id}); if (!json["success"]) { sendLog("ERROR WHILE GETTING PROFILE: ${json["error"]}"); return null; } - // Check if there is a new name (also handled by the profile endpoint) - if (json["name"] != friend.name) { + // Check if there is a new name or a new display name + if (json["name"] != friend.name || json["display_name"] != friend.displayName.value) { friend.name = json["name"]; - await friend.update(); - } - - // Check if there is a new display name - final displayName = json["display_name"]; - if (displayName != friend.displayName.value) { - friend.updateDisplayName(displayName); + friend.displayName.value = json["display_name"]; + await FriendsVault.updateFriend(friend); } - sendLog("downloading ${friend.name}"); + sendLog("downloading ${friend.name} on ${friend.id.server}"); // Check if there is a profile picture if ((json["profile"]["container"] ?? "") == "") { @@ -59,8 +52,10 @@ class ProfileHelper { } // Decrypt the profile picture data - final containerJson = jsonDecode(decryptSymmetric(json["profile"]["container"], friend.keyStorage.profileKey)); - final container = Get.find().fromJson(StorageType.permanent, containerJson); + final containerJson = jsonDecode( + decryptSymmetric(json["profile"]["container"], (await friend.getKeys()).profileKey), + ); + final container = AttachmentController.fromJson(StorageType.permanent, containerJson); String? oldPictureId; String? oldPath; @@ -81,7 +76,7 @@ class ProfileHelper { } // Download the file - final success = await Get.find().downloadAttachment(container, popups: false, trustPopups: true); + final success = await AttachmentController.downloadAttachment(container, popups: false, trustPopups: true); if (!success) { sendLog("download failed"); return null; @@ -103,7 +98,7 @@ class ProfileHelper { /// Upload a profile picture to the server and set it as the current profile picture static Future uploadProfilePicture(XFile file, String originalName, {Uint8List? bytes}) async { // Upload the file - final response = await Get.find().uploadFile( + final response = await AttachmentController.uploadFile( UploadData(file), StorageType.permanent, Constants.fileAppDataTag, @@ -126,7 +121,7 @@ class ProfileHelper { } // Set in local database - await Get.find().friends[StatusController.ownAddress]!.updateProfilePicture(response.container!); + await FriendController.friends[StatusController.ownAddress]!.updateProfilePicture(response.container!); return true; } @@ -140,7 +135,7 @@ class ProfileHelper { } // Set in local database - await Get.find().friends[StatusController.ownAddress]!.updateProfilePicture(null); + await FriendController.friends[StatusController.ownAddress]!.updateProfilePicture(null); return true; } diff --git a/lib/services/chat/requests_service.dart b/lib/services/chat/requests_service.dart new file mode 100644 index 00000000..4e5bbfb6 --- /dev/null +++ b/lib/services/chat/requests_service.dart @@ -0,0 +1,85 @@ +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/account/requests_controller.dart'; +import 'package:chat_interface/services/chat/unknown_service.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/controller/current/steps/account_step.dart'; +import 'package:chat_interface/controller/current/steps/key_step.dart'; +import 'package:chat_interface/controller/current/steps/stored_actions_step.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/services/connection/chat/stored_actions_listener.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:drift/drift.dart'; +import 'package:get/get.dart'; + +class RequestsService { + /// Called when the request is updated in the vault + static void onVaultUpdateSent(Request request) { + db.request.insertOnConflictUpdate(request.entity(true)); + RequestController.addSentRequestOrUpdate(request); + } + + /// Called when the request is updated in the vault + static void onVaultUpdate(Request request) { + db.request.insertOnConflictUpdate(request.entity(false)); + RequestController.addRequestOrUpdate(request); + } + + /// Send a friend request to an account or accept if sent before. + /// + /// First value in tuple is an error if there was one. + /// Second value in tuple is the message if successful (request.accepted or request.sent). + /// Both can also be null in case the vault is synchronizing. + static Future<(String?, String?)> sendOrAcceptFriendRequest(UnknownAccount account) async { + // Make sure the vault isn't being synchronized during a friend request + if (FriendsVault.friendsVaultRefreshing.value) { + return (null, null); + } + + // Encrypt friend request + final payload = storedAction("fr_rq", { + "ad": StatusController.ownAddress.encode(), + "pf": packageSymmetricKey(profileKey), + "sa": storedActionKey, + "s": encryptAsymmetricAuth(account.publicKey, asymmetricKeyPair.secretKey, account.name), + }); + + // Send stored action + final result = await sendStoredAction(account.id, account.publicKey, payload); + if (result != null) { + return (result, null); + } + + // Accept friend request if there is one from the other user + final requestReceived = RequestController.requests[account.id]; + if (requestReceived != null) { + // Make the request a friend in the vault + final error = await FriendsVault.updateFriend(requestReceived.friend); + if (error != null) { + return (error, null); + } + + return (null, "request.accepted".tr); + } else { + // Create the request + final request = Request( + account.id, + account.name, + account.displayName, + "", + KeyStorage(account.publicKey, account.signatureKey, profileKey, ""), + DateTime.now().millisecondsSinceEpoch, + ); + + // Store the request in the friends vault + final error = await FriendsVault.storeSentRequest(request); + if (error != null) { + RequestController.requestsLoading.value = false; + return (error, null); + } + + return (null, "request.sent".tr); + } + } +} diff --git a/lib/services/chat/status_service.dart b/lib/services/chat/status_service.dart new file mode 100644 index 00000000..0d6671d3 --- /dev/null +++ b/lib/services/chat/status_service.dart @@ -0,0 +1,67 @@ +import 'package:chat_interface/controller/conversation/attachment_controller.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/database/database.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; + +class StatusService { + /// Send a new status packet informing all friends about a new status. + /// + /// Returns an error if there was one. + static Future sendStatus({String? message, int? type}) async { + // Validate the status to make sure everything is fine + final event = await connector.sendActionAndWait( + ServerAction("st_validate", { + "status": StatusController.statusPacket( + StatusController.newStatusJson( + message ?? StatusController.status.peek(), + type ?? StatusController.type.peek(), + ), + ), + "data": StatusController.sharedContentPacket(), + }), + ); + if (event == null) { + return "server.error".tr; + } + if (!event.data["success"]) { + return event.data["message"]; + } + + // Update teh status in the controller + StatusController.updateStatus(message: message, type: type); + + // Send the new status + sendLog("updating due to status"); + ConversationService.subscribeToConversations(); + return null; + } + + /// Log out of the current account. + /// + /// Optionally delete all local database tables and files. + static Future logOut({deleteEverything = false, deleteFiles = false}) async { + // Delete the session information + await db.setting.deleteWhere((tbl) => tbl.key.equals("profile")); + + // Delete all data + if (deleteEverything) { + for (var table in db.allTables) { + await table.deleteAll(); + } + } + + // Delete all files + if (deleteFiles) { + await AttachmentController.deleteAllFiles(); + } + + // Exit the app + await SystemNavigator.pop(animated: true); + } +} diff --git a/lib/controller/account/unknown_controller.dart b/lib/services/chat/unknown_service.dart similarity index 57% rename from lib/controller/account/unknown_controller.dart rename to lib/services/chat/unknown_service.dart index 16507e5d..57a6b089 100644 --- a/lib/controller/account/unknown_controller.dart +++ b/lib/services/chat/unknown_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/database/database.dart'; import 'package:chat_interface/controller/current/steps/key_step.dart'; @@ -10,22 +10,23 @@ import 'package:chat_interface/pages/status/setup/instance_setup.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; import 'package:drift/drift.dart'; -import 'package:get/get.dart'; - -class UnknownController extends GetxController { - final cache = {}; +class UnknownService { /// Load the profile of an unknown account by name - Future getUnknownProfileByName(String name) async { + static Future getUnknownProfileByName(String name) async { // Ignore if it is the name of the current account - if (Get.find().name.value == name) { - return UnknownAccount(StatusController.ownAddress, name, "", signatureKeyPair.publicKey, asymmetricKeyPair.publicKey); + if (StatusController.name.value == name) { + return UnknownAccount( + StatusController.ownAddress, + name, + "", + signatureKeyPair.publicKey, + asymmetricKeyPair.publicKey, + ); } // Get account - final json = await postAuthorizedJSON("/account/get_name", { - "name": name, - }); + final json = await postAuthorizedJSON("/account/get_name", {"name": name}); // Check if it was successful if (!json["success"]) { @@ -42,31 +43,26 @@ class UnknownController extends GetxController { unpackagePublicKey(json["pub"]), ); - // Add the unknown profile to the database - await db.unknownProfile.insertOnConflictUpdate(profile.toData()); - cache[profile.id] = profile; return profile; } /// Load the profile of someone unknown - Future loadUnknownProfile(LPHAddress address) async { + static Future loadUnknownProfile(LPHAddress address) async { // Ignore if it is the id of the current account if (address == StatusController.ownAddress) { - return UnknownAccount(StatusController.ownAddress, "", "", signatureKeyPair.publicKey, asymmetricKeyPair.publicKey); + return UnknownAccount( + StatusController.ownAddress, + "", + "", + signatureKeyPair.publicKey, + asymmetricKeyPair.publicKey, + ); } // If the id matches a friend, use that instead - final controller = Get.find(); - if (controller.friends[address] != null) { - return UnknownAccount.fromFriend(controller.friends[address]!); - } - - // If the guy is in the cache, that works too - if (cache[address] != null) { - // Make sure the cached version isn't too old - if (cache[address]!.lastFetch != null && DateTime.now().difference(cache[address]!.lastFetch!) < const Duration(minutes: 5)) { - return cache[address]; - } + final friend = FriendController.friends[address]; + if (friend != null) { + return UnknownAccount.fromFriend(friend); } // Make sure the server is trusted @@ -74,10 +70,20 @@ class UnknownController extends GetxController { return null; } + // Check if there is a cached version of the unknown account in the local database + final query = + db.unknownProfile.select()..where( + (tbl) => + tbl.id.equals(address.encode()) & + tbl.lastFetched.isBiggerThanValue(DateTime.now().subtract(Duration(hours: 2))), + ); + final result = await query.getSingleOrNull(); + if (result != null) { + return UnknownAccount.fromData(result); + } + // Get account - final json = await postAddress(address.server, "/account/get", { - "id": address.id, - }); + final json = await postAddress(address.server, "/account/get", {"id": address.id}); // Check if it was successful if (!json["success"]) { @@ -95,8 +101,7 @@ class UnknownController extends GetxController { ); // Add the unknown profile to the database - await db.unknownProfile.insertOnConflictUpdate(profile.toData()); - cache[address] = profile; + await db.unknownProfile.insertOnConflictUpdate(profile.toData(DateTime.now())); return profile; } } @@ -114,32 +119,27 @@ class UnknownAccount { factory UnknownAccount.fromData(UnknownProfileData data) { final keys = jsonDecode(fromDbEncrypted(data.keys)); - return UnknownAccount( + final account = UnknownAccount( LPHAddress.from(data.id), fromDbEncrypted(data.name), fromDbEncrypted(data.displayName), unpackagePublicKey(keys["sg"]), unpackagePublicKey(keys["pub"]), ); + account.lastFetch = data.lastFetched; + return account; } - factory UnknownAccount.fromFriend(Friend friend) { - return UnknownAccount( - friend.id, - friend.name, - friend.displayName.value, - friend.keyStorage.signatureKey, - friend.keyStorage.publicKey, - ); + static Future fromFriend(Friend friend) async { + final keys = await friend.getKeys(); + return UnknownAccount(friend.id, friend.name, friend.displayName.value, keys.signatureKey, keys.publicKey); } - UnknownProfileData toData() => UnknownProfileData( - id: id.encode(), - name: dbEncrypted(name), - displayName: dbEncrypted(displayName), - keys: dbEncrypted(jsonEncode({ - "sg": packagePublicKey(signatureKey), - "pub": packagePublicKey(publicKey), - })), - ); + UnknownProfileData toData(DateTime lastFetched) => UnknownProfileData( + id: id.encode(), + name: dbEncrypted(name), + displayName: dbEncrypted(displayName), + keys: dbEncrypted(jsonEncode({"sg": packagePublicKey(signatureKey), "pub": packagePublicKey(publicKey)})), + lastFetched: lastFetched, + ); } diff --git a/lib/services/chat/vault_versioning_service.dart b/lib/services/chat/vault_versioning_service.dart new file mode 100644 index 00000000..0eb6b8cf --- /dev/null +++ b/lib/services/chat/vault_versioning_service.dart @@ -0,0 +1,19 @@ +import 'package:chat_interface/database/database.dart'; +import 'package:drift/drift.dart'; + +class VaultVersioningService { + // All vault types + static const vaultTypeFriend = "fr"; + static const vaultTypeGeneral = "gn"; + + /// Store or update new version for a type of vault and tag. + static Future storeOrUpdateVersion(String type, String tag, int version) async { + await db.setting.insertOnConflictUpdate(SettingData(key: "$type:$tag:version", value: version.toString())); + } + + /// Get the version for a type and tag. + static Future retrieveVersion(String type, String tag) async { + final result = await (db.setting.select()..where((tbl) => tbl.key.equals("$type:$tag:version"))).getSingleOrNull(); + return int.parse(result?.value ?? "0"); + } +} diff --git a/lib/connection/chat/conversation_listener.dart b/lib/services/connection/chat/conversation_listener.dart similarity index 58% rename from lib/connection/chat/conversation_listener.dart rename to lib/services/connection/chat/conversation_listener.dart index e254b95f..38224d3a 100644 --- a/lib/connection/chat/conversation_listener.dart +++ b/lib/services/connection/chat/conversation_listener.dart @@ -1,7 +1,6 @@ -import 'package:chat_interface/connection/connection.dart'; +import 'package:chat_interface/services/connection/connection.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; import 'package:chat_interface/util/logging_framework.dart'; -import 'package:get/get.dart'; class ConversationListener { static void setupListeners() { @@ -10,14 +9,9 @@ class ConversationListener { sendLog("received late from ${event.data["server"]}"); final server = event.data["server"]; if (event.data["error"]) { - Get.find().finishedLoading(server, {}, [], true); + ConversationController.finishedLoading(server, {}, [], true); } else { - Get.find().finishedLoading( - server, - event.data["info"], - event.data["missing"], - false, - ); + ConversationController.finishedLoading(server, event.data["info"], event.data["missing"], false); } }); } diff --git a/lib/connection/chat/live_share_listener.dart b/lib/services/connection/chat/live_share_listener.dart similarity index 62% rename from lib/connection/chat/live_share_listener.dart rename to lib/services/connection/chat/live_share_listener.dart index d809bdeb..cfe74f67 100644 --- a/lib/connection/chat/live_share_listener.dart +++ b/lib/services/connection/chat/live_share_listener.dart @@ -1,15 +1,14 @@ -import 'package:chat_interface/connection/connection.dart'; +import 'package:chat_interface/services/connection/connection.dart'; import 'package:chat_interface/controller/conversation/zap_share_controller.dart'; import 'package:chat_interface/util/logging_framework.dart'; -import 'package:get/get.dart'; void setupLiveshareListening() { connector.listen("transaction_send_part", (event) { - Get.find().onFilePartRequest(event); + ZapShareController.onFilePartRequest(event); }); connector.listen("transaction_end", (event) { sendLog("transaction cancelled :sad:"); - Get.find().onTransactionEnd(); + ZapShareController.onTransactionEnd(); }); } diff --git a/lib/connection/chat/message_listener.dart b/lib/services/connection/chat/message_listener.dart similarity index 53% rename from lib/connection/chat/message_listener.dart rename to lib/services/connection/chat/message_listener.dart index 7b2e6682..07507c97 100644 --- a/lib/connection/chat/message_listener.dart +++ b/lib/services/connection/chat/message_listener.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'package:chat_interface/connection/connection.dart' as cn; +import 'package:chat_interface/services/chat/message_service.dart'; +import 'package:chat_interface/services/connection/connection.dart' as cn; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; import 'package:chat_interface/controller/conversation/message_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; @@ -17,9 +18,9 @@ class MessageListener { static void setupMessageListener() { // Listen for one message cn.connector.listen("conv_msg", (event) async { - sendLog("received one message"); // Check if the conversation even exists on this account - final conversation = Get.find().conversations[LPHAddress.from(event.data["msg"]["cv"])]; + final (convId, extra) = MessageService.intoIdAndExtra(event.data["msg"]["cv"]); + final conversation = ConversationController.conversations[LPHAddress.from(convId)]; if (conversation == null) { sendLog("WARNING: invalid message, conversation not found"); return; @@ -35,37 +36,34 @@ class MessageListener { } // Tell the controller about the message in a different isolate - unawaited(Get.find().storeMessage(message, conversation)); + unawaited(MessageService.storeMessage(message, conversation, extra: extra)); }); // Listen for multiple messages (mp stands for multiple) - cn.connector.listen( - "conv_msg_mp", - (event) async { - // Check if the conversation even exists on this account - final conversation = Get.find().conversations[LPHAddress.from(event.data["cv"])]; - if (conversation == null) { - sendLog("WARNING: invalid message, conversation not found"); - return; - } + cn.connector.listen("conv_msg_mp", (event) async { + // Check if the conversation even exists on this account + final conversation = ConversationController.conversations[LPHAddress.from(event.data["cv"])]; + if (conversation == null) { + sendLog("WARNING: invalid message, conversation not found"); + return; + } - // Unpack all of the messages in an isolate - final messages = await unpackMessagesInIsolate(conversation, event.data["msgs"], includeSystemMessages: true); + // Unpack all of the messages in an isolate + final messages = await unpackMessagesInIsolate(conversation, event.data["msgs"], includeSystemMessages: true); - // Remove all messages with more than 5 attachments - messages.removeWhere((msg) { - if (msg.attachments.length > 5) { - sendLog("WARNING: invalid message received, dropping it (attachments > 5)"); - return true; - } + // Remove all messages with more than 5 attachments + messages.removeWhere((msg) { + if (msg.$1.attachments.length > 5) { + sendLog("WARNING: invalid message received, dropping it (attachments > 5)"); + return true; + } - return false; - }); + return false; + }); - // Store all of the messages in the local database - unawaited(Get.find().storeMessages(messages, conversation)); - }, - ); + // Store all of the messages in the local database + unawaited(MessageService.storeMessages(messages, conversation)); + }); } /// Unpack a message json in an isolate. @@ -76,26 +74,18 @@ class MessageListener { static Future unpackMessageInIsolate(Conversation conv, Map json) async { // Run an isolate to parse the message final copy = Conversation.copyWithoutKey(conv); - final (message, info) = await sodiumLib.runIsolated( - (sodium, keys, pairs) { - // Unpack the actual message - final (msg, info) = messageFromJson( - json, - sodium: sodium, - key: keys[0], - conversation: copy, - ); - - // Unpack the system message attachments in case needed - if (msg.type == MessageType.system) { - msg.decryptSystemMessageAttachments(keys[0], sodium); - } + final (message, info) = await sodiumLib.runIsolated((sodium, keys, pairs) { + // Unpack the actual message + final (msg, info) = messageFromJson(json, sodium: sodium, key: keys[0], conversation: copy); - // Return it to the main isolate - return (msg, info); - }, - secureKeys: [conv.key], - ); + // Unpack the system message attachments in case needed + if (msg.type == MessageType.system) { + msg.decryptSystemMessageAttachments(keys[0], sodium); + } + + // Return it to the main isolate + return (msg, info); + }, secureKeys: [conv.key]); // Verify the signature if (info != null) { @@ -111,56 +101,59 @@ class MessageListener { /// the signature is ran in the main isolate due to constraints with libsodium. /// /// For the future: TODO: Also process the signatures in the isolate by preloading profiles - static Future> unpackMessagesInIsolate(Conversation conversation, List json, {bool includeSystemMessages = false}) async { + static Future> unpackMessagesInIsolate( + Conversation conversation, + List json, { + bool includeSystemMessages = false, + }) async { // Unpack the messages in an isolate (in a separate thread yk) final copy = Conversation.copyWithoutKey(conversation); - final loadedMessages = await sodiumLib.runIsolated( - (sodium, keys, pairs) async { - // Process all messages - final list = <(Message, SymmetricSequencedInfo?)>[]; - for (var msgJson in json) { - final (message, info) = messageFromJson( - msgJson, - conversation: copy, - key: keys[0], - sodium: sodium, - ); - - // Don't render system messages that shouldn't be rendered (this is only for safety, should never actually happen) - if (message.type == MessageType.system && SystemMessages.messages[message.content]?.render == false && !includeSystemMessages) { - continue; - } - - // Decrypt system message attachments - if (message.type == MessageType.system) { - message.decryptSystemMessageAttachments(keys[0], sodium); - } - - list.add((message, info)); + final loadedMessages = await sodiumLib.runIsolated((sodium, keys, pairs) async { + // Process all messages + final list = <(Message, String, SymmetricSequencedInfo?)>[]; + for (var msgJson in json) { + final (message, info) = messageFromJson(msgJson, conversation: copy, key: keys[0], sodium: sodium); + + // Don't render system messages that shouldn't be rendered (this is only for safety, should never actually happen) + if (message.type == MessageType.system && + SystemMessages.messages[message.content]?.render == false && + !includeSystemMessages) { + continue; } - // Return the list to the main isolate - return list; - }, - secureKeys: [conversation.key], - ); + // Decrypt system message attachments + if (message.type == MessageType.system) { + message.decryptSystemMessageAttachments(keys[0], sodium); + } + + list.add((message, MessageService.intoIdAndExtra(msgJson["cv"]).$2, info)); + } + + // Return the list to the main isolate + return list; + }, secureKeys: [conversation.key]); // Verify the signature of all messages - for (var (msg, info) in loadedMessages) { + for (var (msg, _, info) in loadedMessages) { if (info != null) { await msg.verifySignature(info); } } - return loadedMessages.map((tuple) => tuple.$1).toList(); + return loadedMessages.map((tuple) => (tuple.$1, tuple.$2)).toList(); } /// Load a message from json (from the server) and get the corresponding [SymmetricSequencedInfo] (only if no system message). /// /// **Doesn't verify the signature** - static (Message, SymmetricSequencedInfo?) messageFromJson(Map json, {Conversation? conversation, SecureKey? key, Sodium? sodium}) { + static (Message, SymmetricSequencedInfo?) messageFromJson( + Map json, { + Conversation? conversation, + SecureKey? key, + Sodium? sodium, + }) { // Convert to message - conversation ??= Get.find().conversations[json["cv"]]!; + conversation ??= ConversationController.conversations[json["cv"]]!; final senderAddress = LPHAddress.from(json["sr"]); final message = Message( id: json["id"], diff --git a/lib/services/connection/chat/setup_listener.dart b/lib/services/connection/chat/setup_listener.dart new file mode 100644 index 00000000..ede97ccb --- /dev/null +++ b/lib/services/connection/chat/setup_listener.dart @@ -0,0 +1,20 @@ +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/controller/current/steps/account_step.dart'; + +void setupSetupListeners() { + //* New status + connector.listen("setup", (event) { + final data = event.data["data"]! as String; + + // Check if there even is a status that has been saved + if (data == "" || data == "-") { + StatusController.loadDefaultStatus(); + return; + } + + // Decrypt status with profile key + StatusController.fromStatusJson(decryptSymmetric(data, profileKey)); + }, afterSetup: true); +} diff --git a/lib/services/connection/chat/shared_space_listener.dart b/lib/services/connection/chat/shared_space_listener.dart new file mode 100644 index 00000000..12ffb3e3 --- /dev/null +++ b/lib/services/connection/chat/shared_space_listener.dart @@ -0,0 +1,45 @@ +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/square/shared_space_controller.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/services/squares/square_shared_space.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/web.dart'; + +class SharedSpaceListener { + static void setupListeners() { + // Listen for shared space update/create events + connector.listen("shared_space", (event) { + // Make sure the conversation is valid and exists on the client + final conversationAdr = LPHAddress.from(event.data["space"]["conv"]); + if (conversationAdr.isError()) { + return; + } + final conversation = ConversationController.conversations[conversationAdr]; + if (conversation == null) { + return; + } + + // Parse the space and make sure it's valid + final sharedSpace = SharedSpace.fromJson(event.data["space"], conversation.key); + if (sharedSpace.id != sharedSpace.container.roomId) { + sendLog("WARNING: invalid space id received in conversation with server ${conversation.id.server}"); + return; + } + + // Add it to the controller or update it + SharedSpaceController.addSharedSpace(conversationAdr, sharedSpace); + }); + + // Listen for shared space deletion events + connector.listen("shared_space_delete", (event) { + // Make sure the conversation is valid + final conversationAdr = LPHAddress.from(event.data["conv"]); + if (conversationAdr.isError()) { + return; + } + + // Delete the thing from the controller + SharedSpaceController.deleteSharedSpace(conversationAdr, event.data["id"]); + }); + } +} diff --git a/lib/connection/chat/status_listener.dart b/lib/services/connection/chat/status_listener.dart similarity index 63% rename from lib/connection/chat/status_listener.dart rename to lib/services/connection/chat/status_listener.dart index 6366441e..d26477a3 100644 --- a/lib/connection/chat/status_listener.dart +++ b/lib/services/connection/chat/status_listener.dart @@ -1,42 +1,35 @@ import 'dart:convert'; -import 'package:chat_interface/connection/connection.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/messaging.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/services/chat/conversation_member.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/member_controller.dart'; -import 'package:chat_interface/controller/spaces/space_container.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/controller/current/steps/account_step.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; -import 'package:get/get.dart'; import 'package:sodium_libs/sodium_libs.dart'; void setupStatusListener() { // Handle friend status change connector.listen("acc_st", (event) async { - final friend = handleStatus(event, false); + final friend = await handleStatus(event, false); if (friend == null) return; if (!friend.answerStatus) return; friend.answerStatus = false; - // Send back status - final controller = Get.find(); - // Get dm with friend - final dm = Get.find().conversations.values.firstWhere( - (element) => element.members.length == 2 && element.members.values.any((element) => element.address == friend.id), - ); + final dm = ConversationController.conversations.values.firstWhere( + (element) => element.members.length == 2 && element.members.values.any((element) => element.address == friend.id), + ); sendLog("sending status answer"); await postNodeJSON("/conversations/answer_status", { - "token": dm.token.toMap(), - "data": { - "status": controller.statusPacket(), - "data": controller.sharedContentPacket(), - } + "token": dm.token.toMap(dm.id), + "data": {"status": StatusController.statusPacket(), "data": StatusController.sharedContentPacket()}, }); }, afterSetup: true); @@ -53,19 +46,21 @@ void setupStatusListener() { }, afterSetup: true); } -Friend? handleStatus(Event event, bool own) { +Future handleStatus(Event event, bool own) async { final message = event.data["st"] as String; // Load own status when the packet specifies it - final statusController = Get.find(); - final controller = Get.find(); if (own) { - controller.friends[StatusController.ownAddress]!.loadStatus(message); - statusController.fromStatusJson(decryptSymmetric(message, profileKey)); + await FriendController.friends[StatusController.ownAddress]!.loadStatus(message); + StatusController.fromStatusJson(decryptSymmetric(message, profileKey)); // Load own shared content - final (container, shouldUpdate) = _dataToContainer(statusController.ownContainer.value, event.data["d"], profileKey); + final (container, shouldUpdate) = _dataToContainer( + StatusController.ownContainer.value, + event.data["d"], + profileKey, + ); if (shouldUpdate) { - statusController.ownContainer.value = container; + StatusController.ownContainer.value = container; } return null; @@ -76,8 +71,7 @@ Friend? handleStatus(Event event, bool own) { final owner = LPHAddress.from(event.data["o"] as String); // Get conversation from the status packet - final convController = Get.find(); - final conversation = convController.conversations[convId]; + final conversation = ConversationController.conversations[convId]; if (conversation == null) { sendLog("conversation not found for status packet $convId"); return null; @@ -92,23 +86,27 @@ Friend? handleStatus(Event event, bool own) { sendLog("member $owner not found in conversation $convId (status packet)"); return null; } - final friend = controller.friends[member.address]; + final friend = FriendController.friends[member.address]; if (friend == null) { sendLog("account ${member.address.toString()} isn't a friend (status packet)"); return null; } // Load the status - friend.loadStatus(message); + await friend.loadStatus(message); // Extract shared content - final (container, shouldUpdate) = _dataToContainer(statusController.sharedContent[friend.id], event.data["d"], friend.keyStorage.profileKey); + final (container, shouldUpdate) = _dataToContainer( + StatusController.sharedContent[friend.id], + event.data["d"], + (await friend.getKeys()).profileKey, + ); if (shouldUpdate) { if (container == null) { - final container = statusController.sharedContent.remove(friend.id); + final container = StatusController.sharedContent.remove(friend.id); container?.onDrop(); } else { - statusController.sharedContent[friend.id] = container; + StatusController.sharedContent[friend.id] = container; } } diff --git a/lib/connection/chat/stored_actions_listener.dart b/lib/services/connection/chat/stored_actions_listener.dart similarity index 61% rename from lib/connection/chat/stored_actions_listener.dart rename to lib/services/connection/chat/stored_actions_listener.dart index 46800d99..51da3d84 100644 --- a/lib/connection/chat/stored_actions_listener.dart +++ b/lib/services/connection/chat/stored_actions_listener.dart @@ -1,14 +1,16 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:chat_interface/connection/connection.dart'; -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/chat/setup_listener.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/account/friends/requests_controller.dart'; +import 'package:chat_interface/controller/current/tasks/vault_sync_task.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/chat/unknown_service.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/util/constants.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/account/requests_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/member_controller.dart'; import 'package:chat_interface/database/database_entities.dart' as model; import 'package:chat_interface/controller/current/steps/key_step.dart'; import 'package:chat_interface/database/trusted_links.dart'; @@ -16,8 +18,8 @@ import 'package:chat_interface/standards/server_stored_information.dart'; import 'package:chat_interface/util/web.dart'; import 'package:get/get.dart'; -import '../../controller/current/status_controller.dart'; -import '../../util/logging_framework.dart'; +import '../../../controller/current/status_controller.dart'; +import '../../../util/logging_framework.dart'; part 'stored_actions_util.dart'; @@ -51,7 +53,7 @@ Future processStoredAction(Map action) async { // Parse the json and get the sender final json = jsonDecode(extracted.text); final address = LPHAddress.from(json["s"]); - final sender = Get.find().friends[address]; + final sender = FriendController.friends[address]; if (sender == null) { sendLog("ERROR: sender of authenticated stored action isn't a friend"); return false; @@ -73,15 +75,20 @@ Future processStoredAction(Map action) async { } // Verify the signature - if (!extracted.verifySignature(sender.keyStorage.signatureKey)) { + if (!extracted.verifySignature((await sender.getKeys()).signatureKey)) { sendLog("ERROR: signature of authenticated stored action is invalid"); return false; } // Update the last receive date to the latest sequence number - final result = await FriendsVault.setReceiveDate(sender.vaultId, DateTime.fromMillisecondsSinceEpoch(extracted.seq)); + final result = await FriendsVault.setReceiveDate( + sender.vaultId, + DateTime.fromMillisecondsSinceEpoch(extracted.seq), + ); if (!result) { - sendLog("WARNING: the last receive date couldn't be updated, this might cause future replay attacks, ignoring for now"); + sendLog( + "WARNING: the last receive date couldn't be updated, this might cause future replay attacks, ignoring for now", + ); return false; } @@ -100,7 +107,11 @@ Future processStoredAction(Map action) async { } // Handle normal stored actions - final payload = decryptAsymmetricAnonymous(asymmetricKeyPair.publicKey, asymmetricKeyPair.secretKey, action["payload"]); + final payload = decryptAsymmetricAnonymous( + asymmetricKeyPair.publicKey, + asymmetricKeyPair.secretKey, + action["payload"], + ); if (payload == "") { return true; } @@ -126,43 +137,48 @@ Future _handleFriendRequestAction(String actionId, Map js return true; } + // Query the guy + final account = await UnknownService.loadUnknownProfile(address); + if (account == null) { + sendLog("invalid friend request: couldn't find sender"); + return false; + } + // Check the signature and stuff - final publicKey = unpackagePublicKey(json["pub"]); - final signaturePub = unpackagePublicKey(json["sg"]); - final statusController = Get.find(); - final signedMessage = statusController.name.value; - final result = decryptAsymmetricAuth(publicKey, asymmetricKeyPair.secretKey, json["s"]); + final signedMessage = StatusController.name.value; + final result = decryptAsymmetricAuth(account.publicKey, asymmetricKeyPair.secretKey, json["s"]); if (!result.success || result.message != signedMessage) { sendLog("invalid friend request: invalid signature"); return true; } - // Check if the current account already sent this account a friend request (-> add friend) - final requestController = Get.find(); - var request = requestController.requestsSent[address]; - + // Add as a friend in case there is an existing request that was sent by the current client + var request = RequestController.requestsSent[address]; if (request != null) { // This request doesn't have the right key storage yet - request.keyStorage.publicKey = publicKey; + request.keyStorage.publicKey = account.publicKey; + request.keyStorage.signatureKey = account.signatureKey; request.keyStorage.profileKeyPacked = json["pf"]; request.keyStorage.unpackedProfileKey = unpackageSymmetricKey(json["pf"]); request.keyStorage.storedActionKey = json["sa"]; - // Add friend - final controller = Get.find(); - await controller.addFromRequest(request); + // Add friend to the vault + final error = await FriendsVault.updateFriend(request.friend); + if (error != null) { + sendLog("couldn't accept friend request: $error"); + } return true; } // Check if the request is already in the list - if (requestController.requests[address] != null) { + if (RequestController.requests[address] != null) { sendLog("invalid friend request: already in list"); return true; } // Check if the guy is already a friend - if (Get.find().friends.values.any((element) => element.id == address)) { + if (FriendController.friends.values.any((element) => element.id == address)) { sendLog("invalid friend request: already a friend"); return true; } @@ -171,30 +187,26 @@ Future _handleFriendRequestAction(String actionId, Map js final profileKey = unpackageSymmetricKey(json["pf"]); request = Request( address, - json["name"], - json["dname"], + account.name, + account.displayName, "", - KeyStorage(publicKey, signaturePub, profileKey, json["sa"]), + KeyStorage(account.publicKey, account.signatureKey, profileKey, json["sa"]), DateTime.now().millisecondsSinceEpoch, ); - final vaultId = await FriendsVault.store(request.toStoredPayload(false)); - if (vaultId == null) { - sendLog("couldn't store in vault: something happened"); - return true; + // Store the friend in the vault + final error = await FriendsVault.storeReceivedRequest(request); + if (error != null) { + sendLog("couldn't store in vault: $error"); } - // Add friend request - request.vaultId = vaultId; - Get.find().addRequest(request); - return true; } //* Conversation opening Future _handleConversationOpening(String actionId, Map actionJson) async { sendLog("opening conversation with ${actionJson["s"]}"); - final friend = Get.find().friends[LPHAddress.from(actionJson["s"])]; + final friend = FriendController.friends[LPHAddress.from(actionJson["s"])]; if (friend == null) { sendLog("invalid conversation opening: friend doesn't exist"); return true; @@ -202,7 +214,6 @@ Future _handleConversationOpening(String actionId, Map ac // Activate the token from the request final token = jsonDecode(actionJson["token"]); - sendLog(token); final json = await postNodeJSON("/conversations/activate", {"token": token}); if (!json["success"]) { sendLog("couldn't activate conversation: ${json["error"]}"); @@ -211,29 +222,24 @@ Future _handleConversationOpening(String actionId, Map ac token["token"] = json["token"]; // Set new token (from activation request) final key = unpackageSymmetricKey(actionJson["key"]); - final members = []; - for (var memberData in json["members"]) { - sendLog(memberData); - final memberContainer = MemberContainer.decrypt(memberData["data"], key); - members.add(Member(LPHAddress.from(memberData["id"]), memberContainer.id, MemberRole.fromValue(memberData["rank"]))); - } - final container = ConversationContainer.decrypt(json["data"], key); - final convToken = ConversationToken.fromJson(token); - await Get.find().addCreated( - Conversation( - LPHAddress.from(actionJson["id"]), - "", - model.ConversationType.values[json["type"]], - convToken, - container, - packageSymmetricKey(key), - 0, - DateTime.now().millisecondsSinceEpoch, - ), - members, + final conversation = Conversation( + LPHAddress.from(actionJson["id"]), + "", + model.ConversationType.values[json["type"]], + ConversationToken.fromJson(token), + ConversationContainer.decrypt(json["data"], key), + packageSymmetricKey(key), + 0, + DateTime.now().millisecondsSinceEpoch, + ConversationReads.fromContainer(""), ); - subscribeToConversation(convToken, deletions: false); + + // Add to vault + final (error, _) = await addToVault(Constants.vaultConversationTag, conversation.toJson()); + if (error != null) { + sendLog("WARNING: Conversation couldn't be added to vault: $error"); + } return true; } @@ -241,12 +247,13 @@ Future _handleConversationOpening(String actionId, Map ac //* Friend removal Future _handleFriendRemoval(String actionId, Map actionJson) async { sendLog("deleting friend ${actionJson["s"]}"); - final friend = Get.find().friends[LPHAddress.from(actionJson["s"])]; + final friend = FriendController.friends[LPHAddress.from(actionJson["s"])]; if (friend == null) { sendLog("invalid friend deletion: friend doesn't exist"); return true; } // Remove the friend without asking - return friend.remove(false.obs, removeAction: false); + final error = await friend.remove(removeAction: false); + return error != null; } diff --git a/lib/connection/chat/stored_actions_util.dart b/lib/services/connection/chat/stored_actions_util.dart similarity index 69% rename from lib/connection/chat/stored_actions_util.dart rename to lib/services/connection/chat/stored_actions_util.dart index 5dfe99a6..40bc605b 100644 --- a/lib/connection/chat/stored_actions_util.dart +++ b/lib/services/connection/chat/stored_actions_util.dart @@ -11,30 +11,34 @@ Future deleteStoredAction(String id) async { return true; } -Future sendAuthenticatedStoredAction(Friend friend, Map payload) async { +/// Send an authenticated stored action to a friend. +/// +/// Returns an error if there was one. +Future sendAuthenticatedStoredAction(Friend friend, Map payload) async { // Set the sender payload["s"] = StatusController.ownAddress.encode(); // Make sure the server is trusted if (!await TrustedLinkHelper.askToAddIfNotAdded(friend.id.server)) { - sendLog("COULDN'T SEND STORED ACTION: domain not trusted"); - return false; + return "error.untrusted_server".trParams({"domain": friend.id.server}); } - // Send stored action + // Send the authenticated stored action + final keys = await friend.getKeys(); final json = await postAddress(friend.id.server, "/account/stored_actions/send_auth", { "account": friend.id.id, - // actual data (safe from replay attacks thanks to sequence numbers) - "payload": AsymmetricSequencedInfo.builder(jsonEncode(payload), DateTime.now().millisecondsSinceEpoch).finish(friend.keyStorage.publicKey), - "key": friend.keyStorage.storedActionKey, + // actual data (safe from replay attacks thanks to sequence numbers and the sender saving the last receive time) + "payload": AsymmetricSequencedInfo.builder( + jsonEncode(payload), + DateTime.now().millisecondsSinceEpoch, + ).finish(keys.publicKey), + "key": keys.storedActionKey, }); - if (!json["success"]) { - sendLog("couldn't send stored action: ${json["error"]}"); - return false; + return json["error"]; } - return true; + return null; } /// Send a stored action to someone using their address (returns null when successful) diff --git a/lib/connection/connection.dart b/lib/services/connection/connection.dart similarity index 80% rename from lib/connection/connection.dart rename to lib/services/connection/connection.dart index ad36d3c2..accc39f4 100644 --- a/lib/connection/connection.dart +++ b/lib/services/connection/connection.dart @@ -1,15 +1,15 @@ import 'dart:async'; import 'dart:convert'; -import 'package:chat_interface/connection/chat/conversation_listener.dart'; -import 'package:chat_interface/connection/encryption/aes.dart'; -import 'package:chat_interface/connection/encryption/hash.dart'; -import 'package:chat_interface/connection/encryption/rsa.dart'; -import 'package:chat_interface/connection/chat/live_share_listener.dart'; -import 'package:chat_interface/connection/chat/message_listener.dart'; -import 'package:chat_interface/connection/chat/status_listener.dart'; -import 'package:chat_interface/connection/chat/stored_actions_listener.dart'; -import 'package:chat_interface/connection/spaces/space_connection.dart'; +import 'package:chat_interface/services/connection/chat/conversation_listener.dart'; +import 'package:chat_interface/services/connection/chat/shared_space_listener.dart'; +import 'package:chat_interface/util/encryption/aes.dart'; +import 'package:chat_interface/util/encryption/hash.dart'; +import 'package:chat_interface/util/encryption/rsa.dart'; +import 'package:chat_interface/services/connection/chat/live_share_listener.dart'; +import 'package:chat_interface/services/connection/chat/message_listener.dart'; +import 'package:chat_interface/services/connection/chat/status_listener.dart'; +import 'package:chat_interface/services/connection/chat/stored_actions_listener.dart'; import 'package:chat_interface/controller/current/connection_controller.dart'; import 'package:chat_interface/main.dart'; import 'package:chat_interface/pages/status/setup/setup_manager.dart'; @@ -77,10 +77,8 @@ class Connector { } _connected = true; - connection!.sendText(jsonEncode({ - "token": token, - "attachments": base64Encode(encryptedKey), - })); + // Send the first request for authentication + connection!.sendText(jsonEncode({"token": token, "attachments": base64Encode(encryptedKey)})); connection!.events.listen( (message) { @@ -105,7 +103,8 @@ class Connector { sendLog("FAILED TO DECRYPT MESSAGE"); sendLog( - "This is most likely due to another client being in the same network, connected over the same port as you are. We can't do anything about this and this will not occur in production."); + "This is most likely due to another client being in the same network, connected over the same port as you are. We can't do anything about this and this will not occur in production.", + ); e.printError(); return; } @@ -147,7 +146,7 @@ class Connector { } // Add it to the after setup queue (in case it is an after setup handler) - if (_afterSetup[event.name] == true && !SetupManager.setupFinished && !Get.find().connected.value) { + if (_afterSetup[event.name] == true && !SetupManager.setupFinished && !ConnectionController.connected.value) { _afterSetupQueue.add(event); return; } @@ -162,7 +161,7 @@ class Connector { onDone(false); } if (restart) { - Get.find().connectionStopped(); + ConnectionController.connectionStopped(); } initialized = false; }, @@ -220,6 +219,11 @@ class Connector { return null; } + // Send a confirmation in debug mode + if (isDebug) { + sendLog("[$url] sending: ${action.action}"); + } + // Generate a valid response id var responseId = getRandomString(5); while (_responders.containsKey(responseId)) { @@ -253,23 +257,20 @@ class Connector { if (responseId == null) { completer.complete(null); } else { - Timer( - timeout ?? Duration(seconds: 10), - () { - // If the event already received a response, it doesn't matter - if (completer.isCompleted) { - return; - } + Timer(timeout ?? Duration(seconds: 10), () { + // If the event already received a response, it doesn't matter + if (completer.isCompleted) { + return; + } - // Attach an error handler to make sure the error is logged when the server doesn't respond - _responders[responseId] = (event) { - sendLog("Event ${event.name} received even though there was an error with this previously."); - }; + // Attach an error handler to make sure the error is logged when the server doesn't respond + _responders[responseId] = (event) { + sendLog("Event ${event.name} received even though there was an error with this previously."); + }; - sendLog("Response to ${action.action} timed out"); - completer.complete(null); - }, - ); + sendLog("Response to ${action.action} timed out"); + completer.complete(null); + }); } return completer.future; } @@ -293,9 +294,7 @@ Future startConnection(String node, String connectionToken) async { MessageListener.setupMessageListener(); ConversationListener.setupListeners(); setupLiveshareListening(); - - // Add listeners for Spaces (unrelated to chat node) - setupSpaceListeners(); + SharedSpaceListener.setupListeners(); return true; } diff --git a/lib/connection/messaging.dart b/lib/services/connection/messaging.dart similarity index 80% rename from lib/connection/messaging.dart rename to lib/services/connection/messaging.dart index 80ab0a7b..498caf2a 100644 --- a/lib/connection/messaging.dart +++ b/lib/services/connection/messaging.dart @@ -16,10 +16,7 @@ class Event { Event.fromMap(Map map) : this(map['name'], map['data']); Event.fromJson(String json) : this.fromMap(jsonDecode(json)); - Map toMap() => { - 'name': name, - 'data': data, - }; + Map toMap() => {'name': name, 'data': data}; String toJson() => jsonEncode(toMap()); } @@ -37,10 +34,10 @@ class ServerAction { ServerAction.fromJson(String json) : this.fromMap(jsonDecode(json)); Map toMap() => { - 'action': action, - 'lc': localeString(Get.locale ?? Get.fallbackLocale ?? const Locale("en", "US")), - 'data': data, - }; + 'action': action, + 'lc': localeString(Get.locale ?? Get.fallbackLocale ?? const Locale("en", "US")), + 'data': data, + }; String toJson() => jsonEncode(toMap()); } diff --git a/lib/services/spaces/space_connection.dart b/lib/services/spaces/space_connection.dart new file mode 100644 index 00000000..1348509f --- /dev/null +++ b/lib/services/spaces/space_connection.dart @@ -0,0 +1,76 @@ +import 'package:chat_interface/controller/current/status_controller.dart'; +import 'package:chat_interface/controller/spaces/studio/studio_controller.dart'; +import 'package:chat_interface/controller/spaces/studio/studio_track_controller.dart'; +import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; +import 'package:chat_interface/controller/spaces/warp_controller.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; +import 'package:chat_interface/main.dart'; +import 'package:chat_interface/services/spaces/space_message_service.dart'; +import 'package:chat_interface/services/spaces/studio/studio_service.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_service.dart'; +import 'package:chat_interface/services/spaces/warp/warp_service.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:get/get.dart'; + +class SpaceConnection { + /// Connector the the space node. + static Connector? spaceConnector; + + /// Connect to the space node. + static Future createSpaceConnection(String domain, String token) async { + spaceConnector = Connector(); + final success = await spaceConnector!.connect( + "${isHttps ? "wss://" : "ws://"}$domain/gateway", + token, + restart: false, + onDone: ((error) { + if (error) { + showErrorPopup("error", "spaces.connection_error".tr); + } + + // Tell all controllers about the leaving of the space + StatusController.stopSharing(); + TabletopController.resetControllerState(); + SpaceController.leaveSpace(error: error); + WarpController.resetControllerState(); + SpaceMemberController.onDisconnect(); + StudioController.handleDisconnect(); + StudioTrackController.handleDisconnect(); + }), + ); + + // Setup all the listeners for the connector + setupSpaceListeners(); + + // Set up everything for the connection + TabletopController.resetControllerState(); + + return success; + } + + /// Disconnect from the Space. + static void disconnect() { + spaceConnector?.disconnect(); + } + + /// Setup listeners for space events. + static void setupSpaceListeners() { + // Listen for room data changes + spaceConnector!.listen("room_data", (event) => _handleRoomData(event)); // Sent on change + spaceConnector!.listen("room_info", (event) => _handleRoomData(event)); // Sent on join + + StudioService.setupStudioHandlers(spaceConnector!); + TabletopService.setupTabletopListeners(spaceConnector!); + SpaceMessageService.setupSpaceMessageListeners(spaceConnector!); + WarpService.setupWarpListeners(spaceConnector!); + } + + /// Sends the room data to all controllers + static void _handleRoomData(Event event) { + SpaceController.updateStartDate(DateTime.fromMillisecondsSinceEpoch(event.data["start"])); + SpaceMemberController.onMembersChanged(event.data["members"]); + } +} diff --git a/lib/controller/spaces/space_container.dart b/lib/services/spaces/space_container.dart similarity index 63% rename from lib/controller/spaces/space_container.dart rename to lib/services/spaces/space_container.dart index 2e6b1e03..743b2744 100644 --- a/lib/controller/spaces/space_container.dart +++ b/lib/services/spaces/space_container.dart @@ -3,11 +3,10 @@ import 'dart:convert'; import 'package:chat_interface/util/web.dart'; import 'package:http/http.dart' as http; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; -import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:sodium_libs/sodium_libs.dart'; class SpaceConnectionContainer extends ShareContainer { @@ -15,14 +14,14 @@ class SpaceConnectionContainer extends ShareContainer { final String roomId; // Token required for joining (even though it's not really a token) final SecureKey key; // Symmetric key - final info = Rx(null); + final info = signal(null); int errorCount = 0; Timer? _timer; bool get cancelled => _timer == null; SpaceConnectionContainer(this.node, this.roomId, this.key, Friend? sender) : super(sender, ShareType.space); SpaceConnectionContainer.fromJson(Map json, [Friend? sender]) - : this(json["node"], json["id"], unpackageSymmetricKey(json["key"]), sender); + : this(json["node"], json["id"], unpackageSymmetricKey(json["key"]), sender); @override Map toMap() { @@ -42,9 +41,7 @@ class SpaceConnectionContainer extends ShareContainer { try { req = await http.post( Uri.parse("${nodeProtocol()}$node/info"), - headers: { - 'Content-Type': 'application/json', - }, + headers: {'Content-Type': 'application/json'}, body: jsonEncode({"room": roomId}), ); } catch (e) { @@ -84,3 +81,37 @@ class SpaceConnectionContainer extends ShareContainer { return info.value!; } } + +class SpaceInfo { + late bool exists; + bool error = false; + late DateTime start; + final List friends = []; + late final List members; + + SpaceInfo(this.start, this.members) { + error = false; + exists = true; + for (var member in members) { + final friend = FriendController.friends[member]; + if (friend != null) friends.add(friend); + } + } + + SpaceInfo.fromJson(SpaceConnectionContainer container, Map json) { + start = DateTime.fromMillisecondsSinceEpoch(json["start"]); + members = List.from(json["members"].map((e) => LPHAddress.from(decryptSymmetric(e, container.key)))); + exists = true; + + for (var member in members) { + final friend = FriendController.friends[member]; + if (friend != null) friends.add(friend); + } + } + + SpaceInfo.notLoaded({bool wasError = false}) { + exists = false; + error = wasError; + members = []; + } +} diff --git a/lib/controller/spaces/spaces_message_controller.dart b/lib/services/spaces/space_message_provider.dart similarity index 60% rename from lib/controller/spaces/spaces_message_controller.dart rename to lib/services/spaces/space_message_provider.dart index 683b1b7b..1df88ce7 100644 --- a/lib/controller/spaces/spaces_message_controller.dart +++ b/lib/services/spaces/space_message_provider.dart @@ -1,64 +1,22 @@ -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/spaces/space_connection.dart'; import 'package:chat_interface/controller/conversation/message_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; -import 'package:chat_interface/connection/messaging.dart'; -import 'package:chat_interface/controller/spaces/ringing_manager.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; import 'package:chat_interface/controller/conversation/system_messages.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; import 'package:chat_interface/main.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; import 'package:chat_interface/standards/server_stored_information.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; import 'package:get/get.dart'; import 'package:sodium_libs/sodium_libs.dart'; -class SpacesMessageController extends GetxController { - SpacesMessageProvider provider = SpacesMessageProvider(); - - /// Clear the chat log for the provider (aka reset the provider). - void clearProvider() { - provider = SpacesMessageProvider(); - } - - /// Called when the space is started to load the first messages. - void open() { - provider.loadNewMessagesTop(date: DateTime.now().millisecondsSinceEpoch); - } - - /// Add a message to the Spaces chat. - /// - /// Also plays a notification sound if desired by the user. - void addMessage(Message message) { - // Play a notification sound when a new message arrives - RingingManager.playNotificationSound(); - - // Check if it is a system message and if it should be rendered or not - if (message.type == MessageType.system) { - if (SystemMessages.messages[message.content]?.render == true) { - provider.addMessageToBottom(message); - } - } else { - // Store normal type of message - if (provider.messages.isNotEmpty && provider.messages[0].id != message.id) { - provider.addMessageToBottom(message); - } else if (provider.messages.isEmpty) { - provider.addMessageToBottom(message); - } - } - - // Handle system messages - if (message.type == MessageType.system) { - SystemMessages.messages[message.content]?.handle(message, provider); - } - } -} - class SpacesMessageProvider extends MessageProvider { @override Future loadMessageFromServer(String id, {bool init = true}) async { - final event = await spaceConnector.sendActionAndWait(ServerAction("msg_get", id)); + final event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("msg_get", id)); if (event == null) { return null; } @@ -69,7 +27,7 @@ class SpacesMessageProvider extends MessageProvider { @override Future<(List?, bool)> loadMessagesAfter(int time) async { // Load the messages from the server using the list_before endpoint - final event = await spaceConnector.sendActionAndWait(ServerAction("msg_list_after", time)); + final event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("msg_list_after", time)); if (event == null) { sendLog("something went wrong"); return (null, true); @@ -96,7 +54,7 @@ class SpacesMessageProvider extends MessageProvider { sendLog("load messages before"); // Load messages from the server - final event = await spaceConnector.sendActionAndWait(ServerAction("msg_list_before", time)); + final event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("msg_list_before", time)); if (event == null) { sendLog("nothing"); return (null, true); @@ -120,7 +78,7 @@ class SpacesMessageProvider extends MessageProvider { @override Future deleteMessage(Message message) async { - final event = await spaceConnector.sendActionAndWait(ServerAction("msg_delete", message.id)); + final event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("msg_delete", message.id)); if (event == null) { return "server.error".tr; } @@ -132,7 +90,7 @@ class SpacesMessageProvider extends MessageProvider { @override Future deleteMessageFromClient(String id) async { - messages.removeWhere((element) => element.id == id); + messages.remove(id); return false; } @@ -144,37 +102,29 @@ class SpacesMessageProvider extends MessageProvider { /// For the future: TODO: Also process the signatures in the isolate by preloading profiles Future> _processMessages(List json) async { // Unpack the messages in an isolate (in a separate thread yk) - final members = Get.find().memberIds; - final loadedMessages = await sodiumLib.runIsolated( - (sodium, keys, pairs) async { - // Process all messages - final list = <(Message, SymmetricSequencedInfo?)>[]; - for (var msgJson in json) { - final (message, info) = messageFromJson( - msgJson, - key: keys[0], - sodium: sodium, - members: members, - ); - - // Don't render system messages that shouldn't be rendered (this is only for safety, should never actually happen) - if (message.type == MessageType.system && SystemMessages.messages[message.content]?.render == false) { - continue; - } - - // Decrypt system message attachments - if (message.type == MessageType.system) { - message.decryptSystemMessageAttachments(keys[0], sodium); - } - - list.add((message, info)); + final members = SpaceMemberController.memberIds; + final loadedMessages = await sodiumLib.runIsolated((sodium, keys, pairs) async { + // Process all messages + final list = <(Message, SymmetricSequencedInfo?)>[]; + for (var msgJson in json) { + final (message, info) = messageFromJson(msgJson, key: keys[0], sodium: sodium, members: members); + + // Don't render system messages that shouldn't be rendered (this is only for safety, should never actually happen) + if (message.type == MessageType.system && SystemMessages.messages[message.content]?.render == false) { + continue; } - // Return the list to the main isolate - return list; - }, - secureKeys: [SpacesController.key!], - ); + // Decrypt system message attachments + if (message.type == MessageType.system) { + message.decryptSystemMessageAttachments(keys[0], sodium); + } + + list.add((message, info)); + } + + // Return the list to the main isolate + return list; + }, secureKeys: [SpaceController.key!]); // Init the attachments on all messages and verify signatures for (var (msg, info) in loadedMessages) { @@ -194,7 +144,7 @@ class SpacesMessageProvider extends MessageProvider { /// For the future also: TODO: Unpack the signature in a different isolate static Future unpackMessageInIsolate(Map json) async { // Run an isolate to parse the message - final (message, info) = await _extractMessageIsolate(json, Get.find().memberIds, SpacesController.key!); + final (message, info) = await _extractMessageIsolate(json, SpaceMemberController.memberIds, SpaceController.key!); // Verify the signature if (info != null) { @@ -209,21 +159,13 @@ class SpacesMessageProvider extends MessageProvider { Map members, SecureKey key, ) { - return sodiumLib.runIsolated( - (sodium, keys, pairs) { - // Unpack the actual message - final (msg, info) = messageFromJson( - json, - sodium: sodium, - key: keys[0], - members: members, - ); - - // Return it to the main isolate - return (msg, info); - }, - secureKeys: [key], - ); + return sodiumLib.runIsolated((sodium, keys, pairs) { + // Unpack the actual message + final (msg, info) = messageFromJson(json, sodium: sodium, key: keys[0], members: members); + + // Return it to the main isolate + return (msg, info); + }, secureKeys: [key]); } /// Load a message from json (from the server) and get the corresponding [SymmetricSequencedInfo] (only if no system message). @@ -237,12 +179,12 @@ class SpacesMessageProvider extends MessageProvider { Sodium? sodium, }) { // Convert to message - members ??= Get.find().memberIds; + members ??= SpaceMemberController.memberIds; LPHAddress account; if (json["sr"] == MessageController.systemSender.encode()) { account = MessageController.systemSender; } else { - key ??= SpacesController.key!; + key ??= SpaceController.key!; account = LPHAddress.from(decryptSymmetric(json["sr"] as String, key, sodium)); } var message = Message( @@ -268,7 +210,7 @@ class SpacesMessageProvider extends MessageProvider { } // Decrypt content and check signature - key ??= SpacesController.key!; + key ??= SpaceController.key!; final info = SymmetricSequencedInfo.extract(message.content, key, sodium); message.content = info.text; message.loadContent(); @@ -278,12 +220,12 @@ class SpacesMessageProvider extends MessageProvider { @override SecureKey encryptionKey() { - return SpacesController.key!; + return SpaceController.key!; } @override Future<(String, int)?> getTimestamp() async { - final event = await spaceConnector.sendActionAndWait(ServerAction("msg_timestamp", {})); + final event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("msg_timestamp", {})); if (event == null) { return null; } @@ -293,11 +235,10 @@ class SpacesMessageProvider extends MessageProvider { } @override - Future handleMessageSend(String timeToken, String data) async { - final event = await spaceConnector.sendActionAndWait(ServerAction("msg_send", { - "token": timeToken, - "data": data, - })); + Future handleMessageSend(String timeToken, String data, int stamp) async { + final event = await SpaceConnection.spaceConnector!.sendActionAndWait( + ServerAction("msg_send", {"token": timeToken, "data": data}), + ); // Return a server error if the thing didn't work if (event == null) { diff --git a/lib/services/spaces/space_message_service.dart b/lib/services/spaces/space_message_service.dart new file mode 100644 index 00000000..a3dbf894 --- /dev/null +++ b/lib/services/spaces/space_message_service.dart @@ -0,0 +1,29 @@ +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/services/spaces/space_message_provider.dart'; +import 'package:chat_interface/util/logging_framework.dart'; + +class SpaceMessageService { + static void setupSpaceMessageListeners(Connector connector) { + // Listen for deletions + connector.listen("msg", (event) async { + // Make sure we're actually in a space right now + if (!SpaceController.connected.value || SpaceController.key == null) { + sendLog("WARNING: received space message even though not in space"); + return; + } + + // Unpack the message in a different isolate (to prevent lag) + final message = await SpacesMessageProvider.unpackMessageInIsolate(event.data["msg"]); + + // Check if there are too many attachments + if (message.attachments.length > 5) { + sendLog("WARNING: invalid message, more than 5 attachments"); + return; + } + + // Tell the controller about the message + SpaceController.addMessage(message); + }); + } +} diff --git a/lib/services/spaces/space_service.dart b/lib/services/spaces/space_service.dart index bcc77066..4cc1d4d6 100644 --- a/lib/services/spaces/space_service.dart +++ b/lib/services/spaces/space_service.dart @@ -1,14 +1,20 @@ -import 'package:chat_interface/connection/encryption/signatures.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; -import 'package:chat_interface/connection/spaces/space_connection.dart'; +import 'dart:async'; + +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/controller/spaces/studio/studio_controller.dart'; +import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; +import 'package:chat_interface/pages/chat/chat_page_desktop.dart'; +import 'package:chat_interface/util/encryption/signatures.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/controller/current/steps/key_step.dart'; -import 'package:chat_interface/controller/spaces/space_container.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/main.dart'; import 'package:chat_interface/util/logging_framework.dart'; import 'package:chat_interface/util/web.dart'; -import 'package:chat_interface/connection/messaging.dart' as msg; +import 'package:chat_interface/services/connection/messaging.dart' as msg; import 'package:get/get.dart'; import 'package:sodium_libs/sodium_libs.dart'; @@ -19,9 +25,7 @@ class SpaceService { final spaceJson = await postAddress( "${nodeProtocol()}$domain", "/enc/join", - { - "id": spaceId, - }, + {"id": spaceId}, noApiVersion: true, checkProtocol: false, ); @@ -41,15 +45,11 @@ class SpaceService { /// Create a new space (returns an error if there was one) static Future<(SpaceConnectionContainer?, String?)> createSpace() async { // Get a connection token for Spaces (required to create a new space) - final json = await postAuthorizedJSON( - "/node/connect", - { - "tag": appTagSpaces, - "token": refreshToken, - "extra": "", - }, - checkProtocol: false, - ); + final json = await postAuthorizedJSON("/node/connect", { + "tag": appTagSpaces, + "token": refreshToken, + "extra": "", + }, checkProtocol: false); if (!json["success"]) { return (null, json["error"] as String); } @@ -58,9 +58,7 @@ class SpaceService { final spaceJson = await postAddress( "${nodeProtocol()}${json["domain"]}", "/enc/create", - { - "token": json["token"], - }, + {"token": json["token"]}, noApiVersion: true, checkProtocol: false, ); @@ -70,7 +68,13 @@ class SpaceService { // Connect to the space final key = randomSymmetricKey(); - final error = await _connectToRoom(json["domain"], spaceJson["token"], spaceJson["client"], spaceJson["space"], key); + final error = await _connectToRoom( + json["domain"], + spaceJson["token"], + spaceJson["client"], + spaceJson["space"], + key, + ); if (error != null) { return (null, error); } @@ -81,22 +85,40 @@ class SpaceService { } /// Returns an error if there was one - static Future _connectToRoom(String server, String token, String clientId, String spaceId, SecureKey key) async { + static Future _connectToRoom( + String server, + String token, + String clientId, + String spaceId, + SecureKey key, + ) async { // Connect to space node - final result = await createSpaceConnection(server, token); + final result = await SpaceConnection.createSpaceConnection(server, token); sendLog("COULD CONNECT TO SPACE NODE"); if (!result) { return "server.error".tr; } // Make everything ready - Get.find().onConnect(spaceId, key); + SpaceController.onConnect(server, spaceId, key); + TabletopController.resetControllerState(); + StudioController.resetControllerState(); + + // Open the screen + if (SpaceController.shouldSwitchToPage) { + SidebarController.openTab(SpaceSidebarTab()); + } // Send the server all the data required for setup - final event = await spaceConnector.sendActionAndWait(msg.ServerAction("setup", { - "data": encryptSymmetric(StatusController.ownAddress.encode(), key), - "signature": signMessage(signatureKeyPair.secretKey, craftSignature(spaceId, clientId, StatusController.ownAddress.encode())), - })); + final event = await SpaceConnection.spaceConnector!.sendActionAndWait( + msg.ServerAction("setup", { + "data": encryptSymmetric(StatusController.ownAddress.encode(), key), + "signature": signMessage( + signatureKeyPair.secretKey, + craftSignature(spaceId, clientId, StatusController.ownAddress.encode()), + ), + }), + ); if (event == null) { return "server.error".tr; } @@ -104,6 +126,9 @@ class SpaceService { return event.data["message"]; } + // Create a Studio connection + unawaited(StudioController.connectToStudio()); + return null; } diff --git a/lib/services/spaces/studio/media_profile.dart b/lib/services/spaces/studio/media_profile.dart new file mode 100644 index 00000000..ccf37b15 --- /dev/null +++ b/lib/services/spaces/studio/media_profile.dart @@ -0,0 +1,104 @@ +import 'package:get/get.dart'; + +enum MediaProfileType { + static("media_profile.static"), + motion("media_profile.motion"), + balanced("media_profile.balanced"); + + final String _label; + String get label => _label.tr; + + const MediaProfileType(this._label); +} + +class MediaProfile { + final MediaProfileType type; + final int width; + final int height; + final int framerate; + final int bitrate; + + MediaProfile(this.type, this.width, this.height, this.framerate, this.bitrate); + + /// Convert the media profile to a constraints map readable by the getUserMedia function + Map toConstraints() => {"width": width, "height": height, "maxFrameRate": framerate}; + + @override + String toString() { + return 'MediaProfile{type: $type, width: $width, height: $height, framerate: $framerate, bitrate: $bitrate}'; + } +} + +class MediaProfiles { + static const _kbps = 1000; + + static final staticMediaProfiles = [ + MediaProfile(MediaProfileType.static, 640, 360, 10, 1 * _kbps), + MediaProfile(MediaProfileType.static, 854, 480, 12, 2 * _kbps), + MediaProfile(MediaProfileType.static, 1280, 720, 15, 4 * _kbps), + MediaProfile(MediaProfileType.static, 1600, 900, 15, 6 * _kbps), + MediaProfile(MediaProfileType.static, 1920, 1080, 15, 8 * _kbps), + MediaProfile(MediaProfileType.static, 2560, 1440, 15, 12 * _kbps), + MediaProfile(MediaProfileType.static, 3840, 2160, 15, 16 * _kbps), + ]; + + static final motionMediaProfiles = [ + MediaProfile(MediaProfileType.motion, 640, 360, 30, 1 * _kbps), + MediaProfile(MediaProfileType.motion, 854, 480, 45, 2 * _kbps), + MediaProfile(MediaProfileType.motion, 1280, 720, 60, 4 * _kbps), + MediaProfile(MediaProfileType.motion, 1600, 900, 45, 6 * _kbps), + MediaProfile(MediaProfileType.motion, 1600, 900, 60, 8 * _kbps), + MediaProfile(MediaProfileType.motion, 1920, 1080, 45, 10 * _kbps), + MediaProfile(MediaProfileType.motion, 1920, 1080, 60, 12 * _kbps), + MediaProfile(MediaProfileType.motion, 2560, 1440, 60, 16 * _kbps), + ]; + + static final balancedMediaProfiles = [ + MediaProfile(MediaProfileType.balanced, 640, 360, 24, 1 * _kbps), + MediaProfile(MediaProfileType.balanced, 854, 480, 24, 2 * _kbps), + MediaProfile(MediaProfileType.balanced, 1280, 720, 30, 4 * _kbps), + MediaProfile(MediaProfileType.balanced, 1600, 900, 30, 6 * _kbps), + MediaProfile(MediaProfileType.balanced, 1920, 1080, 30, 8 * _kbps), + MediaProfile(MediaProfileType.balanced, 2560, 1440, 30, 12 * _kbps), + MediaProfile(MediaProfileType.balanced, 3840, 2160, 30, 16 * _kbps), + ]; + + static final profiles = { + MediaProfileType.static: staticMediaProfiles, + MediaProfileType.motion: motionMediaProfiles, + MediaProfileType.balanced: balancedMediaProfiles, + }; + + // Parameters to configure the algorithm + + /// How much of the available bitrate is available as wiggleroom to see if a higher profile can be chosen + static const _wiggleRoom = 0.2; + + /// How much bandwidth a track should try to use of the total bandwidth + static const _bandwidthUsage = 0.2; + + /// What bandwidth will be used as a fallback in case the available bandwidth can't be determined + static const _fallbackBandwidth = 4 * _kbps; + + /// Determine a fitting media profile for a [MediaProfileType] and the bandwidth available for sharing. + static MediaProfile determineMediaProfile(MediaProfileType type, double? availableBandwidth) { + final availableProfiles = profiles[type]!; + + // Calculate bandwidth amount that can be used + final usableBandwidth = availableBandwidth == null ? _fallbackBandwidth : availableBandwidth * _bandwidthUsage; + final bandwidthWiggle = availableBandwidth == null ? 0 : availableBandwidth * _wiggleRoom; + + // Find the nearest to the current bandwidth + wiggle room + MediaProfile? max; + for (var profile in availableProfiles) { + if (profile.bitrate <= usableBandwidth + bandwidthWiggle) { + max = profile; + } + } + + // Set the profile to the lowest in case nothing could be found + max ??= availableProfiles[0]; + + return max; + } +} diff --git a/lib/services/spaces/studio/studio_connection.dart b/lib/services/spaces/studio/studio_connection.dart new file mode 100644 index 00000000..055f577d --- /dev/null +++ b/lib/services/spaces/studio/studio_connection.dart @@ -0,0 +1,222 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; +import 'package:chat_interface/controller/spaces/studio/studio_controller.dart'; +import 'package:chat_interface/pages/settings/app/audio_settings.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; +import 'package:chat_interface/services/spaces/studio/studio_track_publisher.dart'; +import 'package:chat_interface/src/rust/api/engine.dart' as libspace; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +class StudioConnection { + final RTCPeerConnection _peer; + late final StudioTrackPublisher _publisher; + libspace.LightwireEngine? _engine; + Timer? _talkingTimer; + final _disposeFunctions = []; + + StudioConnection(this._peer) { + // Create all the required listeners on the peer + _peer + ..onConnectionState = (state) { + sendLog("studio: new connection state: $state"); + if (state == RTCPeerConnectionState.RTCPeerConnectionStateClosed) { + StudioController.handleDisconnect(); + } + } + ..onRenegotiationNeeded = _handleRenegotiation + ..onSignalingState = (state) { + sendLog("studio: new signaling state: $state"); + } + ..onIceConnectionState = (state) { + sendLog("studio: new ice connection state: $state"); + } + ..onIceGatheringState = (state) { + sendLog("studio: new ice gathering state: $state"); + } + ..onTrack = _handleNewTrack; + + // Create the publisher to manage all the tracks + _publisher = StudioTrackPublisher(this); + } + + /// Create the default data channel used for lightwire + Future createLightwireChannel() async { + // Create the channel + final channel = await _peer.createDataChannel( + "lightwire", + RTCDataChannelInit() + ..maxRetransmits = 0 + ..ordered = false, + ); + + // Create a new timer for making sure talking states are deleted once no longer talking + _talkingTimer = Timer.periodic(100.ms, (timer) { + final flagDate = DateTime.now().subtract(250.ms); + for (var member in SpaceMemberController.members.peek().values) { + if (member.id == SpaceMemberController.getOwnId()) { + continue; + } + // Check if they have not talked since the last iteration + member.talking.value = !(member.lastPacket?.isBefore(flagDate) ?? !member.talking.peek()); + } + }); + + // Subscribe to all the events the data channel has + _handleLightwireChannel(channel); + } + + /// Handle a new data channel created by the server or the client + void _handleLightwireChannel(RTCDataChannel channel) { + channel.bufferedAmountLowThreshold = 256 * 1024; // 256 KB + + // Subscribe to all the events the data channel has + channel + ..onDataChannelState = (state) async { + sendLog("studio: state of lightwire: $state"); + + // Start lightwire when the channel has been opeed + if (state == RTCDataChannelState.RTCDataChannelOpen) { + sendLog("studio: starting lightwire.."); + + // Create a new lightwire engine and wire it up with the data channel + _engine = await libspace.createLightwireEngine(); + libspace.startPacketStream(engine: _engine!).listen((data) { + final (packet, amplitude, speech) = data; + SpaceMemberController.handleTalkingState(SpaceMemberController.getOwnId(), speech ?? false); + + // Send the packets to the data channel + if (packet != null) { + channel.send(RTCDataChannelMessage.fromBinary(packet)); + } + }); + + // Listen for output and input device changes to make sure the current engine is also using them + _disposeFunctions.add( + AudioSettings.microphone.value.subscribe((value) { + if (_engine != null) { + libspace.setInputDevice(engine: _engine!, device: value ?? AudioSettings.useDefaultDevice); + } + }), + ); + _disposeFunctions.add( + AudioSettings.outputDevice.value.subscribe((value) { + if (_engine != null) { + libspace.setOutputDevice(engine: _engine!, device: value ?? AudioSettings.useDefaultDevice); + } + }), + ); + _disposeFunctions.addAll(await AudioSettings.subscribeToSettings(_engine!)); + } + + // Close the lightwire engine when the data channel is closed + if (state == RTCDataChannelState.RTCDataChannelClosed && _engine != null) { + await libspace.stopEngine(engine: _engine!); + SpaceMemberController.handleTalkingState(SpaceMemberController.getOwnId(), false); + } + } + ..onMessage = (msg) { + if (!msg.isBinary) { + sendLog("studio: error: message other than binary over lightwire"); + return; + } + + // Decode the packet + // Format: | id_length (8 bytes) | client_id (of id_length) | voice_data (rest) | + final bytes = msg.binary; + final idLength = bytes[0]; + final clientIdBytes = bytes.sublist(1, 1 + idLength); + final clientId = utf8.decode(clientIdBytes); + final voicePacket = bytes.sublist(1 + idLength); + + // Set talking state (will be automatically cleared) + final member = SpaceMemberController.getMember(clientId); + member?.talking.value = true; + member?.lastPacket = DateTime.now(); + + // Let lightwire handle the rest + unawaited(libspace.handlePacket(engine: _engine!, id: clientId, packet: voicePacket)); + } + ..onBufferedAmountLow = (buf) { + sendLog("studio: lightwire buffer amount low"); + }; + } + + /// Handle a new ice candidate + void handleIceCandidate(RTCIceCandidate candidate) { + _peer.addCandidate(candidate); + } + + /// Handle the renegotiation + /// + /// TODO: This should be the way we always negotiate with the server + Future _handleRenegotiation() async { + if ((_peer.connectionState ?? RTCPeerConnectionState.RTCPeerConnectionStateDisconnected) != + RTCPeerConnectionState.RTCPeerConnectionStateConnected) { + return; + } + sendLog("studio: renegotiating with the server.."); + + // Create a new offer + final offer = await _peer.createOffer({ + // TODO: Re-enable when video is implemented + /* "offerToReceiveVideo": true, */ + }); + await _peer.setLocalDescription(offer); + + // Send the server the new offer + final event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("st_reneg", offer.toMap())); + if (event == null) { + sendLog("studio: renegotiation failed, disconnect would be good probably"); + return; + } + if (!event.data["success"]) { + sendLog("studio: renegotiation failed cause of ${event.data["message"]}, disconnect would be good probably"); + return; + } + + // Set new answer as remote description + await _peer.setRemoteDescription(RTCSessionDescription(event.data["answer"]["sdp"], event.data["answer"]["type"])); + } + + /// Handle a new track + void _handleNewTrack(RTCTrackEvent event) { + sendLog("studio: received new track: ${event.track.kind ?? "no type found"}"); + } + + /// Handle the update of the audio state + Future handleAudioState({bool? muted, bool? deafened}) async { + if (muted != null) { + await libspace.setVoiceEnabled(engine: _engine!, enabled: !muted); + } + if (deafened != null) { + await libspace.setAudioEnabled(engine: _engine!, enabled: !deafened); + } + } + + /// Get the underlying RTC connection + RTCPeerConnection getPeer() { + return _peer; + } + + /// Get the underlying Track publisher + StudioTrackPublisher getPublisher() { + return _publisher; + } + + libspace.LightwireEngine? getEngine() { + return _engine; + } + + void close() { + _talkingTimer?.cancel(); + _peer.close(); // This will close lightwire, etc. + for (var func in _disposeFunctions) { + func.call(); + } + } +} diff --git a/lib/services/spaces/studio/studio_service.dart b/lib/services/spaces/studio/studio_service.dart new file mode 100644 index 00000000..b561b9b2 --- /dev/null +++ b/lib/services/spaces/studio/studio_service.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; +import 'package:chat_interface/controller/spaces/studio/studio_controller.dart'; +import 'package:chat_interface/controller/spaces/studio/studio_track_controller.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; +import 'package:chat_interface/services/spaces/studio/studio_connection.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:get/get.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +class StudioService { + /// Connect to Studio (Liphium's WebRTC SFU integrated into Spaces) + /// + /// Returns a new WebRTC connection and an error if there was one. + static Future<(StudioConnection?, String?)> connectToStudio() async { + // Make sure we are connected to a Space + if (!SpaceController.connected.peek() || SpaceConnection.spaceConnector == null) { + return (null, "error.connection".tr); + } + + // Get all the info needed for a WebRTC connection from the server + var event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("st_info", {})); + if (event == null) { + return (null, "server.error".tr); + } + if (!event.data["success"]) { + return (null, event.data["message"] as String); + } + + // Create a connection and generate an offer + final config = { + 'sdpSemantics': 'unified-plan', + "iceServers": [ + { + "urls": ["stun:${event.data["stun"]}"], + }, + if ((event.data["turn"] ?? "") != "") + { + "urls": ["turn:${event.data["turn"]}"], + "username": event.data["turn_user"] ?? "", + "credential": event.data["turn_pass"] ?? "", + }, + ], + }; + sendLog(config); + final peer = await createPeerConnection(config); + + // Create a data channel for pipes + final studioConn = StudioConnection(peer); + await studioConn.createLightwireChannel(); + + // Create an offer for the server + final offer = await peer.createOffer({ + // TODO: Uncomment when video implementation is done + // "offerToReceiveVideo": true, + }); + await peer.setLocalDescription(offer); + + // Send all the ice candidates to the server + final completer = Completer(); + peer.onIceCandidate = (candidate) async { + if (candidate.candidate != null) { + await completer.future; // Make sure to not send ice candidates before the client is registered + sendLog("ice candidate: ${candidate.candidate}"); + SpaceConnection.spaceConnector!.sendAction(ServerAction("st_ice", candidate.toMap())); + } + }; + + // Send the offer to the server + event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("st_join", offer.toMap())); + if (event == null) { + return (null, "error.studio.rtp".trParams({"code": "200"})); + } + if (!event.data["success"]) { + return (null, event.data["message"] as String); + } + completer.complete(); + + // Accept the offer from the server + await peer.setRemoteDescription(RTCSessionDescription(event.data["answer"]["sdp"], event.data["answer"]["type"])); + + return (studioConn, null); + } + + /// Register all event handlers needed for Studio. + static void setupStudioHandlers(Connector connector) { + // Handle track updates + connector.listen("st_tr_update", (event) { + // Convert to a track + final track = StudioTrack( + id: event.data["track"], + publisher: SpaceMemberController.members[event.data["sender"]]!, + paused: event.data["paused"], + channels: List.generate(event.data["channels"].length, (index) { + return event.data["channels"][index] as String; + }), + subscribers: event.data["subs"] ?? [], + ); + + // Tell the controller about the updated track + StudioTrackController.updateOrRegisterTrack(track); + }); + + // Handle track deletion + connector.listen("st_tr_deleted", (event) { + // Tell the controller about the deleted track + StudioTrackController.deleteTrack(event.data["track"]); + }); + + // Handle ice candidates for studio + connector.listen("st_ice", (event) { + // Pass the candidate to the current connection + final candidate = event.data["candidate"]; + StudioController.getConnection()?.handleIceCandidate( + RTCIceCandidate(candidate["candidate"], candidate["sdpMid"], candidate["sdpMLineIndex"]), + ); + }); + } + + /// Update your audio state on the server. Set muted and deafened only when changed. + /// + /// Returns an error if there was one. + static Future updateAudioState({bool? muted, bool? deafened}) async { + assert(muted != null || deafened != null); + + // Send the new audio state to the server + final event = await SpaceConnection.spaceConnector!.sendActionAndWait( + ServerAction("set_audio_state", {if (muted != null) "muted": muted, if (deafened != null) "deafened": deafened}), + ); + if (event == null) { + return "server.error".tr; + } + if (!event.data["success"]) { + return event.data["message"]; + } + + // Update the audio state on the underlying connection + unawaited(StudioController.getConnection()?.handleAudioState(muted: muted, deafened: deafened)); + + return null; + } +} diff --git a/lib/services/spaces/studio/studio_track.dart b/lib/services/spaces/studio/studio_track.dart new file mode 100644 index 00000000..1a31b398 --- /dev/null +++ b/lib/services/spaces/studio/studio_track.dart @@ -0,0 +1,21 @@ +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +class PublishedStudioTrack { + /// The transceiver responsible for the track + final RTCRtpTransceiver _transceiver; + + /// The underlying media stream powering the track + final MediaStream _stream; + + PublishedStudioTrack(this._transceiver, this._stream); + + /// Get the underlying transceiver. + RTCRtpTransceiver getTransceiver() { + return _transceiver; + } + + /// Get the underlying media stream. + MediaStream getStream() { + return _stream; + } +} diff --git a/lib/services/spaces/studio/studio_track_publisher.dart b/lib/services/spaces/studio/studio_track_publisher.dart new file mode 100644 index 00000000..f4c5a218 --- /dev/null +++ b/lib/services/spaces/studio/studio_track_publisher.dart @@ -0,0 +1,92 @@ +import 'package:chat_interface/services/spaces/studio/media_profile.dart'; +import 'package:chat_interface/services/spaces/studio/studio_connection.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +class StudioTrackPublisher { + /// The connection this track publisher is related to + final StudioConnection _connection; + + final List transceivers = []; + + StudioTrackPublisher(this._connection); + + /// The local stream that has all the tracks inside of it + MediaStream? _stream; + + /// Create a video track for the camera + Future createCameraTrack() async { + // Determine a good quality for the camera + final bandwidth = await determineBandwidth(); + final profile = MediaProfiles.determineMediaProfile(MediaProfileType.balanced, bandwidth); + + sendLog("using profile $profile"); + + // Get the actual user's stream + final media = await mediaDevices.getUserMedia(_getMediaConstraints(video: profile)); + if (_stream != null) { + // Remove all the existing video tracks + for (var track in _stream!.getVideoTracks()) { + try { + await _stream!.removeTrack(track); + } catch (e) { + sendLog("error: couldn't stop local track: $e"); + } + await track.stop(); + } + + // Add all the new tracks + for (var track in media.getVideoTracks()) { + await _stream!.addTrack(track); + } + } else { + _stream = media; + } + + // Make sure there are tracks to publish + if (media.getVideoTracks().isEmpty) { + sendLog("error: no video tracks found"); + return; + } + + // Add the video track + final videoTrack = media.getVideoTracks()[0]; + final encodings = [ + RTCRtpEncoding(active: true, rid: "f", maxBitrate: profile.bitrate, scaleResolutionDownBy: 1), + RTCRtpEncoding(active: false, rid: "h", maxBitrate: (profile.bitrate / 2).toInt(), scaleResolutionDownBy: 2), + RTCRtpEncoding(active: false, rid: "q", maxBitrate: (profile.bitrate / 4).toInt(), scaleResolutionDownBy: 4), + ]; + + // Create the transceiver for simulcasting + final transceiver = await _connection.getPeer().addTransceiver( + track: videoTrack, + kind: RTCRtpMediaType.RTCRtpMediaTypeVideo, + init: RTCRtpTransceiverInit(direction: TransceiverDirection.SendOnly, streams: [media], sendEncodings: encodings), + ); + transceivers.add(transceiver); + } + + /// Media constraints for video and audio tracks + Map _getMediaConstraints({bool audio = false, MediaProfile? video}) { + return { + 'audio': audio ? true : false, + 'video': video != null ? {'mandatory': video.toConstraints(), 'facingMode': 'user'} : false, + }; + } + + /// Determine the available bandwidth of the user by getting the stats of the connection + Future determineBandwidth() async { + // Try to read it from the stats + final stats = await _connection.getPeer().getStats(); + for (var stat in stats) { + if (stat.type == "candidate-pair") { + if (stat.values.containsKey("availableOutgoingBitrate")) { + return stat.values["availableOutgoingBitrate"]; + } + } + } + + // Return null if non existent + return null; + } +} diff --git a/lib/services/spaces/tabletop/tabletop_object.dart b/lib/services/spaces/tabletop/tabletop_object.dart new file mode 100644 index 00000000..a54ec037 --- /dev/null +++ b/lib/services/spaces/tabletop/tabletop_object.dart @@ -0,0 +1,306 @@ +import 'dart:async'; + +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:flutter/material.dart'; + +abstract class TableObject { + TableObject(this.id, this.order, this.location, this.size, this.type); + + Function()? dataCallback; + String id; + int order; + TableObjectType type; + + /// The size of the object + Size size; + + /// The top left location of the object on the table + String? dataBeforeQueue; + DateTime? _lastMove; + Offset? _lastLocation; + Offset location; + bool deleted = false; + bool added = false; + + // Modifiers + bool positionOverwrite = false; + final positionX = AnimatedDouble(0.0); + final positionY = AnimatedDouble(0.0); + final rotation = AnimatedDouble(0.0); + final scale = AnimatedDouble(1.0, from: 0.0); + + Offset interpolatedLocation(DateTime now) { + if (positionOverwrite) { + return Offset(positionX.value(now), positionY.value(now)); + } + if (_lastMove == null || _lastLocation == null) { + return location; + } + final time = now.difference(_lastMove!).inMilliseconds; + final delta = time / (1000 ~/ TabletopController.tickRate); + return Offset.lerp(_lastLocation!, location, delta.clamp(0, 1))!; + } + + void move(Offset location) { + _lastMove = DateTime.now(); + _lastLocation = this.location; + this.location = location; + } + + double lastRotation = 0; + void rotate(double rot) { + sendLog(lastRotation); + if (lastRotation == -1) { + rotation.setValue(rot); + } else { + lastRotation = rot; + } + } + + void newRotation(double rot) { + queue(() async { + final event = await SpaceConnection.spaceConnector!.sendActionAndWait( + ServerAction("tobj_rotate", {"id": id, "r": rot}), + ); + currentlyModifying = false; + + // Check if there was an error with the rotation + if (event == null) { + sendLog("error with object rotation: no response"); + return; + } + if (!event.data["success"]) { + sendLog("error with object rotation: ${event.data["message"]}"); + } + }); + } + + /// Called every frame when the object is hovered + void hoverRotation(double rot) { + if (lastRotation == -1) { + lastRotation = rotation.realValue; + } + rotation.setValue(rot); + } + + /// Called every frame when the object is no longer hovered + void unhoverRotation() { + if (lastRotation != -1) { + rotation.setValue(lastRotation); + lastRotation = -1; + } + } + + /// DONT OVERWRITE THIS METHOD + void decryptData(String data) { + handleData(decryptSymmetric(data, SpaceController.key!)); + } + + /// NEVER CALL THIS METHOD WITH ENCRYPTED DATA + void handleData(String data) {} + + /// Implemented optionally when needed + String getData() { + return ""; + } + + String encryptedData() { + return encryptSymmetric(getData(), SpaceController.key!); + } + + /// Render with rotation and scale applied (used for movable objects) + void render(Canvas canvas, Offset location) {} + + /// Called when the object is clicked + void runAction() {} + + /// Called when the object is right clicked + List getContextMenuAdditions() { + return []; + } + + /// Add a new object + Future sendAdd() { + deleted = false; + if (added) { + sendLog("WHAT DA HELL"); + } + added = true; + final completer = Completer(); + + // Send to the server + SpaceConnection.spaceConnector!.sendAction( + ServerAction("tobj_create", { + "x": location.dx, + "y": location.dy, + "w": size.width, + "h": size.height, + "r": lastRotation, + "type": type.index, + "data": encryptedData(), + }), + handler: (event) { + if (!event.data["success"]) { + sendLog("SOMETHING WENT WRONG"); + completer.complete(false); + return; + } + id = event.data["id"]; + order = event.data["o"]; + sendLog("ADDING $id to table with order $order"); + TabletopController.addObject(this); + completer.complete(true); + }, + ); + + return completer.future; + } + + /// Remove an object + void sendRemove() { + deleted = true; + added = false; + SpaceConnection.spaceConnector!.sendAction(ServerAction("tobj_delete", id)); + } + + /// Start a modification process (data) + Future select() { + final completer = Completer(); + + SpaceConnection.spaceConnector!.sendAction( + ServerAction("tobj_select", id), + handler: (event) { + if (!event.data["success"]) { + showErrorPopup("error", event.data["message"]); + sendLog("can't select rn"); + completer.complete(false); + return; + } + completer.complete(true); + }, + ); + + return completer.future; + } + + /// Start a modification process (data) + Future unselect() { + final completer = Completer(); + if (deleted) { + completer.complete(false); + } else { + SpaceConnection.spaceConnector!.sendAction( + ServerAction("tobj_unselect", id), + handler: (event) { + if (!event.data["success"]) { + sendLog("can't unselect rn"); + completer.complete(false); + return; + } + completer.complete(true); + }, + ); + } + + return completer.future; + } + + // Boolean to make sure the object is not modified + bool currentlyModifying = false; + + /// Wait until the data can be modified + void queue(Function() callback) { + if (currentlyModifying) { + return; + } + currentlyModifying = true; + dataBeforeQueue = getData(); + SpaceConnection.spaceConnector!.sendAction( + ServerAction("tobj_mqueue", id), + handler: (event) { + if (!event.data["success"]) { + showErrorPopup("error", event.data["message"]); + return; + } + + if (event.data["direct"]) { + callback(); + } else { + dataCallback = callback; + } + }, + ); + } + + /// Update the data of the object + Future modifyData() { + final completer = Completer(); + SpaceConnection.spaceConnector!.sendAction( + ServerAction("tobj_modify", { + "id": id, + "data": encryptedData(), + "width": size.width, + "height": size.height, + }), + handler: (event) { + currentlyModifying = false; + // Reset data in case the modification wasn't successful + if (!event.data["success"]) { + if (dataBeforeQueue == null) { + sendLog("NO ROLLBACK STATE FOR OBJECT"); + return; + } + + sendLog("modification of $id wasn't possible: ${event.data["message"]}"); + handleData(dataBeforeQueue!); + completer.complete(false); + } else { + completer.complete(true); + } + + // Reset it + dataBeforeQueue = null; + }, + ); + return completer.future; + } +} + +enum TableObjectType { + text(Icons.text_fields, "Text"), + deck(Icons.filter_none, "Deck"), + card(Icons.image, "Card", creatable: false), + inventory(Icons.business_center, "Inventory", creatable: false); + + final IconData icon; + final String label; + final bool creatable; + + const TableObjectType(this.icon, this.label, {this.creatable = true}); +} + +class ContextMenuAction { + final IconData icon; + final bool category; + final String label; + final Color? color; + final Color? iconColor; + final bool goBack; + final Function() onTap; + + const ContextMenuAction({ + required this.icon, + required this.label, + required this.onTap, + this.category = false, + this.goBack = true, + this.color, + this.iconColor, + }); +} diff --git a/lib/services/spaces/tabletop/tabletop_service.dart b/lib/services/spaces/tabletop/tabletop_service.dart new file mode 100644 index 00000000..d4fe7ea2 --- /dev/null +++ b/lib/services/spaces/tabletop/tabletop_service.dart @@ -0,0 +1,135 @@ +import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; +import 'package:chat_interface/controller/spaces/tabletop/tabletop_controller.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_card.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_deck.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_inventory.dart'; +import 'package:chat_interface/pages/spaces/tabletop/objects/tabletop_text.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/services/spaces/tabletop/tabletop_object.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:flutter/material.dart'; + +class TabletopService { + static void setupTabletopListeners(Connector connector) { + connector.listen("table_obj", (event) { + for (var obj in event.data["obj"]) { + TabletopController.addObject( + newObject( + TableObjectType.values[obj["t"]], + obj["id"] as String, + obj["o"] as int, + Offset((obj["x"] as num).toDouble(), (obj["y"] as num).toDouble()), + Size((obj["w"] as num).toDouble(), (obj["h"] as num).toDouble()), + (obj["r"] as num).toDouble(), + obj["d"], + ), + ); + } + }); + + // Listen for creations + connector.listen("tobj_created", (event) { + if (event.data["c"] == SpaceMemberController.getOwnId()) { + return; + } + + TabletopController.addObject( + newObject( + TableObjectType.values[event.data["type"]], + event.data["id"], + event.data["o"], + Offset((event.data["x"] as num).toDouble(), (event.data["y"] as num).toDouble()), + Size((event.data["w"] as num).toDouble(), (event.data["h"] as num).toDouble()), + (event.data["r"] as num).toDouble(), + event.data["data"], + ), + ); + }); + + // Listen for a new order of an object + connector.listen("tobj_order", (event) { + var objectId = event.data["o"]; + var newOrder = (event.data["or"] as num).toInt(); + TabletopController.setOrder(objectId, newOrder); + }); + + // Listen for cursor movements + connector.listen("tc_moved", (event) { + TabletopController.updateCursor( + event.data["c"], + Offset((event.data["x"] as num).toDouble(), (event.data["y"] as num).toDouble()), + (event.data["col"] as num).toDouble(), + ); + }); + + // Listen for deletions + connector.listen("tobj_deleted", (event) { + TabletopController.removeObject(id: event.data["id"]); + }); + + // Listen for moves + connector.listen("tobj_moved", (event) { + final object = TabletopController.objects[event.data["id"]]; + if (object == null || object == TabletopController.heldObject) { + return; + } + object.move(Offset((event.data["x"] as num).toDouble(), (event.data["y"] as num).toDouble())); + }); + + // Listen for rotations + connector.listen("tobj_rotated", (event) { + final object = TabletopController.objects[event.data["id"]]; + if (object == null) { + return; + } + object.rotate((event.data["r"] as num).toDouble()); + }); + + // Listen for modifications + connector.listen("tobj_modified", (event) { + final object = TabletopController.objects[event.data["id"]]; + if (object == null) { + return; + } + object.decryptData(event.data["data"]); + sendLog(event.data["w"]); + object.size = Size((event.data["w"] as num).toDouble(), (event.data["h"] as num).toDouble()); + }); + + // Listen for when edits are allowed + connector.listen("tobj_mqueue_allowed", (event) { + final object = TabletopController.objects[event.data["id"]]; + if (object == null) { + sendLog("object not found, modification can't be done"); + return; + } + object.dataCallback?.call(); + }); + } + + /// Create a new object + static TableObject newObject( + TableObjectType type, + String id, + int order, + Offset location, + Size size, + double rotation, + String data, + ) { + TableObject object; + switch (type) { + case TableObjectType.text: + object = TextObject(id, order, location, size); + case TableObjectType.deck: + object = DeckObject(id, order, location, size); + case TableObjectType.card: + object = CardObject(id, order, location, size); + case TableObjectType.inventory: + object = InventoryObject(id, order, location, size); + } + object.rotate(rotation); + object.decryptData(data); + return object; + } +} diff --git a/lib/services/spaces/warp/warp_connection.dart b/lib/services/spaces/warp/warp_connection.dart new file mode 100644 index 00000000..3259b784 --- /dev/null +++ b/lib/services/spaces/warp/warp_connection.dart @@ -0,0 +1,156 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/spaces/warp_controller.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/logging_framework.dart'; + +/// A Warp that is being shared with the user and has been bound to a port. +class ConnectedWarp { + /// The id of the Warp (to identify it) + final String id; + + /// The port on the hoster's computer (for clarity when rendering) + final int originPort; + + /// The port the server is being bound to on the local system + final int goalPort; + + /// The friend that's hosting the Warp + final Friend hoster; + + ConnectedWarp(this.id, this.originPort, this.goalPort, this.hoster); + + /// The server that proxies the connections. + ServerSocket? server; + + /// The counter keeping track of the current connection number (for forwarding more efficiently) + int connectionCount = 1; + + /// All the sockets that are currently connected to the local server + final _sockets = {}; + + // All sequence related stuff for jitter buffering (can happen cause server) + final _sequenceNumbers = {}; + final _packetQueue = >{}; + + /// Start the server for proxying the connection. + Future startServer() async { + server = await ServerSocket.bind(InternetAddress.loopbackIPv4, goalPort, shared: false); + + sendLog("bound server on ${server!.address.toString()} ${server!.port}"); + + server!.listen( + (socket) { + // Increment the connection count and take current count as new identifier for this connection + final currentId = connectionCount; + _sockets[currentId] = socket; + connectionCount++; + + // Listen to all packets from the socket + int seq = 0; + StreamSubscription? sub; + sub = socket.listen( + (packet) async { + // Forward the bytes to the server + seq++; + final result = await forwardBytesToHost(currentId, packet, seq); + if (!result) { + disconnectFromWarp(); + } + }, + onDone: () { + sub?.cancel(); + _sockets.remove(currentId); + }, + onError: (e) { + sendLog("disconnected cause $e"); + }, + cancelOnError: true, + ); + }, + onDone: () => disconnectFromWarp(), + onError: (e) { + sendLog("warp ended cause $e"); + }, + cancelOnError: true, + ); + } + + /// Send bytes to the host server. + Future forwardBytesToHost(int connId, Uint8List bytes, int seq) async { + final event = await SpaceConnection.spaceConnector!.sendActionAndWait( + ServerAction("wp_send_to", { + "w": id, + "s": seq, + "c": connId, + "p": base64Encode(encryptSymmetricBytes(bytes, SpaceController.key!)), + }), + ); + if (event == null || !event.data["success"]) { + return false; + } + return true; + } + + /// Forward a packet to a socket connected to the local server by their connection id + Future forwardPacketToSocket(int connId, Uint8List bytes, int seq) async { + if (_sockets[connId] == null) { + return false; + } + if (_packetQueue[connId] == null) { + _packetQueue[connId] = {}; + _sequenceNumbers[connId] = 0; + } + + // Make sure it's the right sequence number + if (seq != _sequenceNumbers[connId]! + 1) { + _packetQueue[connId]![seq] = bytes; + sendLog("packet queue (conn)"); + return true; + } else { + _sequenceNumbers[connId] = seq; + } + + // Forward the packet + _sockets[connId]!.add(bytes); + + // Check the queue for more packets after the current one + while (_packetQueue[connId]![seq + 1] != null) { + seq++; + + // Send the packet in the queue + _sockets[connId]!.add(_packetQueue[connId]![seq]!); + _packetQueue[connId]!.remove(seq); + + // Update the sequence number accordingly + _sequenceNumbers[connId] = seq; + } + return true; + } + + /// Disconnect from the Warp and close the local server. + void disconnectFromWarp({bool action = true}) { + if (action) { + SpaceConnection.spaceConnector!.sendAction(ServerAction("wp_disconnect", id)); + } + onDisconnect(); + } + + /// Called when the user gets disconnected. + void onDisconnect() { + for (var socket in _sockets.values) { + socket.close(); + } + server?.close(); + + // Remove from the controller + WarpController.removeActiveWarp(this); + } +} diff --git a/lib/services/spaces/warp/warp_service.dart b/lib/services/spaces/warp/warp_service.dart new file mode 100644 index 00000000..59c653f4 --- /dev/null +++ b/lib/services/spaces/warp/warp_service.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/controller/spaces/spaces_member_controller.dart'; +import 'package:chat_interface/controller/spaces/warp_controller.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; +import 'package:chat_interface/services/spaces/warp/warp_connection.dart'; +import 'package:chat_interface/services/spaces/warp/warp_shared.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:get/get.dart'; + +class WarpService { + static void setupWarpListeners(Connector connector) { + // Listen for new Warps created on the server + connector.listen("wp_new", (event) { + // Add the container to the list of Warps on the server + final container = WarpShareContainer( + id: event.data["w"], + account: SpaceMemberController.getMember(event.data["h"])!.friend, + port: event.data["p"] as int, + ); + WarpController.addWarp(container); + }); + + // Listen for the Warps that end + connector.listen("wp_end", (event) { + WarpController.onWarpEnd(event.data["w"]); + }); + + // Listen for packets meant for the local server (hoster -> current client) + connector.listen("wp_to", (event) { + // Get the Warp and make sure it's not null + WarpController.getActiveWarp(event.data["w"]); + final warp = WarpController.getActiveWarp(event.data["w"]); + if (warp == null) { + return; + } + + // Decrypt the content and forward + final decrypted = decryptSymmetricBytes(base64Decode(event.data["p"]), SpaceController.key!); + warp.forwardPacketToSocket(event.data["c"], decrypted, event.data["s"]); + }); + + // Listen for packets meant for the local server (current client -> hoster) + connector.listen("wp_back", (event) { + // Get the Warp and make sure it's not null + final warp = WarpController.getSharedWarp(event.data["w"]); + if (warp == null) { + return; + } + + // Decrypt the content and handle receiving + final decrypted = decryptSymmetricBytes(base64Decode(event.data["p"]), SpaceController.key!); + warp.receivePacketFromClient(event.data["s"], event.data["c"], decrypted, event.data["sq"]); + }); + + // Listen for clients disconnecting from the shared server (as hoster) + connector.listen("wp_disconnected", (event) { + // Get the Warp and make sure it's not null + final warp = WarpController.getSharedWarp(event.data["w"]); + if (warp == null) { + return; + } + + // Decrypt the content and handle receiving + warp.handleDisconnect(event.data["c"]); + }); + } + + /// Create a Warp using the port it should share. + /// + /// This will tell the server about a port that this client wants to share with others in + /// the Space. The connections to the local server will only start being opened once the + /// first packet from the server arrives (they will be made on demand). + /// + /// This function doesn't do any validation since that's already happening in the WarpCreateWindow + /// that calls this function. + /// + /// Returns an error if there was one together with the created warp. + static Future<(String?, SharedWarp?)> createWarp(int port) async { + // Try connecting to the port to make sure there is a server there + try { + await Socket.connect("localhost", port); + } catch (e) { + return ("warp.error.port_not_used".tr, null); + } + + final event = await SpaceConnection.spaceConnector!.sendActionAndWait(ServerAction("wp_create", port)); + if (event == null) { + return ("server.error".tr, null); + } + + // Make sure the request was valid + if (!event.data["success"]) { + return (event.data["message"] as String, null); + } + + // Return the warp to the controller + return (null, SharedWarp(event.data["id"], port)); + } + + /// Connect to a Warp using its container. + /// + /// This will start an isolate that then tries to connect to every port on the local system. + static Future connectToWarp(WarpShareContainer container) async { + // Scan for a port that is free on the current system + final random = Random(); + int currentPort = container.port; // Start with the port that the sharer desired + bool found = false; + while (!found) { + // Try connecting to the port + try { + await Socket.connect("localhost", currentPort); + + // Generate a new random port + currentPort = random.nextInt(65535 - 1024) + 1024; + + // This is just here in case this turns into an infinite loop and to prevent over-spinning + await Future.delayed(Duration(milliseconds: 100)); + } catch (e) { + found = true; + } + } + + // Use the port that's been scanned above to start a socket for the Warp + final warp = ConnectedWarp(container.id, container.port, currentPort, container.account); + await warp.startServer(); + + return warp; + } +} diff --git a/lib/services/spaces/warp/warp_shared.dart b/lib/services/spaces/warp/warp_shared.dart new file mode 100644 index 00000000..a067d2e4 --- /dev/null +++ b/lib/services/spaces/warp/warp_shared.dart @@ -0,0 +1,204 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/services/connection/messaging.dart'; +import 'package:chat_interface/services/spaces/space_connection.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/logging_framework.dart'; + +/// A Warp that has been shared by the user. +class SharedWarp { + /// The id of the Warp (to identify it) + final String id; + + /// The port being shared through Warp + final int port; + + SharedWarp(this.id, this.port); + + /// All the current connections coming from clients + final _sockets = >{}; + + /// Completers to make sure packets aren't sent before the socket is connected + final _completers = >>{}; + + // All the subscriptions made for the sockets (they need to be cancelled on disconnect) + final _subs = >{}; + + // All sequence related stuff for jitter buffering (can happen cause server) + final _sequenceNumbers = >{}; + final _packetQueue = >>{}; + + /// Send a packet from a client to the Warp. + /// + /// This will also open a new connection to the server in case there isn't one yet (for this client). + Future receivePacketFromClient(String id, int connId, Uint8List bytes, int seq) async { + if (_sockets[id] == null) { + sendLog("reset"); + + // Initialize the socket storage and completers + _sockets[id] = {}; + _completers[id] = >{}; + + // Initialize jitter buffering with correct values + _packetQueue[id] = >{}; + _sequenceNumbers[id] = {}; + } + + // Check if there is a connection already + if (_sockets[id]![connId] == null) { + // Make sure other packets wait + _sequenceNumbers[id]![connId] = 0; + final completer = Completer(); + _completers[id]![connId] = completer; + + // Create a new connection to the local server + try { + final socket = await Socket.connect("localhost", port); + registerListener(id, connId, socket); + _sockets[id]![connId] = socket; + completer.complete(true); + } catch (e) { + sendLog("couldn't connect to local server: $e"); + completer.complete(false); + return false; + } + } + + // Check if there is a completer to wait for + if (_completers[id]![connId] != null) { + final result = await _completers[id]![connId]!.future; + if (!result) { + return false; + } + } + + sendLog("received $connId $seq ${_sequenceNumbers[id]![connId]!}"); + + // Check what the last sequence number was + if (seq != _sequenceNumbers[id]![connId]! + 1) { + if (_packetQueue[id]![connId] == null) { + _packetQueue[id]![connId] = {}; + } + sendLog("packet queue $connId (share)"); + + // Add the packet to the packet queue for now + _packetQueue[id]![connId]![seq] = bytes; + return true; + } else { + _sequenceNumbers[id]![connId] = seq; + } + + // Send the packet to the socket + final socket = _sockets[id]![connId]!; + socket.add(bytes); + sendLog("sending $connId $seq"); + + // Check if there is a packet after it in the sequence queue + while (_packetQueue[id]![connId]?[seq + 1] != null) { + seq++; + + // Send the packet to the socket and remove it from the queue + socket.add(_packetQueue[id]![connId]![seq]!); + _packetQueue[id]![connId]!.remove(seq); + + sendLog("sending pq $connId $seq"); + + // Update the sequence number accordingly + _sequenceNumbers[id]![connId] = seq; + } + + return true; + } + + /// Register the listener that listens to the packets sent to the socket. + /// + /// This method will take all those packets and send them back to the other client + /// through the server. + void registerListener(String id, int connId, Socket socket) { + if (_subs[id] == null) { + _subs[id] = {}; + } + + int seq = 1; + _subs[id]![connId] = socket.listen( + (packet) { + sendPacketToClient(id, connId, packet, seq); + seq++; + }, + onError: (e) { + removeClientFromWarp(id); + }, + cancelOnError: true, + ); + } + + /// Send a packet to a client through the server. + Future sendPacketToClient(String id, int connId, Uint8List bytes, int seq) async { + final event = await SpaceConnection.spaceConnector!.sendActionAndWait( + ServerAction("wp_send_back", { + "w": this.id, // The parameter called "id" almost got me here xd + "t": id, + "c": connId, + "s": seq, + "p": base64Encode(encryptSymmetricBytes(bytes, SpaceController.key!)), + }), + ); + + // Remove the client from the warp in case the response from the server is invalid (or there was an error) + if (event == null || !event.data["success"]) { + removeClientFromWarp(id); + return; + } + } + + /// Disconnect a client from the Warp. + /// + /// This kicks them and blocks packets on the server side. + void removeClientFromWarp(String id) { + // Tell the server to kick the client + SpaceConnection.spaceConnector!.sendAction(ServerAction("wp_kick", {"w": this.id, "t": id})); + + // Disconnect them from the local server + handleDisconnect(id); + } + + /// Handle a client disconnect. + /// + /// This stops the client's connection to the local server. + void handleDisconnect(String id) { + if (_subs[id] != null) { + for (var sub in _subs[id]!.values) { + sub.cancel(); + } + } + if (_sockets[id] != null) { + for (var socket in _sockets[id]!.values) { + socket.close(); + } + } + _sequenceNumbers.remove(id); + _packetQueue.remove(id); + _subs.remove(id); + _sockets.remove(id); + } + + /// Stop the Warp completely. + /// + /// Disconnects all clients + tells the server about the closure. + void stop({bool action = true}) { + if (action) { + SpaceConnection.spaceConnector!.sendAction(ServerAction("wp_end", id)); + } + + // Disconnect all clients + for (var map in _sockets.values) { + for (var socket in map.values) { + socket.close(); + } + } + } +} diff --git a/lib/services/squares/square_container.dart b/lib/services/squares/square_container.dart new file mode 100644 index 00000000..03e22e46 --- /dev/null +++ b/lib/services/squares/square_container.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:sodium_libs/sodium_libs.dart'; + +class SquareContainer extends ConversationContainer { + late List topics; + late List spaces; + + SquareContainer(super.name, this.topics, this.spaces); + + @override + SquareContainer.fromJson(Map json) : super(json["name"]) { + // Parse all of the topics + topics = []; + if (json["topics"] != null) { + for (var topic in json["topics"]) { + topics.add(Topic.fromJson(topic)); + } + } + + // Parse all of the spaces + spaces = []; + if (json["spaces"] != null) { + for (var space in json["spaces"]) { + spaces.add(PinnedSharedSpace.fromJson(space)); + } + } + } + + @override + factory SquareContainer.decrypt(String cipherText, SecureKey key) { + return SquareContainer.fromJson(jsonDecode(decryptSymmetric(cipherText, key))); + } + + factory SquareContainer.copy(SquareContainer other) { + return SquareContainer(other.name, [...other.topics], [...other.spaces]); + } + + @override + String encrypted(SecureKey key) { + return encryptSymmetric(jsonEncode(toJson()), key); + } + + @override + Map toJson() { + final json = super.toJson(); + json["topics"] = topics.map((t) => t.toJson()).toList(); + json["spaces"] = spaces.map((s) => s.toJson()).toList(); + return json; + } +} + +class Topic { + final String id; + final String name; + + Topic(this.id, this.name); + Topic.fromJson(Map json) : this(json["id"], json["name"]); + + Map toJson() => {"id": id, "name": name}; +} + +class PinnedSharedSpace { + final String id; + final String name; + + PinnedSharedSpace(this.id, this.name); + PinnedSharedSpace.fromJson(Map json) : this(json["id"], json["name"]); + + Map toJson() => {"id": id, "name": name}; +} diff --git a/lib/services/squares/square_service.dart b/lib/services/squares/square_service.dart new file mode 100644 index 00000000..f1994ffc --- /dev/null +++ b/lib/services/squares/square_service.dart @@ -0,0 +1,231 @@ +import 'dart:math'; + +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/database/database_entities.dart' as model; +import 'package:chat_interface/services/chat/conversation_service.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/services/squares/square_shared_space.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/web.dart'; +import 'package:get/get_utils/get_utils.dart'; + +class SquareService { + /// Create a new square. + /// + /// Returns an error if there was one. + static Future openSquare(List friends, String name) async { + // Create the conversation for the square + return ConversationService.openConversation(model.ConversationType.square, friends, SquareContainer(name, [], [])); + } + + /// Add a topic to a square. + /// + /// Returns an error if there was one. + static Future createTopic(Square square, String name) async { + // Generate a new container for the square + final newContainer = SquareContainer.copy(square.container as SquareContainer); + String topicId = randomString(8); + while (newContainer.topics.any((t) => t.id == topicId)) { + topicId = randomString(8); + } + newContainer.topics.add(Topic(topicId, name)); + + // Try to change the data of the square to the new container + return await ConversationService.setData(square, newContainer); + } + + /// Cryptographically‐secure random generator + static final _rnd = Random.secure(); + + /// Generate a random string of given length using a-z, A-Z, 1-9 (requirements for topic id) + static String randomString(int length) { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789'; + return List.generate(length, (_) => chars[_rnd.nextInt(chars.length)]).join(); + } + + /// Rename a topic in a square. + /// + /// Returns an error if there was one. + static Future renameTopic(Square square, String id, String name) async { + // Create a new container with the modifications + final newContainer = SquareContainer.copy(square.container as SquareContainer); + final index = newContainer.topics.indexWhere((t) => t.id == id); + if (index == -1) { + return "not.found".tr; + } + newContainer.topics[index] = Topic(id, name); + + return await refreshContainer(square, newContainer); + } + + /// Delete a topic in a square. + /// + /// Returns an error if there was one. + static Future deleteTopic(Square square, String id) async { + // Create a new container with the modifications + final newContainer = SquareContainer.copy(square.container as SquareContainer); + newContainer.topics.removeWhere((t) => t.id == id); + + return await refreshContainer(square, newContainer); + } + + /// Create a new Space and add it to a square. + /// + /// Returns an error if there was one. + static Future createSharedSpace( + Square square, + String name, { + String underlyingId = "-", + bool rejoin = false, + }) async { + // Create a new Space (if not connected already) + if (!SpaceController.connected.peek() || rejoin) { + // Leave in case rejoin is true + if (rejoin && SpaceController.connected.peek()) { + await SpaceController.leaveSpace(); + } + + // Create a new Space + final error = await SpaceController.createSpace(false, openPage: false); + if (error != null) { + return error; + } + } + final container = SpaceController.getContainer(); + + // Add the Space to the square as a shared space + final json = await postNodeJSON("/conversations/shared_spaces/add", { + "token": square.token.toMap(square.id), + "data": { + "server": container.node, + "id": container.roomId, + "underlying_id": underlyingId, + "name": encryptSymmetric(name, square.key), + "container": encryptSymmetric(container.toInviteJson(), square.key), + }, + }); + if (!json["success"]) { + return json["error"]; + } + if (json["exists"]) { + return "squares.space.already_added".tr; + } + + return null; + } + + /// Pin a new shared space in a Square. + /// + /// Returns an error if there was one. + static Future pinSharedSpace(Square square, SharedSpace space) async { + // Add to the square as a new pinned space + final pinnedSpace = newPinnedSharedSpace(square, space.name); + final error = await pinPinnedSpace(square, pinnedSpace); + if (error != null) { + return error; + } + + // Change to pinned on the chat server + return await changePinnedStatus(square, space.id, pinnedSpace.id); + } + + /// Pin a pinned shared space in a Square. + /// + /// Returns an error if there was one. + static Future pinPinnedSpace(Square square, PinnedSharedSpace space) async { + final current = square.container as SquareContainer; + + // Generate a new container for the square + final newContainer = SquareContainer.copy(current); + newContainer.spaces.add(space); + + // Set the new data + return await ConversationService.setData(square, newContainer); + } + + /// Generate a new pinned shared space for a square. + static PinnedSharedSpace newPinnedSharedSpace(Square square, String name) { + final container = square.container as SquareContainer; + + // Generate a new id for the pinned shared space + String sharedSpaceId = randomString(8); + while (container.spaces.any((s) => s.id == sharedSpaceId)) { + sharedSpaceId = randomString(8); + } + + return PinnedSharedSpace(sharedSpaceId, name); + } + + /// Unpin a shared space in a Square. + /// Set [space] in case currently shared. + /// + /// Returns an error if there was one. + static Future unpinSharedSpace(Square square, String id, {SharedSpace? space}) async { + // Generate a new container for the square + final newContainer = SquareContainer.copy(square.container as SquareContainer); + newContainer.spaces.removeWhere((s) => s.id == id); + + // Set the new data + var error = await ConversationService.setData(square, newContainer); + if (error != null) { + return error; + } + + // Change status on the server + if (space != null) { + return await changePinnedStatus(square, space.id, "-"); + } + return null; + } + + /// Rename a pinned shared space. + /// + /// Returns an error if there was one. + static Future changePinnedName(Square square, PinnedSharedSpace space, String name) async { + // Generate a new container for the square + final newContainer = SquareContainer.copy(square.container as SquareContainer); + final index = newContainer.spaces.indexOf(space); + if (index == -1) { + return "not.found".tr; + } + newContainer.spaces[index] = PinnedSharedSpace(space.id, name); + + // Set the new data + return await ConversationService.setData(square, newContainer); + } + + /// Change the pin status on the chat server. + /// + /// Returns an error if there was one. + static Future changePinnedStatus(Square square, String id, String underlying) async { + final json = await postNodeJSON("/conversations/shared_spaces/pin_status", { + "token": square.token.toMap(square.id), + "data": {"id": id, "underlying": underlying}, + }); + if (!json["success"]) { + return json["error"]; + } + return null; + } + + /// Change the name on the chat server. + /// + /// Returns an error if there was one. + static Future renameSharedSpace(Square square, String id, String name) async { + final json = await postNodeJSON("/conversations/shared_spaces/rename", { + "token": square.token.toMap(square.id), + "data": {"id": id, "name": encryptSymmetric(name, square.key)}, + }); + if (!json["success"]) { + return json["error"]; + } + return null; + } + + /// Refresh the container of a Square. + static Future refreshContainer(Square square, SquareContainer container) { + return ConversationService.setData(square, container); + } +} diff --git a/lib/services/squares/square_shared_space.dart b/lib/services/squares/square_shared_space.dart new file mode 100644 index 00000000..b37f0688 --- /dev/null +++ b/lib/services/squares/square_shared_space.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +import 'package:chat_interface/services/spaces/space_container.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; +import 'package:sodium_libs/sodium_libs.dart'; + +class SharedSpace { + final String id; + final String underlyingId; + final String name; + final SpaceConnectionContainer container; + final List members; + + SharedSpace(this.id, this.underlyingId, this.name, this.container, this.members); + + factory SharedSpace.fromJson(Map json, SecureKey conversationKey) { + // Decrypt the connection container for the space + final container = SpaceConnectionContainer.fromJson( + jsonDecode(decryptSymmetric(json["container"], conversationKey)), + ); + + // Decrypt the members using this key (if there are any) + List members; + if (json["members"] != null) { + final jsonMembers = json["members"] as List; + members = List.filled(jsonMembers.length, "", growable: true); + for (int i = 0; i < jsonMembers.length; i++) { + members[i] = decryptSymmetric(jsonMembers[i], container.key); + } + } else { + members = []; + } + + // Return the actual instance of the shared space with the rest + final name = (json["name"] ?? "") != "" ? decryptSymmetric(json["name"], conversationKey) : ""; + return SharedSpace(json["id"], json["underlying"], name, container, members); + } +} diff --git a/lib/signal_demo.dart b/lib/signal_demo.dart new file mode 100644 index 00000000..795b3b13 --- /dev/null +++ b/lib/signal_demo.dart @@ -0,0 +1,34 @@ +import 'package:chat_interface/theme/components/forms/fj_button.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:signals/signals_flutter.dart'; + +class SignalDemo extends StatefulWidget { + const SignalDemo({super.key}); + + @override + State createState() => _SignalDemoState(); +} + +class _SignalDemoState extends State with SignalsMixin { + late final count = createSignal(0); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Current value: ${count.value}"), + verticalSpacing(sectionSpacing), + FJElevatedButton(onTap: () => count.value++, child: Text("Increment")), + ], + ), + ), + ), + ); + } +} diff --git a/lib/size_demo.dart b/lib/size_demo.dart index 3243fc73..dd4a7b9a 100644 --- a/lib/size_demo.dart +++ b/lib/size_demo.dart @@ -28,14 +28,10 @@ class SizeDemo extends StatelessWidget { children: List.generate( n, (index) => ConstrainedBox( - constraints: BoxConstraints( - maxHeight: computedHeight, - ), + constraints: BoxConstraints(maxHeight: computedHeight), child: AspectRatio( aspectRatio: 16 / 9, - child: Container( - color: Colors.primaries[index % Colors.primaries.length], - ), + child: Container(color: Colors.primaries[index % Colors.primaries.length]), ), ), ), diff --git a/lib/src/rust/api/audio_devices.dart b/lib/src/rust/api/audio_devices.dart new file mode 100644 index 00000000..b7293898 --- /dev/null +++ b/lib/src/rust/api/audio_devices.dart @@ -0,0 +1,57 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Get all audio input devices on the system. +Future> getInputDevices() => + RustLib.instance.api.crateApiAudioDevicesGetInputDevices(); + +Future getDefaultInputDevice() => + RustLib.instance.api.crateApiAudioDevicesGetDefaultInputDevice(); + +/// Get all audio output devices on the system. +Future> getOutputDevices() => + RustLib.instance.api.crateApiAudioDevicesGetOutputDevices(); + +Future getDefaultOutputDevice() => + RustLib.instance.api.crateApiAudioDevicesGetDefaultOutputDevice(); + +class AudioInputDevice { + final String name; + final bool systemDefault; + + const AudioInputDevice({required this.name, required this.systemDefault}); + + @override + int get hashCode => name.hashCode ^ systemDefault.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AudioInputDevice && + runtimeType == other.runtimeType && + name == other.name && + systemDefault == other.systemDefault; +} + +class AudioOuputDevice { + final String name; + final bool systemDefault; + + const AudioOuputDevice({required this.name, required this.systemDefault}); + + @override + int get hashCode => name.hashCode ^ systemDefault.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AudioOuputDevice && + runtimeType == other.runtimeType && + name == other.name && + systemDefault == other.systemDefault; +} diff --git a/lib/src/rust/api/engine.dart b/lib/src/rust/api/engine.dart new file mode 100644 index 00000000..7da4ded1 --- /dev/null +++ b/lib/src/rust/api/engine.dart @@ -0,0 +1,61 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +Future createLightwireEngine() => RustLib.instance.api.crateApiEngineCreateLightwireEngine(); + +Stream<(Uint8List?, double?, bool?)> startPacketStream({required LightwireEngine engine}) => + RustLib.instance.api.crateApiEngineStartPacketStream(engine: engine); + +Future setVoiceEnabled({required LightwireEngine engine, required bool enabled}) => + RustLib.instance.api.crateApiEngineSetVoiceEnabled(engine: engine, enabled: enabled); + +Future setInputDevice({required LightwireEngine engine, required String device}) => + RustLib.instance.api.crateApiEngineSetInputDevice(engine: engine, device: device); + +Future setAudioEnabled({required LightwireEngine engine, required bool enabled}) => + RustLib.instance.api.crateApiEngineSetAudioEnabled(engine: engine, enabled: enabled); + +Future setOutputDevice({required LightwireEngine engine, required String device}) => + RustLib.instance.api.crateApiEngineSetOutputDevice(engine: engine, device: device); + +Future setActivityDetection({required LightwireEngine engine, required bool enabled}) => + RustLib.instance.api.crateApiEngineSetActivityDetection(engine: engine, enabled: enabled); + +Future setAutomaticDetection({required LightwireEngine engine, required bool enabled}) => + RustLib.instance.api.crateApiEngineSetAutomaticDetection(engine: engine, enabled: enabled); + +Future setTalkingAmplitude({required LightwireEngine engine, required double amplitude}) => + RustLib.instance.api.crateApiEngineSetTalkingAmplitude(engine: engine, amplitude: amplitude); + +Future setEncodingBitrate({ + required LightwireEngine engine, + required bool auto, + required bool max, + required int bitrate, +}) => RustLib.instance.api.crateApiEngineSetEncodingBitrate(engine: engine, auto: auto, max: max, bitrate: bitrate); + +Future handlePacket({required LightwireEngine engine, required String id, required List packet}) => + RustLib.instance.api.crateApiEngineHandlePacket(engine: engine, id: id, packet: packet); + +Future stopEngine({required LightwireEngine engine}) => + RustLib.instance.api.crateApiEngineStopEngine(engine: engine); + +Future stopAllEngines() => RustLib.instance.api.crateApiEngineStopAllEngines(); + +class LightwireEngine { + final int id; + + const LightwireEngine({required this.id}); + + @override + int get hashCode => id.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || other is LightwireEngine && runtimeType == other.runtimeType && id == other.id; +} diff --git a/lib/src/rust/api/general.dart b/lib/src/rust/api/general.dart new file mode 100644 index 00000000..1fabad5d --- /dev/null +++ b/lib/src/rust/api/general.dart @@ -0,0 +1,10 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +Stream createLogStream() => + RustLib.instance.api.crateApiGeneralCreateLogStream(); diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart new file mode 100644 index 00000000..91e04335 --- /dev/null +++ b/lib/src/rust/frb_generated.dart @@ -0,0 +1,1333 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'api/audio_devices.dart'; +import 'api/engine.dart'; +import 'api/general.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'frb_generated.dart'; +import 'frb_generated.io.dart' + if (dart.library.js_interop) 'frb_generated.web.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Main entrypoint of the Rust API +class RustLib extends BaseEntrypoint { + @internal + static final instance = RustLib._(); + + RustLib._(); + + /// Initialize flutter_rust_bridge + static Future init({ + RustLibApi? api, + BaseHandler? handler, + ExternalLibrary? externalLibrary, + }) async { + await instance.initImpl( + api: api, + handler: handler, + externalLibrary: externalLibrary, + ); + } + + /// Initialize flutter_rust_bridge in mock mode. + /// No libraries for FFI are loaded. + static void initMock({required RustLibApi api}) { + instance.initMockImpl(api: api); + } + + /// Dispose flutter_rust_bridge + /// + /// The call to this function is optional, since flutter_rust_bridge (and everything else) + /// is automatically disposed when the app stops. + static void dispose() => instance.disposeImpl(); + + @override + ApiImplConstructor get apiImplConstructor => + RustLibApiImpl.new; + + @override + WireConstructor get wireConstructor => + RustLibWire.fromExternalLibrary; + + @override + Future executeRustInitializers() async {} + + @override + ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => + kDefaultExternalLibraryLoaderConfig; + + @override + String get codegenVersion => '2.9.0'; + + @override + int get rustContentHash => 2025983791; + + static const kDefaultExternalLibraryLoaderConfig = + ExternalLibraryLoaderConfig( + stem: 'libspaceship', + ioDirectory: 'libspaceship/target/release/', + webPrefix: 'pkg/', + ); +} + +abstract class RustLibApi extends BaseApi { + Future crateApiEngineCreateLightwireEngine(); + + Stream crateApiGeneralCreateLogStream(); + + Future crateApiAudioDevicesGetDefaultInputDevice(); + + Future crateApiAudioDevicesGetDefaultOutputDevice(); + + Future> crateApiAudioDevicesGetInputDevices(); + + Future> crateApiAudioDevicesGetOutputDevices(); + + Future crateApiEngineHandlePacket({ + required LightwireEngine engine, + required String id, + required List packet, + }); + + Future crateApiEngineSetActivityDetection({ + required LightwireEngine engine, + required bool enabled, + }); + + Future crateApiEngineSetAudioEnabled({ + required LightwireEngine engine, + required bool enabled, + }); + + Future crateApiEngineSetAutomaticDetection({ + required LightwireEngine engine, + required bool enabled, + }); + + Future crateApiEngineSetEncodingBitrate({ + required LightwireEngine engine, + required bool auto, + required bool max, + required int bitrate, + }); + + Future crateApiEngineSetInputDevice({ + required LightwireEngine engine, + required String device, + }); + + Future crateApiEngineSetOutputDevice({ + required LightwireEngine engine, + required String device, + }); + + Future crateApiEngineSetTalkingAmplitude({ + required LightwireEngine engine, + required double amplitude, + }); + + Future crateApiEngineSetVoiceEnabled({ + required LightwireEngine engine, + required bool enabled, + }); + + Stream<(Uint8List?, double?, bool?)> crateApiEngineStartPacketStream({ + required LightwireEngine engine, + }); + + Future crateApiEngineStopAllEngines(); + + Future crateApiEngineStopEngine({required LightwireEngine engine}); +} + +class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { + RustLibApiImpl({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @override + Future crateApiEngineCreateLightwireEngine() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 1, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_lightwire_engine, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineCreateLightwireEngineConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineCreateLightwireEngineConstMeta => + const TaskConstMeta(debugName: "create_lightwire_engine", argNames: []); + + @override + Stream crateApiGeneralCreateLogStream() { + final sink = RustStreamSink(); + unawaited( + handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_StreamSink_String_Sse(sink, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 2, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiGeneralCreateLogStreamConstMeta, + argValues: [sink], + apiImpl: this, + ), + ), + ); + return sink.stream; + } + + TaskConstMeta get kCrateApiGeneralCreateLogStreamConstMeta => + const TaskConstMeta(debugName: "create_log_stream", argNames: ["sink"]); + + @override + Future crateApiAudioDevicesGetDefaultInputDevice() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 3, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_audio_input_device, + decodeErrorData: null, + ), + constMeta: kCrateApiAudioDevicesGetDefaultInputDeviceConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiAudioDevicesGetDefaultInputDeviceConstMeta => + const TaskConstMeta(debugName: "get_default_input_device", argNames: []); + + @override + Future crateApiAudioDevicesGetDefaultOutputDevice() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 4, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_audio_ouput_device, + decodeErrorData: null, + ), + constMeta: kCrateApiAudioDevicesGetDefaultOutputDeviceConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiAudioDevicesGetDefaultOutputDeviceConstMeta => + const TaskConstMeta(debugName: "get_default_output_device", argNames: []); + + @override + Future> crateApiAudioDevicesGetInputDevices() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 5, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_audio_input_device, + decodeErrorData: null, + ), + constMeta: kCrateApiAudioDevicesGetInputDevicesConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiAudioDevicesGetInputDevicesConstMeta => + const TaskConstMeta(debugName: "get_input_devices", argNames: []); + + @override + Future> crateApiAudioDevicesGetOutputDevices() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 6, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_audio_ouput_device, + decodeErrorData: null, + ), + constMeta: kCrateApiAudioDevicesGetOutputDevicesConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiAudioDevicesGetOutputDevicesConstMeta => + const TaskConstMeta(debugName: "get_output_devices", argNames: []); + + @override + Future crateApiEngineHandlePacket({ + required LightwireEngine engine, + required String id, + required List packet, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_String(id, serializer); + sse_encode_list_prim_u_8_loose(packet, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 7, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineHandlePacketConstMeta, + argValues: [engine, id, packet], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineHandlePacketConstMeta => const TaskConstMeta( + debugName: "handle_packet", + argNames: ["engine", "id", "packet"], + ); + + @override + Future crateApiEngineSetActivityDetection({ + required LightwireEngine engine, + required bool enabled, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_bool(enabled, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 8, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineSetActivityDetectionConstMeta, + argValues: [engine, enabled], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineSetActivityDetectionConstMeta => + const TaskConstMeta( + debugName: "set_activity_detection", + argNames: ["engine", "enabled"], + ); + + @override + Future crateApiEngineSetAudioEnabled({ + required LightwireEngine engine, + required bool enabled, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_bool(enabled, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 9, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineSetAudioEnabledConstMeta, + argValues: [engine, enabled], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineSetAudioEnabledConstMeta => + const TaskConstMeta( + debugName: "set_audio_enabled", + argNames: ["engine", "enabled"], + ); + + @override + Future crateApiEngineSetAutomaticDetection({ + required LightwireEngine engine, + required bool enabled, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_bool(enabled, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 10, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineSetAutomaticDetectionConstMeta, + argValues: [engine, enabled], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineSetAutomaticDetectionConstMeta => + const TaskConstMeta( + debugName: "set_automatic_detection", + argNames: ["engine", "enabled"], + ); + + @override + Future crateApiEngineSetEncodingBitrate({ + required LightwireEngine engine, + required bool auto, + required bool max, + required int bitrate, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_bool(auto, serializer); + sse_encode_bool(max, serializer); + sse_encode_i_32(bitrate, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 11, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineSetEncodingBitrateConstMeta, + argValues: [engine, auto, max, bitrate], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineSetEncodingBitrateConstMeta => + const TaskConstMeta( + debugName: "set_encoding_bitrate", + argNames: ["engine", "auto", "max", "bitrate"], + ); + + @override + Future crateApiEngineSetInputDevice({ + required LightwireEngine engine, + required String device, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_String(device, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 12, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineSetInputDeviceConstMeta, + argValues: [engine, device], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineSetInputDeviceConstMeta => + const TaskConstMeta( + debugName: "set_input_device", + argNames: ["engine", "device"], + ); + + @override + Future crateApiEngineSetOutputDevice({ + required LightwireEngine engine, + required String device, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_String(device, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 13, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineSetOutputDeviceConstMeta, + argValues: [engine, device], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineSetOutputDeviceConstMeta => + const TaskConstMeta( + debugName: "set_output_device", + argNames: ["engine", "device"], + ); + + @override + Future crateApiEngineSetTalkingAmplitude({ + required LightwireEngine engine, + required double amplitude, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_f_32(amplitude, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 14, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineSetTalkingAmplitudeConstMeta, + argValues: [engine, amplitude], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineSetTalkingAmplitudeConstMeta => + const TaskConstMeta( + debugName: "set_talking_amplitude", + argNames: ["engine", "amplitude"], + ); + + @override + Future crateApiEngineSetVoiceEnabled({ + required LightwireEngine engine, + required bool enabled, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_bool(enabled, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 15, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineSetVoiceEnabledConstMeta, + argValues: [engine, enabled], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineSetVoiceEnabledConstMeta => + const TaskConstMeta( + debugName: "set_voice_enabled", + argNames: ["engine", "enabled"], + ); + + @override + Stream<(Uint8List?, double?, bool?)> crateApiEngineStartPacketStream({ + required LightwireEngine engine, + }) { + final packetSink = RustStreamSink<(Uint8List?, double?, bool?)>(); + unawaited( + handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + sse_encode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + packetSink, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 16, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineStartPacketStreamConstMeta, + argValues: [engine, packetSink], + apiImpl: this, + ), + ), + ); + return packetSink.stream; + } + + TaskConstMeta get kCrateApiEngineStartPacketStreamConstMeta => + const TaskConstMeta( + debugName: "start_packet_stream", + argNames: ["engine", "packetSink"], + ); + + @override + Future crateApiEngineStopAllEngines() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 17, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineStopAllEnginesConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineStopAllEnginesConstMeta => + const TaskConstMeta(debugName: "stop_all_engines", argNames: []); + + @override + Future crateApiEngineStopEngine({required LightwireEngine engine}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_lightwire_engine(engine, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 18, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiEngineStopEngineConstMeta, + argValues: [engine], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiEngineStopEngineConstMeta => + const TaskConstMeta(debugName: "stop_engine", argNames: ["engine"]); + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AnyhowException(raw as String); + } + + @protected + RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(); + } + + @protected + RustStreamSink<(Uint8List?, double?, bool?)> + dco_decode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(); + } + + @protected + String dco_decode_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as String; + } + + @protected + AudioInputDevice dco_decode_audio_input_device(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return AudioInputDevice( + name: dco_decode_String(arr[0]), + systemDefault: dco_decode_bool(arr[1]), + ); + } + + @protected + AudioOuputDevice dco_decode_audio_ouput_device(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return AudioOuputDevice( + name: dco_decode_String(arr[0]), + systemDefault: dco_decode_bool(arr[1]), + ); + } + + @protected + bool dco_decode_bool(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as bool; + } + + @protected + bool dco_decode_box_autoadd_bool(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as bool; + } + + @protected + double dco_decode_box_autoadd_f_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as double; + } + + @protected + LightwireEngine dco_decode_box_autoadd_lightwire_engine(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_lightwire_engine(raw); + } + + @protected + double dco_decode_f_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as double; + } + + @protected + int dco_decode_i_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + LightwireEngine dco_decode_lightwire_engine(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 1) + throw Exception('unexpected arr length: expect 1 but see ${arr.length}'); + return LightwireEngine(id: dco_decode_u_32(arr[0])); + } + + @protected + List dco_decode_list_audio_input_device(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_audio_input_device).toList(); + } + + @protected + List dco_decode_list_audio_ouput_device(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_audio_ouput_device).toList(); + } + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as List; + } + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint8List; + } + + @protected + bool? dco_decode_opt_box_autoadd_bool(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_bool(raw); + } + + @protected + double? dco_decode_opt_box_autoadd_f_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_f_32(raw); + } + + @protected + Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_list_prim_u_8_strict(raw); + } + + @protected + (Uint8List?, double?, bool?) + dco_decode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) { + throw Exception('Expected 3 elements, got ${arr.length}'); + } + return ( + dco_decode_opt_list_prim_u_8_strict(arr[0]), + dco_decode_opt_box_autoadd_f_32(arr[1]), + dco_decode_opt_box_autoadd_bool(arr[2]), + ); + } + + @protected + int dco_decode_u_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + int dco_decode_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + void dco_decode_unit(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return; + } + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_String(deserializer); + return AnyhowException(inner); + } + + @protected + RustStreamSink sse_decode_StreamSink_String_Sse( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + throw UnimplementedError('Unreachable ()'); + } + + @protected + RustStreamSink<(Uint8List?, double?, bool?)> + sse_decode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + throw UnimplementedError('Unreachable ()'); + } + + @protected + String sse_decode_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return utf8.decoder.convert(inner); + } + + @protected + AudioInputDevice sse_decode_audio_input_device(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_name = sse_decode_String(deserializer); + var var_systemDefault = sse_decode_bool(deserializer); + return AudioInputDevice(name: var_name, systemDefault: var_systemDefault); + } + + @protected + AudioOuputDevice sse_decode_audio_ouput_device(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_name = sse_decode_String(deserializer); + var var_systemDefault = sse_decode_bool(deserializer); + return AudioOuputDevice(name: var_name, systemDefault: var_systemDefault); + } + + @protected + bool sse_decode_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8() != 0; + } + + @protected + bool sse_decode_box_autoadd_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_bool(deserializer)); + } + + @protected + double sse_decode_box_autoadd_f_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_f_32(deserializer)); + } + + @protected + LightwireEngine sse_decode_box_autoadd_lightwire_engine( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_lightwire_engine(deserializer)); + } + + @protected + double sse_decode_f_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getFloat32(); + } + + @protected + int sse_decode_i_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getInt32(); + } + + @protected + LightwireEngine sse_decode_lightwire_engine(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_id = sse_decode_u_32(deserializer); + return LightwireEngine(id: var_id); + } + + @protected + List sse_decode_list_audio_input_device( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_audio_input_device(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_audio_ouput_device( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_audio_ouput_device(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_bool(deserializer)); + } else { + return null; + } + } + + @protected + double? sse_decode_opt_box_autoadd_f_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_f_32(deserializer)); + } else { + return null; + } + } + + @protected + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_list_prim_u_8_strict(deserializer)); + } else { + return null; + } + } + + @protected + (Uint8List?, double?, bool?) + sse_decode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = sse_decode_opt_list_prim_u_8_strict(deserializer); + var var_field1 = sse_decode_opt_box_autoadd_f_32(deserializer); + var var_field2 = sse_decode_opt_box_autoadd_bool(deserializer); + return (var_field0, var_field1, var_field2); + } + + @protected + int sse_decode_u_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint32(); + } + + @protected + int sse_decode_u_8(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8(); + } + + @protected + void sse_decode_unit(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.message, serializer); + } + + @protected + void sse_encode_StreamSink_String_Sse( + RustStreamSink self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String( + self.setupAndSerialize( + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + ), + serializer, + ); + } + + @protected + void + sse_encode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + RustStreamSink<(Uint8List?, double?, bool?)> self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String( + self.setupAndSerialize( + codec: SseCodec( + decodeSuccessData: + sse_decode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool, + decodeErrorData: sse_decode_AnyhowException, + ), + ), + serializer, + ); + } + + @protected + void sse_encode_String(String self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); + } + + @protected + void sse_encode_audio_input_device( + AudioInputDevice self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.name, serializer); + sse_encode_bool(self.systemDefault, serializer); + } + + @protected + void sse_encode_audio_ouput_device( + AudioOuputDevice self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.name, serializer); + sse_encode_bool(self.systemDefault, serializer); + } + + @protected + void sse_encode_bool(bool self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self ? 1 : 0); + } + + @protected + void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_bool(self, serializer); + } + + @protected + void sse_encode_box_autoadd_f_32(double self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_f_32(self, serializer); + } + + @protected + void sse_encode_box_autoadd_lightwire_engine( + LightwireEngine self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_lightwire_engine(self, serializer); + } + + @protected + void sse_encode_f_32(double self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putFloat32(self); + } + + @protected + void sse_encode_i_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putInt32(self); + } + + @protected + void sse_encode_lightwire_engine( + LightwireEngine self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_32(self.id, serializer); + } + + @protected + void sse_encode_list_audio_input_device( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_audio_input_device(item, serializer); + } + } + + @protected + void sse_encode_list_audio_ouput_device( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_audio_ouput_device(item, serializer); + } + } + + @protected + void sse_encode_list_prim_u_8_loose( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List( + self is Uint8List ? self : Uint8List.fromList(self), + ); + } + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List(self); + } + + @protected + void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_bool(self, serializer); + } + } + + @protected + void sse_encode_opt_box_autoadd_f_32(double? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_f_32(self, serializer); + } + } + + @protected + void sse_encode_opt_list_prim_u_8_strict( + Uint8List? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_list_prim_u_8_strict(self, serializer); + } + } + + @protected + void + sse_encode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + (Uint8List?, double?, bool?) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_opt_list_prim_u_8_strict(self.$1, serializer); + sse_encode_opt_box_autoadd_f_32(self.$2, serializer); + sse_encode_opt_box_autoadd_bool(self.$3, serializer); + } + + @protected + void sse_encode_u_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint32(self); + } + + @protected + void sse_encode_u_8(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self); + } + + @protected + void sse_encode_unit(void self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } +} diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart new file mode 100644 index 00000000..962d6aee --- /dev/null +++ b/lib/src/rust/frb_generated.io.dart @@ -0,0 +1,311 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'api/audio_devices.dart'; +import 'api/engine.dart'; +import 'api/general.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi' as ffi; +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw); + + @protected + RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw); + + @protected + RustStreamSink<(Uint8List?, double?, bool?)> + dco_decode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + dynamic raw, + ); + + @protected + String dco_decode_String(dynamic raw); + + @protected + AudioInputDevice dco_decode_audio_input_device(dynamic raw); + + @protected + AudioOuputDevice dco_decode_audio_ouput_device(dynamic raw); + + @protected + bool dco_decode_bool(dynamic raw); + + @protected + bool dco_decode_box_autoadd_bool(dynamic raw); + + @protected + double dco_decode_box_autoadd_f_32(dynamic raw); + + @protected + LightwireEngine dco_decode_box_autoadd_lightwire_engine(dynamic raw); + + @protected + double dco_decode_f_32(dynamic raw); + + @protected + int dco_decode_i_32(dynamic raw); + + @protected + LightwireEngine dco_decode_lightwire_engine(dynamic raw); + + @protected + List dco_decode_list_audio_input_device(dynamic raw); + + @protected + List dco_decode_list_audio_ouput_device(dynamic raw); + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + bool? dco_decode_opt_box_autoadd_bool(dynamic raw); + + @protected + double? dco_decode_opt_box_autoadd_f_32(dynamic raw); + + @protected + Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + + @protected + (Uint8List?, double?, bool?) + dco_decode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + dynamic raw, + ); + + @protected + int dco_decode_u_32(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + + @protected + RustStreamSink sse_decode_StreamSink_String_Sse( + SseDeserializer deserializer, + ); + + @protected + RustStreamSink<(Uint8List?, double?, bool?)> + sse_decode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + SseDeserializer deserializer, + ); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + AudioInputDevice sse_decode_audio_input_device(SseDeserializer deserializer); + + @protected + AudioOuputDevice sse_decode_audio_ouput_device(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + bool sse_decode_box_autoadd_bool(SseDeserializer deserializer); + + @protected + double sse_decode_box_autoadd_f_32(SseDeserializer deserializer); + + @protected + LightwireEngine sse_decode_box_autoadd_lightwire_engine( + SseDeserializer deserializer, + ); + + @protected + double sse_decode_f_32(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + LightwireEngine sse_decode_lightwire_engine(SseDeserializer deserializer); + + @protected + List sse_decode_list_audio_input_device( + SseDeserializer deserializer, + ); + + @protected + List sse_decode_list_audio_ouput_device( + SseDeserializer deserializer, + ); + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer); + + @protected + double? sse_decode_opt_box_autoadd_f_32(SseDeserializer deserializer); + + @protected + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + (Uint8List?, double?, bool?) + sse_decode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + SseDeserializer deserializer, + ); + + @protected + int sse_decode_u_32(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_StreamSink_String_Sse( + RustStreamSink self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + RustStreamSink<(Uint8List?, double?, bool?)> self, + SseSerializer serializer, + ); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_audio_input_device( + AudioInputDevice self, + SseSerializer serializer, + ); + + @protected + void sse_encode_audio_ouput_device( + AudioOuputDevice self, + SseSerializer serializer, + ); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_f_32(double self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_lightwire_engine( + LightwireEngine self, + SseSerializer serializer, + ); + + @protected + void sse_encode_f_32(double self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_lightwire_engine( + LightwireEngine self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_audio_input_device( + List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_audio_ouput_device( + List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer); + + @protected + void sse_encode_opt_box_autoadd_f_32(double? self, SseSerializer serializer); + + @protected + void sse_encode_opt_list_prim_u_8_strict( + Uint8List? self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + (Uint8List?, double?, bool?) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_u_32(int self, SseSerializer serializer); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => + RustLibWire(lib.ffiDynamicLibrary); + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + RustLibWire(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; +} diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart new file mode 100644 index 00000000..aa110fb1 --- /dev/null +++ b/lib/src/rust/frb_generated.web.dart @@ -0,0 +1,311 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +// Static analysis wrongly picks the IO variant, thus ignore this +// ignore_for_file: argument_type_not_assignable + +import 'api/audio_devices.dart'; +import 'api/engine.dart'; +import 'api/general.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw); + + @protected + RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw); + + @protected + RustStreamSink<(Uint8List?, double?, bool?)> + dco_decode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + dynamic raw, + ); + + @protected + String dco_decode_String(dynamic raw); + + @protected + AudioInputDevice dco_decode_audio_input_device(dynamic raw); + + @protected + AudioOuputDevice dco_decode_audio_ouput_device(dynamic raw); + + @protected + bool dco_decode_bool(dynamic raw); + + @protected + bool dco_decode_box_autoadd_bool(dynamic raw); + + @protected + double dco_decode_box_autoadd_f_32(dynamic raw); + + @protected + LightwireEngine dco_decode_box_autoadd_lightwire_engine(dynamic raw); + + @protected + double dco_decode_f_32(dynamic raw); + + @protected + int dco_decode_i_32(dynamic raw); + + @protected + LightwireEngine dco_decode_lightwire_engine(dynamic raw); + + @protected + List dco_decode_list_audio_input_device(dynamic raw); + + @protected + List dco_decode_list_audio_ouput_device(dynamic raw); + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + bool? dco_decode_opt_box_autoadd_bool(dynamic raw); + + @protected + double? dco_decode_opt_box_autoadd_f_32(dynamic raw); + + @protected + Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + + @protected + (Uint8List?, double?, bool?) + dco_decode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + dynamic raw, + ); + + @protected + int dco_decode_u_32(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + + @protected + RustStreamSink sse_decode_StreamSink_String_Sse( + SseDeserializer deserializer, + ); + + @protected + RustStreamSink<(Uint8List?, double?, bool?)> + sse_decode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + SseDeserializer deserializer, + ); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + AudioInputDevice sse_decode_audio_input_device(SseDeserializer deserializer); + + @protected + AudioOuputDevice sse_decode_audio_ouput_device(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + bool sse_decode_box_autoadd_bool(SseDeserializer deserializer); + + @protected + double sse_decode_box_autoadd_f_32(SseDeserializer deserializer); + + @protected + LightwireEngine sse_decode_box_autoadd_lightwire_engine( + SseDeserializer deserializer, + ); + + @protected + double sse_decode_f_32(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + LightwireEngine sse_decode_lightwire_engine(SseDeserializer deserializer); + + @protected + List sse_decode_list_audio_input_device( + SseDeserializer deserializer, + ); + + @protected + List sse_decode_list_audio_ouput_device( + SseDeserializer deserializer, + ); + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer); + + @protected + double? sse_decode_opt_box_autoadd_f_32(SseDeserializer deserializer); + + @protected + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + (Uint8List?, double?, bool?) + sse_decode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + SseDeserializer deserializer, + ); + + @protected + int sse_decode_u_32(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ); + + @protected + void sse_encode_StreamSink_String_Sse( + RustStreamSink self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_StreamSink_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool_Sse( + RustStreamSink<(Uint8List?, double?, bool?)> self, + SseSerializer serializer, + ); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_audio_input_device( + AudioInputDevice self, + SseSerializer serializer, + ); + + @protected + void sse_encode_audio_ouput_device( + AudioOuputDevice self, + SseSerializer serializer, + ); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_f_32(double self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_lightwire_engine( + LightwireEngine self, + SseSerializer serializer, + ); + + @protected + void sse_encode_f_32(double self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_lightwire_engine( + LightwireEngine self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_audio_input_device( + List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_audio_ouput_device( + List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer); + + @protected + void sse_encode_opt_box_autoadd_f_32(double? self, SseSerializer serializer); + + @protected + void sse_encode_opt_list_prim_u_8_strict( + Uint8List? self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_record_opt_list_prim_u_8_strict_opt_box_autoadd_f_32_opt_box_autoadd_bool( + (Uint8List?, double?, bool?) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_u_32(int self, SseSerializer serializer); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + RustLibWire.fromExternalLibrary(ExternalLibrary lib); +} + +@JS('wasm_bindgen') +external RustLibWasmModule get wasmModule; + +@JS() +@anonymous +extension type RustLibWasmModule._(JSObject _) implements JSObject {} diff --git a/lib/src/rust/lightwire.dart b/lib/src/rust/lightwire.dart new file mode 100644 index 00000000..f7857d8d --- /dev/null +++ b/lib/src/rust/lightwire.dart @@ -0,0 +1,10 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// Rust type: RustOpaqueMoi> +abstract class Engine implements RustOpaqueInterface {} diff --git a/lib/standards/server_stored_information.dart b/lib/standards/server_stored_information.dart index 24522774..dd858e93 100644 --- a/lib/standards/server_stored_information.dart +++ b/lib/standards/server_stored_information.dart @@ -1,9 +1,9 @@ import 'dart:typed_data'; -import 'package:chat_interface/connection/encryption/asymmetric_sodium.dart'; -import 'package:chat_interface/connection/encryption/hash.dart'; -import 'package:chat_interface/connection/encryption/signatures.dart'; -import 'package:chat_interface/connection/encryption/symmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/asymmetric_sodium.dart'; +import 'package:chat_interface/util/encryption/hash.dart'; +import 'package:chat_interface/util/encryption/signatures.dart'; +import 'package:chat_interface/util/encryption/symmetric_sodium.dart'; import 'package:chat_interface/controller/current/steps/key_step.dart'; import 'package:sodium_libs/sodium_libs.dart'; @@ -15,14 +15,23 @@ class ServerStoredInfo { /// Decrypt stored stored info with own public and private key factory ServerStoredInfo.untransform(String transformed, {Sodium? sodium, KeyPair? ownKeyPair}) { - final result = - decryptAsymmetricAuth((ownKeyPair ?? asymmetricKeyPair).publicKey, (ownKeyPair ?? asymmetricKeyPair).secretKey, transformed, sodium); + final result = decryptAsymmetricAuth( + (ownKeyPair ?? asymmetricKeyPair).publicKey, + (ownKeyPair ?? asymmetricKeyPair).secretKey, + transformed, + sodium, + ); return ServerStoredInfo(result.message, error: !result.success); } /// Get the server stored info in encrypted form with the own public and private key String transform({Sodium? sodium, KeyPair? ownKeyPair}) { - return encryptAsymmetricAuth((ownKeyPair ?? asymmetricKeyPair).publicKey, (ownKeyPair ?? asymmetricKeyPair).secretKey, text, sodium); + return encryptAsymmetricAuth( + (ownKeyPair ?? asymmetricKeyPair).publicKey, + (ownKeyPair ?? asymmetricKeyPair).secretKey, + text, + sodium, + ); } } diff --git a/lib/theme/components/duration_renderer.dart b/lib/theme/components/duration_renderer.dart index c5d18e4b..2c004537 100644 --- a/lib/theme/components/duration_renderer.dart +++ b/lib/theme/components/duration_renderer.dart @@ -1,35 +1,37 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; -import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class DurationRenderer extends StatelessWidget { +class DurationRenderer extends StatefulWidget { final DateTime start; - final current = const Duration(seconds: 0).obs; final TextStyle? style; - DurationRenderer(this.start, {this.style, super.key}); + const DurationRenderer(this.start, {this.style, super.key}); + + @override + State createState() => _DurationRendererState(); +} + +class _DurationRendererState extends State with SignalsMixin { + late final _current = createSignal(const Duration(seconds: 0)); @override Widget build(BuildContext context) { - current.value = DateTime.now().difference(start); - Timer.periodic(const Duration(seconds: 1), - (timer) => current.value = DateTime.now().difference(start)); - - return RepaintBoundary(child: Obx(() { - final duration = current.value; - final hours = duration.inHours; - final minutes = duration.inMinutes - (hours * 60); - final seconds = duration.inSeconds - (minutes * 60) - (hours * 60 * 60); - - if (hours > 0) { - return Text( - "${hours.toString().padLeft(2, "0")}:${minutes.toString().padLeft(2, "0")}:${seconds.toString().padLeft(2, "0")}", - style: style); - } + _current.value = DateTime.now().difference(widget.start); + Timer.periodic(const Duration(seconds: 1), (timer) => _current.value = DateTime.now().difference(widget.start)); + + final duration = _current.value; + final hours = duration.inHours; + final minutes = duration.inMinutes - (hours * 60); + final seconds = duration.inSeconds - (minutes * 60) - (hours * 60 * 60); + + if (hours > 0) { return Text( - "${minutes.toString().padLeft(2, "0")}:${seconds.toString().padLeft(2, "0")}", - style: style); - })); + "${hours.toString().padLeft(2, "0")}:${minutes.toString().padLeft(2, "0")}:${seconds.toString().padLeft(2, "0")}", + style: widget.style, + ); + } + return Text("${minutes.toString().padLeft(2, "0")}:${seconds.toString().padLeft(2, "0")}", style: widget.style); } } diff --git a/lib/theme/components/file_renderer.dart b/lib/theme/components/file_renderer.dart index e631ed61..4e32e718 100644 --- a/lib/theme/components/file_renderer.dart +++ b/lib/theme/components/file_renderer.dart @@ -6,14 +6,9 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'package:liphium_bridge/liphium_bridge.dart'; import 'package:path/path.dart' as path; +import 'package:signals/signals_flutter.dart'; -enum FileTypes { - image, - video, - audio, - document, - unidentified, -} +enum FileTypes { image, video, audio, document, unidentified } const extensionToType = { "png": FileTypes.image, @@ -64,35 +59,17 @@ class FilePreview extends StatelessWidget { switch (type) { case FileTypes.image: - return XImage( - file: file, - fit: BoxFit.cover, - ); + return XImage(file: file, fit: BoxFit.cover); case FileTypes.audio: - return const Center( - child: Icon( - size: 50, - Icons.library_music, - ), - ); + return const Center(child: Icon(size: 50, Icons.library_music)); case FileTypes.document: - return const Center( - child: Icon( - size: 50, - Icons.text_snippet, - ), - ); + return const Center(child: Icon(size: 50, Icons.text_snippet)); case FileTypes.video: case FileTypes.unidentified: - return const Center( - child: Icon( - size: 50, - Icons.insert_drive_file, - ), - ); + return const Center(child: Icon(size: 50, Icons.insert_drive_file)); } } } @@ -116,14 +93,17 @@ class _SquareFileRendererState extends State { child: Container( width: 200, height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(defaultSpacing), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(defaultSpacing)), child: ClipRRect( borderRadius: BorderRadius.circular(defaultSpacing), child: Stack( children: [ - Container(color: Get.theme.colorScheme.primaryContainer, width: 200, height: 200, child: FilePreview(file: widget.file.file)), + Container( + color: Get.theme.colorScheme.primaryContainer, + width: 200, + height: 200, + child: FilePreview(file: widget.file.file), + ), Align( alignment: Alignment.topRight, child: Padding( @@ -137,15 +117,10 @@ class _SquareFileRendererState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ horizontalSpacing(defaultSpacing), - Expanded( - child: Text( - path.basename(widget.file.file.path), - overflow: TextOverflow.ellipsis, - ), - ), + Expanded(child: Text(path.basename(widget.file.file.path), overflow: TextOverflow.ellipsis)), horizontalSpacing(defaultSpacing), - Obx( - () => Visibility( + Watch( + (ctx) => Visibility( visible: widget.file.progress.value == 0, replacement: Padding( padding: const EdgeInsets.all(defaultSpacing), diff --git a/lib/theme/components/forms/fj_button.dart b/lib/theme/components/forms/fj_button.dart index b73655cd..089a589e 100644 --- a/lib/theme/components/forms/fj_button.dart +++ b/lib/theme/components/forms/fj_button.dart @@ -1,6 +1,7 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class FJElevatedButton extends StatelessWidget { final Function() onTap; @@ -30,10 +31,7 @@ class FJElevatedButton extends StatelessWidget { onTap: onTap, borderRadius: BorderRadius.circular(defaultSpacing * (smallCorners ? 1.0 : 1.5)), splashColor: Get.theme.hoverColor.withAlpha(20), - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: child, - ), + child: Padding(padding: const EdgeInsets.all(defaultSpacing), child: child), ), ); } @@ -42,26 +40,37 @@ class FJElevatedButton extends StatelessWidget { class FJElevatedLoadingButton extends StatelessWidget { final Function() onTap; final String label; - final RxBool loading; + final ReadonlySignal? loading; - const FJElevatedLoadingButton({super.key, required this.onTap, required this.label, required this.loading}); + const FJElevatedLoadingButton({super.key, required this.onTap, required this.label, this.loading}); @override Widget build(BuildContext context) { return FJElevatedButton( - onTap: () => loading.value ? null : onTap(), + onTap: () => (loading?.value ?? false) ? null : onTap(), child: Center( - child: Obx( - () => loading.value - ? SizedBox( - height: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, - width: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, - child: Padding( - padding: const EdgeInsets.all(defaultSpacing * 0.25), - child: CircularProgressIndicator(strokeWidth: 3.0, color: Get.theme.colorScheme.onPrimary), - ), - ) - : Text(label, style: Get.theme.textTheme.labelLarge), + child: Builder( + builder: (context) { + // Don't care about state in case there is no loading state + if (loading == null) { + return Text(label, style: Get.theme.textTheme.labelLarge); + } + + // Handle loading states as well + return Watch( + (ctx) => + loading!.value + ? SizedBox( + height: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, + width: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, + child: Padding( + padding: const EdgeInsets.all(defaultSpacing * 0.25), + child: CircularProgressIndicator(strokeWidth: 3.0, color: Get.theme.colorScheme.onPrimary), + ), + ) + : Text(label, style: Get.theme.textTheme.labelLarge), + ); + }, ), ), ); @@ -72,25 +81,34 @@ class FJElevatedLoadingButtonCustom extends StatelessWidget { final Function() onTap; final Widget Function()? builder; final Widget child; - final RxBool loading; + final Signal loading; - const FJElevatedLoadingButtonCustom({super.key, required this.onTap, required this.child, required this.loading, this.builder}); + const FJElevatedLoadingButtonCustom({ + super.key, + required this.onTap, + required this.child, + required this.loading, + this.builder, + }); @override Widget build(BuildContext context) { return FJElevatedButton( onTap: () => loading.value ? null : onTap(), - child: Obx(() => loading.value - ? builder?.call() ?? - SizedBox( - height: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, - width: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, - child: Padding( - padding: const EdgeInsets.all(defaultSpacing * 0.25), - child: CircularProgressIndicator(strokeWidth: 3.0, color: Get.theme.colorScheme.onPrimary), - ), - ) - : child), + child: Watch( + (ctx) => + loading.value + ? builder?.call() ?? + SizedBox( + height: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, + width: Get.theme.textTheme.labelLarge!.fontSize! + defaultSpacing, + child: Padding( + padding: const EdgeInsets.all(defaultSpacing * 0.25), + child: CircularProgressIndicator(strokeWidth: 3.0, color: Get.theme.colorScheme.onPrimary), + ), + ) + : child, + ), ); } } diff --git a/lib/theme/components/forms/fj_option_button.dart b/lib/theme/components/forms/fj_option_button.dart index 996f0654..ce2f1c08 100644 --- a/lib/theme/components/forms/fj_option_button.dart +++ b/lib/theme/components/forms/fj_option_button.dart @@ -23,10 +23,14 @@ class _FJTextFieldState extends State { borderRadius: BorderRadius.circular(defaultSpacing), onTap: widget.onTap, child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Row( - children: [Expanded(child: Text(widget.text, style: theme.textTheme.labelLarge)), const Icon(Icons.arrow_forward)], - )), + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + Expanded(child: Text(widget.text, style: theme.textTheme.labelLarge)), + const Icon(Icons.arrow_forward), + ], + ), + ), ), ); } diff --git a/lib/theme/components/forms/fj_slider.dart b/lib/theme/components/forms/fj_slider.dart index 7a07f464..b97c4adf 100644 --- a/lib/theme/components/forms/fj_slider.dart +++ b/lib/theme/components/forms/fj_slider.dart @@ -51,10 +51,7 @@ class FJSlider extends StatelessWidget { ), ), label != null - ? Padding( - padding: const EdgeInsets.only(left: defaultSpacing), - child: Text(label!), - ) + ? Padding(padding: const EdgeInsets.only(left: defaultSpacing), child: Text(label!)) : const SizedBox(), ], ), @@ -96,7 +93,9 @@ class _FJSliderWithInputState extends State { @override void initState() { super.initState(); - _controller.value = TextEditingValue(text: (widget.transformer?.call(widget.value) ?? widget.value).toStringAsFixed(0)); + _controller.value = TextEditingValue( + text: (widget.transformer?.call(widget.value) ?? widget.value).toStringAsFixed(0), + ); } @override @@ -127,7 +126,9 @@ class _FJSliderWithInputState extends State { max: widget.max, onChanged: (value) { widget.onChanged!(value); - _controller.value = TextEditingValue(text: (widget.transformer?.call(value) ?? value).toStringAsFixed(0)); + _controller.value = TextEditingValue( + text: (widget.transformer?.call(value) ?? value).toStringAsFixed(0), + ); }, onChangeEnd: widget.onChangeEnd, ), @@ -163,7 +164,7 @@ class _FJSliderWithInputState extends State { widget.onChangeEnd!(finalValue); }, ), - ) + ), ], ), ); @@ -206,17 +207,20 @@ class CustomSliderThumbShape extends RoundSliderThumbShape { required double textScaleFactor, required Size sizeWithOverflow, }) { - super.paint(context, center.translate(-(value - 0.5) / 0.5 * enabledThumbRadius, 0.0), - activationAnimation: activationAnimation, - enableAnimation: enableAnimation, - isDiscrete: isDiscrete, - labelPainter: labelPainter, - parentBox: parentBox, - sliderTheme: sliderTheme, - textDirection: textDirection, - value: value, - textScaleFactor: textScaleFactor, - sizeWithOverflow: sizeWithOverflow); + super.paint( + context, + center.translate(-(value - 0.5) / 0.5 * enabledThumbRadius, 0.0), + activationAnimation: activationAnimation, + enableAnimation: enableAnimation, + isDiscrete: isDiscrete, + labelPainter: labelPainter, + parentBox: parentBox, + sliderTheme: sliderTheme, + textDirection: textDirection, + value: value, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, + ); } } @@ -239,16 +243,19 @@ class CustomSliderOverlayShape extends RoundSliderOverlayShape { required double textScaleFactor, required Size sizeWithOverflow, }) { - super.paint(context, center.translate(-(value - 0.5) / 0.5 * thumbRadius, 0.0), - activationAnimation: activationAnimation, - enableAnimation: enableAnimation, - isDiscrete: isDiscrete, - labelPainter: labelPainter, - parentBox: parentBox, - sliderTheme: sliderTheme, - textDirection: textDirection, - value: value, - textScaleFactor: textScaleFactor, - sizeWithOverflow: sizeWithOverflow); + super.paint( + context, + center.translate(-(value - 0.5) / 0.5 * thumbRadius, 0.0), + activationAnimation: activationAnimation, + enableAnimation: enableAnimation, + isDiscrete: isDiscrete, + labelPainter: labelPainter, + parentBox: parentBox, + sliderTheme: sliderTheme, + textDirection: textDirection, + value: value, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, + ); } } diff --git a/lib/theme/components/forms/fj_switch.dart b/lib/theme/components/forms/fj_switch.dart index 810ab477..5a8f06b1 100644 --- a/lib/theme/components/forms/fj_switch.dart +++ b/lib/theme/components/forms/fj_switch.dart @@ -15,10 +15,16 @@ class FJSwitch extends StatelessWidget { width: 54, child: Switch( trackColor: WidgetStateColor.resolveWith( - (states) => states.contains(WidgetState.selected) ? Get.theme.colorScheme.primary : Get.theme.colorScheme.primaryContainer), + (states) => + states.contains(WidgetState.selected) + ? Get.theme.colorScheme.primary + : Get.theme.colorScheme.primaryContainer, + ), hoverColor: Get.theme.hoverColor, thumbColor: WidgetStateColor.resolveWith( - (states) => states.contains(WidgetState.selected) ? Get.theme.colorScheme.onPrimary : Get.theme.colorScheme.surface), + (states) => + states.contains(WidgetState.selected) ? Get.theme.colorScheme.onPrimary : Get.theme.colorScheme.surface, + ), value: value, onChanged: onChanged, ), diff --git a/lib/theme/components/forms/fj_textfield.dart b/lib/theme/components/forms/fj_textfield.dart index 108239a7..1560f55e 100644 --- a/lib/theme/components/forms/fj_textfield.dart +++ b/lib/theme/components/forms/fj_textfield.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class FJTextField extends StatefulWidget { final bool obscureText; @@ -49,9 +50,9 @@ class FJTextField extends StatefulWidget { State createState() => _FJTextFieldState(); } -class _FJTextFieldState extends State { +class _FJTextFieldState extends State with SignalsMixin { late FocusNode _node; - final _focus = false.obs; + late final _focus = createSignal(false); @override void initState() { @@ -69,70 +70,68 @@ class _FJTextFieldState extends State { _focus.value = (widget.focusNode ?? _node).hasFocus; }); - return Obx( - () => Animate( - effects: [ - ScaleEffect(end: const Offset(1.08, 1.08), duration: 250.ms, curve: Curves.ease), - CustomEffect( - begin: 0, - end: 1, - duration: 250.ms, - builder: (context, value, child) { - return Padding(padding: EdgeInsets.symmetric(horizontal: defaultSpacing * value), child: child); - }, - ) - ], - target: _focus.value && widget.animation ? 1 : 0, - child: Material( - color: widget.secondaryColor ? Get.theme.colorScheme.onInverseSurface : Get.theme.colorScheme.inverseSurface, - borderRadius: BorderRadius.circular(defaultSpacing), - child: Padding( - padding: const EdgeInsets.all(defaultSpacing * 1.5), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.prefixIcon != null) - Padding( - padding: const EdgeInsets.only(right: defaultSpacing, left: 0), - child: Icon( - widget.prefixIcon, - color: Get.theme.colorScheme.onPrimary, - size: (widget.small ? theme.textTheme.labelMedium : theme.textTheme.labelLarge)!.fontSize! * 1.5, - ), + return Animate( + effects: [ + ScaleEffect(end: const Offset(1.08, 1.08), duration: 250.ms, curve: Curves.ease), + CustomEffect( + begin: 0, + end: 1, + duration: 250.ms, + builder: (context, value, child) { + return Padding(padding: EdgeInsets.symmetric(horizontal: defaultSpacing * value), child: child); + }, + ), + ], + target: _focus.value && widget.animation ? 1 : 0, + child: Material( + color: widget.secondaryColor ? Get.theme.colorScheme.onInverseSurface : Get.theme.colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(defaultSpacing), + child: Padding( + padding: const EdgeInsets.all(defaultSpacing * 1.5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.prefixIcon != null) + Padding( + padding: const EdgeInsets.only(right: defaultSpacing, left: 0), + child: Icon( + widget.prefixIcon, + color: Get.theme.colorScheme.onPrimary, + size: (widget.small ? theme.textTheme.labelMedium : theme.textTheme.labelLarge)!.fontSize! * 1.5, ), - Flexible( - child: TextField( - decoration: InputDecoration( - isDense: true, - hintText: widget.hintText, - labelStyle: widget.small ? theme.textTheme.labelMedium : theme.textTheme.labelLarge, - hintStyle: widget.small ? theme.textTheme.bodyMedium : theme.textTheme.bodyLarge, - errorText: widget.errorText, - border: InputBorder.none, - counterText: "", - ), - style: widget.small ? theme.textTheme.labelMedium : theme.textTheme.labelLarge, - obscureText: widget.obscureText, - autofocus: widget.autofocus, - autocorrect: widget.autocorrect, - maxLines: widget.maxLines, - enableSuggestions: false, - controller: widget.controller, - maxLength: widget.maxLength, - maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, - onTap: () => _focus.value = true, - onTapOutside: (event) { - widget.onTapOutside?.call(event); - (widget.focusNode ?? _node).unfocus(); - }, - focusNode: (widget.focusNode ?? _node), - onChanged: widget.onChange, - inputFormatters: widget.inputFormatters, - onSubmitted: widget.onSubmitted, + ), + Flexible( + child: TextField( + decoration: InputDecoration( + isDense: true, + hintText: widget.hintText, + labelStyle: widget.small ? theme.textTheme.labelMedium : theme.textTheme.labelLarge, + hintStyle: widget.small ? theme.textTheme.bodyMedium : theme.textTheme.bodyLarge, + errorText: widget.errorText, + border: InputBorder.none, + counterText: "", ), + style: widget.small ? theme.textTheme.labelMedium : theme.textTheme.labelLarge, + obscureText: widget.obscureText, + autofocus: widget.autofocus, + autocorrect: widget.autocorrect, + maxLines: widget.maxLines, + enableSuggestions: false, + controller: widget.controller, + maxLength: widget.maxLength, + maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, + onTap: () => _focus.value = true, + onTapOutside: (event) { + widget.onTapOutside?.call(event); + (widget.focusNode ?? _node).unfocus(); + }, + focusNode: (widget.focusNode ?? _node), + onChanged: widget.onChange, + inputFormatters: widget.inputFormatters, + onSubmitted: widget.onSubmitted, ), - ], - ), + ), + ], ), ), ), diff --git a/lib/theme/components/forms/icon_button.dart b/lib/theme/components/forms/icon_button.dart index ad87a8cd..f021bac1 100644 --- a/lib/theme/components/forms/icon_button.dart +++ b/lib/theme/components/forms/icon_button.dart @@ -1,9 +1,10 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class LoadingIconButton extends StatelessWidget { - final RxBool? loading; + final Signal? loading; final IconData icon; final Color? color; final String? tooltip; @@ -13,12 +14,14 @@ class LoadingIconButton extends StatelessWidget { final bool background; final Color? backgroundColor; final Function() onTap; + final Function()? onSecondaryTap; final Function(BuildContext)? onTapContext; const LoadingIconButton({ super.key, this.loading, required this.onTap, + this.onSecondaryTap, this.tooltip, this.onTapContext, required this.icon, @@ -54,23 +57,33 @@ class LoadingIconButton extends StatelessWidget { onTapContext!(context); } }, + onSecondaryTap: () { + if (loading != null) { + if (loading!.value) { + return; + } + } + + onSecondaryTap?.call(); + }, hoverColor: Get.theme.hoverColor, child: Padding( padding: EdgeInsets.all(padding), - child: loading != null - ? Obx( - () => loading!.value - ? Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: CircularProgressIndicator(strokeWidth: 3.0, color: Get.theme.colorScheme.onPrimary), - ) - : Icon(icon, color: color ?? Colors.white, size: iconSize), - ) - : Icon( - icon, - color: color ?? Colors.white, - size: iconSize, - ), + child: + loading != null + ? Watch( + (ctx) => + loading!.value + ? Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: CircularProgressIndicator( + strokeWidth: 3.0, + color: Get.theme.colorScheme.onPrimary, + ), + ) + : Icon(icon, color: color ?? Colors.white, size: iconSize), + ) + : Icon(icon, color: color ?? Colors.white, size: iconSize), ), ), ), diff --git a/lib/theme/components/forms/lph_action_fields.dart b/lib/theme/components/forms/lph_action_fields.dart index b3e3a7e3..59b23d3a 100644 --- a/lib/theme/components/forms/lph_action_fields.dart +++ b/lib/theme/components/forms/lph_action_fields.dart @@ -7,11 +7,7 @@ class LPHCopyField extends StatelessWidget { final String label; final String value; - const LPHCopyField({ - super.key, - required this.label, - required this.value, - }); + const LPHCopyField({super.key, required this.label, required this.value}); @override Widget build(BuildContext context) { @@ -28,20 +24,12 @@ class LPHCopyField extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - overflow: TextOverflow.ellipsis, - style: Get.textTheme.labelSmall, - ), + Text(label, overflow: TextOverflow.ellipsis, style: Get.textTheme.labelSmall), Tooltip( waitDuration: const Duration(milliseconds: 500), exitDuration: const Duration(microseconds: 0), message: "$label: $value", - child: Text( - value, - overflow: TextOverflow.ellipsis, - style: Get.textTheme.bodyLarge, - ), + child: Text(value, overflow: TextOverflow.ellipsis, style: Get.textTheme.bodyLarge), ), ], ), @@ -70,12 +58,7 @@ class LPHActionField extends StatelessWidget { final String secondary; final List actions; - const LPHActionField({ - super.key, - required this.primary, - required this.secondary, - required this.actions, - }); + const LPHActionField({super.key, required this.primary, required this.secondary, required this.actions}); @override Widget build(BuildContext context) { @@ -92,20 +75,12 @@ class LPHActionField extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - primary, - overflow: TextOverflow.ellipsis, - style: Get.textTheme.labelSmall, - ), + Text(primary, overflow: TextOverflow.ellipsis, style: Get.textTheme.labelSmall), Tooltip( waitDuration: const Duration(milliseconds: 500), exitDuration: const Duration(microseconds: 0), message: "$primary: $secondary", - child: Text( - secondary, - overflow: TextOverflow.ellipsis, - style: Get.textTheme.bodyLarge, - ), + child: Text(secondary, overflow: TextOverflow.ellipsis, style: Get.textTheme.bodyLarge), ), ], ), diff --git a/lib/theme/components/legacy/sidebar_button.dart b/lib/theme/components/legacy/sidebar_button.dart deleted file mode 100644 index f1f6e059..00000000 --- a/lib/theme/components/legacy/sidebar_button.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:get/get.dart'; - -class SidebarButton extends StatefulWidget { - final Function() onTap; - final String label; - final RxString selected; - final BorderRadius radius; - final Color? background; - - const SidebarButton({ - super.key, - required this.onTap, - required this.label, - this.radius = const BorderRadius.all(Radius.circular(defaultSpacing)), - this.background, - required this.selected, - }); - - @override - State createState() => _SidebarButtonState(); -} - -class _SidebarButtonState extends State with TickerProviderStateMixin { - late final AnimationController _controller; - - @override - void initState() { - _controller = AnimationController( - vsync: this, - value: 0, - ); - _controller.stop(); - super.initState(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - widget.selected.listen((value) { - if (value == widget.label) { - _controller.loop(count: 1, reverse: true); - } - }); - - return Animate( - controller: _controller, - effects: [ - ScaleEffect( - begin: const Offset(0.95, 0.95), - end: const Offset(1.0, 1.0), - curve: Curves.easeIn, - duration: 100.ms, - ), - ], - child: Obx( - () => Material( - borderRadius: widget.radius, - color: widget.selected.value == widget.label ? Get.theme.colorScheme.primary : widget.background ?? Get.theme.colorScheme.primaryContainer, - child: InkWell( - borderRadius: widget.radius, - onTap: () { - widget.onTap(); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: defaultSpacing * 1.5, vertical: defaultSpacing * 0.5), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - ), - child: Text( - widget.label.tr, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/theme/components/legacy/sidebar_icon_button.dart b/lib/theme/components/legacy/sidebar_icon_button.dart index 54e2d9fa..bf2db526 100644 --- a/lib/theme/components/legacy/sidebar_icon_button.dart +++ b/lib/theme/components/legacy/sidebar_icon_button.dart @@ -2,21 +2,23 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SidebarIconButton extends StatefulWidget { final Function() onTap; final IconData icon; final int index; - final RxInt selected; + final ReadonlySignal selected; final BorderRadius radius; - const SidebarIconButton( - {super.key, - required this.onTap, - required this.icon, - this.radius = const BorderRadius.all(Radius.circular(defaultSpacing)), - required this.index, - required this.selected}); + const SidebarIconButton({ + super.key, + required this.onTap, + required this.icon, + this.radius = const BorderRadius.all(Radius.circular(defaultSpacing)), + required this.index, + required this.selected, + }); @override State createState() => _SidebarButtonState(); @@ -25,11 +27,15 @@ class SidebarIconButton extends StatefulWidget { class _SidebarButtonState extends State with TickerProviderStateMixin { late AnimationController _controller; + late final selectedDispose = widget.selected.subscribe((value) { + if (widget.selected.value == widget.index) { + _controller.loop(count: 1, reverse: true); + } + }); + @override void initState() { - _controller = AnimationController( - vsync: this, - ); + _controller = AnimationController(vsync: this); super.initState(); } @@ -42,27 +48,32 @@ class _SidebarButtonState extends State with TickerProviderSt @override void dispose() { _controller.dispose(); + selectedDispose(); super.dispose(); } @override Widget build(BuildContext context) { - widget.selected.listen((value) { - if (widget.selected.value == widget.index) { - _controller.loop(count: 1, reverse: true); - } - }); - return Animate( controller: _controller, - effects: [ScaleEffect(begin: const Offset(0.8, 0.8), end: const Offset(1.0, 1.0), curve: Curves.easeOut, duration: 120.ms)], - child: Obx( - () => Material( + effects: [ + ScaleEffect( + begin: const Offset(0.8, 0.8), + end: const Offset(1.0, 1.0), + curve: Curves.easeOut, + duration: 120.ms, + ), + ], + child: Watch( + (ctx) => Material( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(defaultSpacing), topRight: Radius.circular(defaultSpacing), ), - color: widget.selected.value == widget.index ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.inverseSurface, + color: + widget.selected.value == widget.index + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.inverseSurface, child: InkWell( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(defaultSpacing), @@ -75,7 +86,10 @@ class _SidebarButtonState extends State with TickerProviderSt padding: const EdgeInsets.symmetric(vertical: elementSpacing, horizontal: sectionSpacing), child: Icon( widget.icon, - color: widget.selected.value == widget.index ? Get.theme.colorScheme.onPrimary : Get.theme.colorScheme.onSurface, + color: + widget.selected.value == widget.index + ? Get.theme.colorScheme.onPrimary + : Get.theme.colorScheme.onSurface, size: fittedIconSize(24), ), ), diff --git a/lib/theme/components/lph_page_switcher.dart b/lib/theme/components/lph_page_switcher.dart new file mode 100644 index 00000000..e2284ffe --- /dev/null +++ b/lib/theme/components/lph_page_switcher.dart @@ -0,0 +1,80 @@ +import 'package:chat_interface/theme/components/forms/icon_button.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; + +class LPHPageSwitcher extends StatelessWidget { + final Signal currentPage; + final Signal count; + final Signal loading; + final Function(int) page; + + const LPHPageSwitcher({ + super.key, + required this.currentPage, + required this.count, + required this.loading, + required this.page, + }); + + int getMaxPage() => (count.value / 20).ceil(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + LoadingIconButton( + loading: loading, + onTap: () { + if (currentPage.value == 0) { + return; + } + page(0); + }, + icon: Icons.skip_previous, + ), + horizontalSpacing(elementSpacing), + LoadingIconButton( + loading: loading, + onTap: () { + if (currentPage.value == 0) { + return; + } + page(currentPage.value - 1); + }, + icon: Icons.arrow_back, + ), + const Spacer(), + Watch( + (ctx) => Text( + "page_switcher".trParams({"count": (currentPage.value + 1).toString(), "max": getMaxPage().toString()}), + style: Get.textTheme.labelLarge, + ), + ), + const Spacer(), + LoadingIconButton( + loading: loading, + onTap: () { + if (currentPage.value == getMaxPage() - 1) { + return; + } + page(currentPage.value + 1); + }, + icon: Icons.arrow_forward, + ), + horizontalSpacing(elementSpacing), + LoadingIconButton( + loading: loading, + onTap: () { + if (currentPage.value == getMaxPage() - 1) { + return; + } + page(getMaxPage() - 1); + }, + icon: Icons.skip_next, + ), + ], + ); + } +} diff --git a/lib/theme/components/lph_tab_element.dart b/lib/theme/components/lph_tab_element.dart index 49ec5270..1708d345 100644 --- a/lib/theme/components/lph_tab_element.dart +++ b/lib/theme/components/lph_tab_element.dart @@ -1,78 +1,89 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class LPHTabElement extends StatefulWidget { - final RxInt? selected; + final FlutterSignal? selected; final List tabs; final Function(String) onTabSwitch; - const LPHTabElement({ - super.key, - required this.tabs, - required this.onTabSwitch, - this.selected, - }); + const LPHTabElement({super.key, required this.tabs, required this.onTabSwitch, this.selected}); @override State createState() => _LPHTabElementState(); } class _LPHTabElementState extends State { - RxInt _selected = 0.obs; + late Signal _selected; + bool _createdHere = false; // The width of all the text in the tabs - final tabWidth = {}; + final _tabWidth = {}; @override void initState() { + // Initialize the signal responsible for the state + if (widget.selected == null) { + _createdHere = true; + } + _selected = widget.selected ?? signal(0); + // Measure all the texts - _selected = widget.selected ?? 0.obs; int count = 0; for (var tab in widget.tabs) { final textPainter = TextPainter( - text: TextSpan( - text: tab, - style: Get.textTheme.titleMedium, - ), + text: TextSpan(text: tab, style: Get.textTheme.titleMedium), textDirection: TextDirection.ltr, ); textPainter.layout(); - tabWidth[count] = textPainter.size.width + defaultSpacing * 2; + _tabWidth[count] = textPainter.size.width + defaultSpacing * 2; count++; } super.initState(); } + @override + void dispose() { + if (_createdHere) { + _selected.dispose(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { return Stack( children: [ - Obx(() { - double left = 0; - int count = 0; - for (var _ in widget.tabs) { - if (count == _selected.value) { - break; + Watch.builder( + builder: (context) { + // Calculate where the background should be placed + double left = 0; + int count = 0; + for (var _ in widget.tabs) { + if (count == _selected.value) { + break; + } + left += _tabWidth[count]! + defaultSpacing; + count++; } - left += tabWidth[count]! + defaultSpacing; - count++; - } - return AnimatedPositioned( - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOutCubicEmphasized, - left: left, - width: tabWidth[_selected.value], - height: Get.textTheme.titleMedium!.fontSize! * 1.5 + elementSpacing * 2, - child: Container( - decoration: BoxDecoration( - color: Get.theme.colorScheme.primary, - borderRadius: BorderRadius.circular(defaultSpacing), + // Render the background with animation + return AnimatedPositioned( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOutCubicEmphasized, + left: left, + width: _tabWidth[_selected.value], + height: Get.textTheme.titleMedium!.fontSize! * 1.5 + elementSpacing * 2, + child: Container( + decoration: BoxDecoration( + color: Get.theme.colorScheme.primary, + borderRadius: BorderRadius.circular(defaultSpacing), + ), ), - ), - ); - }), + ); + }, + ), Row( mainAxisSize: MainAxisSize.min, children: List.generate(widget.tabs.length, (index) { @@ -89,12 +100,9 @@ class _LPHTabElementState extends State { }, borderRadius: BorderRadius.circular(defaultSpacing), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: defaultSpacing, - vertical: elementSpacing, - ), - child: Obx( - () => Text( + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing, vertical: elementSpacing), + child: Watch( + (context) => Text( widget.tabs[index], style: Get.textTheme.titleMedium!.copyWith( color: _selected.value == index ? Colors.white : Colors.white, diff --git a/lib/theme/components/ssr/ssr.dart b/lib/theme/components/ssr/ssr.dart index f4a26449..0fcde486 100644 --- a/lib/theme/components/ssr/ssr.dart +++ b/lib/theme/components/ssr/ssr.dart @@ -4,6 +4,7 @@ import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SSR { /// The path the SSR rendering starts with calling @@ -24,7 +25,7 @@ class SSR { // All data required for the UI final currentInputValues = {}; String? currentToken; - final error = "".obs; + final error = signal(""); Map? suggestButton; SSR({required this.startPath, required this.onSuccess, required this.onRender, this.doRequest = postJSON}); @@ -45,9 +46,7 @@ class SSR { if (currentToken != null) { // Build the request body to send to the server - final baseTokenMap = { - "token": currentToken, - }; + final baseTokenMap = {"token": currentToken}; baseTokenMap.addAll(currentInputValues); // Send a request to the server @@ -100,11 +99,6 @@ class SSR { /// Returns a SSR renderer to render the components in a render response void _renderWidgets(String path, List json) { - onRender.call(SSRRenderer( - key: ValueKey(path), - ssr: this, - json: json, - path: path, - )); + onRender.call(SSRRenderer(key: ValueKey(path), ssr: this, json: json, path: path)); } } diff --git a/lib/theme/components/ssr/ssr_fetcher.dart b/lib/theme/components/ssr/ssr_fetcher.dart index 0d91aeaf..4dcc7ffd 100644 --- a/lib/theme/components/ssr/ssr_fetcher.dart +++ b/lib/theme/components/ssr/ssr_fetcher.dart @@ -4,6 +4,7 @@ import 'package:chat_interface/theme/components/ssr/ssr.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class SSRFetcher extends StatefulWidget { final String label; @@ -11,56 +12,50 @@ class SSRFetcher extends StatefulWidget { final String path; final int frequency; - const SSRFetcher({ - super.key, - required this.label, - required this.ssr, - required this.path, - required this.frequency, - }); + const SSRFetcher({super.key, required this.label, required this.ssr, required this.path, required this.frequency}); @override State createState() => _SSRFetcherState(); } class _SSRFetcherState extends State { - final error = true.obs; - final success = false.obs; - final loading = false.obs; + final _error = signal(true); + final _success = signal(false); + final _loading = signal(false); - Timer? timer; + Timer? _timer; @override void initState() { // Timer for periodically checking the endpoint provided by the server - timer = Timer.periodic( - Duration(seconds: widget.frequency), - (timer) async { - if (loading.value) { - return; - } - loading.value = true; + _timer = Timer.periodic(Duration(seconds: widget.frequency), (timer) async { + if (_loading.value) { + return; + } + _loading.value = true; - // Do a request to the server using the SSR request function - final json = await widget.ssr.doRequest.call(widget.path, { - if (widget.ssr.currentToken != null) "token": widget.ssr.currentToken, - }); - await Future.delayed(const Duration(milliseconds: 250)); // To show the user that it's actually doing something - loading.value = false; - error.value = !json["success"]; - await Future.delayed(const Duration(milliseconds: 500)); // To show the user what's going on - if (json["success"]) { - unawaited(widget.ssr.handleSSRResponse(widget.path, json)); - timer.cancel(); - } - }, - ); + // Do a request to the server using the SSR request function + final json = await widget.ssr.doRequest.call(widget.path, { + if (widget.ssr.currentToken != null) "token": widget.ssr.currentToken, + }); + await Future.delayed(const Duration(milliseconds: 250)); // To show the user that it's actually doing something + _loading.value = false; + _error.value = !json["success"]; + await Future.delayed(const Duration(milliseconds: 500)); // To show the user what's going on + if (json["success"]) { + unawaited(widget.ssr.handleSSRResponse(widget.path, json)); + timer.cancel(); + } + }); super.initState(); } @override void dispose() { - timer?.cancel(); + _error.dispose(); + _success.dispose(); + _loading.dispose(); + _timer?.cancel(); super.dispose(); } @@ -79,38 +74,29 @@ class _SSRFetcherState extends State { // The label of the actual status fetcher Expanded( - child: Text( - widget.label, - style: Get.theme.textTheme.labelMedium, - overflow: TextOverflow.ellipsis, - ), + child: Text(widget.label, style: Get.theme.textTheme.labelMedium, overflow: TextOverflow.ellipsis), ), horizontalSpacing(defaultSpacing), // The icon showing the progress on the fetcher - Obx( - () { - // If it's loading return a loading indicator - if (loading.value) { - return SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 3, - color: Get.theme.colorScheme.onPrimary, - ), - ); - } + Watch((ctx) { + // If it's loading return a loading indicator + if (_loading.value) { + return SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 3, color: Get.theme.colorScheme.onPrimary), + ); + } - // If there was an error, show the error icon until the next request - if (error.value) { - return Icon(Icons.error, color: Get.theme.colorScheme.error); - } + // If there was an error, show the error icon until the next request + if (_error.value) { + return Icon(Icons.error, color: Get.theme.colorScheme.error); + } - // If it was successful, show a success icon until the next request - return Icon(Icons.done_all, color: Get.theme.colorScheme.onPrimary); - }, - ), + // If it was successful, show a success icon until the next request + return Icon(Icons.done_all, color: Get.theme.colorScheme.onPrimary); + }), ], ), ), diff --git a/lib/theme/components/ssr/ssr_renderer.dart b/lib/theme/components/ssr/ssr_renderer.dart index bf742cc1..452adf81 100644 --- a/lib/theme/components/ssr/ssr_renderer.dart +++ b/lib/theme/components/ssr/ssr_renderer.dart @@ -3,10 +3,12 @@ import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; import 'package:chat_interface/theme/components/ssr/ssr.dart'; import 'package:chat_interface/theme/components/ssr/ssr_fetcher.dart'; +import 'package:chat_interface/util/dispose_hook.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; class SSRRenderer extends StatefulWidget { @@ -14,12 +16,7 @@ class SSRRenderer extends StatefulWidget { final String path; final List json; - const SSRRenderer({ - super.key, - required this.ssr, - required this.json, - required this.path, - }); + const SSRRenderer({super.key, required this.ssr, required this.json, required this.path}); @override State createState() => _SSRRendererState(); @@ -59,33 +56,20 @@ class _SSRRendererState extends State { case 0: return Padding( padding: EdgeInsets.only(bottom: last ? 0 : sectionSpacing), - child: Text( - json["text"], - style: Get.textTheme.headlineMedium, - ), + child: Text(json["text"], style: Get.textTheme.headlineMedium), ); case 1: return Padding( - padding: EdgeInsets.only( - top: defaultSpacing, - bottom: last ? 0 : defaultSpacing, - ), + padding: EdgeInsets.only(top: defaultSpacing, bottom: last ? 0 : defaultSpacing), child: Align( alignment: Alignment.centerLeft, - child: Text( - json["text"], - style: Get.textTheme.titleMedium, - textAlign: TextAlign.start, - ), + child: Text(json["text"], style: Get.textTheme.titleMedium, textAlign: TextAlign.start), ), ); case 2: return Padding( padding: EdgeInsets.only(bottom: last ? 0 : defaultSpacing), - child: Text( - json["text"], - style: Get.textTheme.bodyMedium, - ), + child: Text(json["text"], style: Get.textTheme.bodyMedium), ); } @@ -100,9 +84,7 @@ class _SSRRendererState extends State { toDispose.add(controller); // Make sure the thing is disposed return Padding( - padding: EdgeInsets.only( - bottom: last ? 0 : defaultSpacing, - ), + padding: EdgeInsets.only(bottom: last ? 0 : defaultSpacing), child: FJTextField( controller: controller, obscureText: json["hidden"], @@ -118,9 +100,7 @@ class _SSRRendererState extends State { /// Render a submit button from the element json Widget _renderSubmitButton(Map json, bool last) { return Padding( - padding: EdgeInsets.only( - bottom: last ? 0 : defaultSpacing, - ), + padding: EdgeInsets.only(bottom: last ? 0 : defaultSpacing), child: Column( children: [ AnimatedErrorContainer( @@ -129,22 +109,17 @@ class _SSRRendererState extends State { expand: true, ), _renderButton(json, true), // Last = true for no padding - Obx( - () => Animate( - effects: [ - ExpandEffect( - duration: 250.ms, - axis: Axis.vertical, - alignment: Alignment.bottomCenter, - ) - ], + Watch( + (ctx) => Animate( + effects: [ExpandEffect(duration: 250.ms, axis: Axis.vertical, alignment: Alignment.bottomCenter)], target: widget.ssr.error.value == "" ? 0 : 1, - child: widget.ssr.suggestButton != null - ? Padding( - padding: const EdgeInsets.only(top: defaultSpacing), - child: _renderButton(widget.ssr.suggestButton!, true), // Last = true for no padding - ) - : const SizedBox(), + child: + widget.ssr.suggestButton != null + ? Padding( + padding: const EdgeInsets.only(top: defaultSpacing), + child: _renderButton(widget.ssr.suggestButton!, true), // Last = true for no padding + ) + : const SizedBox(), ), ), ], @@ -154,25 +129,30 @@ class _SSRRendererState extends State { /// Render a normal button using json (NOT A NORMAL ELEMENT) Widget _renderButton(Map json, bool last) { - final loading = false.obs; - return Padding( - padding: EdgeInsets.only(bottom: last ? 0 : defaultSpacing), - child: FJElevatedLoadingButton( - onTap: () async { - if (json["link"] ?? false) { - await launchUrl(Uri.parse(json["path"] ?? "")); - return; - } - - widget.ssr.error.value = ""; - loading.value = true; - await Future.delayed(250.ms); - widget.ssr.suggestButton = null; - widget.ssr.error.value = await widget.ssr.next(json["path"]) ?? ""; - loading.value = false; - }, - label: json["label"], - loading: loading, + final loading = signal(false); + return DisposeHook( + dispose: () { + loading.dispose(); + }, + child: Padding( + padding: EdgeInsets.only(bottom: last ? 0 : defaultSpacing), + child: FJElevatedLoadingButton( + onTap: () async { + if (json["link"] ?? false) { + await launchUrl(Uri.parse(json["path"] ?? "")); + return; + } + + widget.ssr.error.value = ""; + loading.value = true; + await Future.delayed(250.ms); + widget.ssr.suggestButton = null; + widget.ssr.error.value = await widget.ssr.next(json["path"]) ?? ""; + loading.value = false; + }, + label: json["label"], + loading: loading, + ), ), ); } @@ -194,12 +174,7 @@ class _SSRRendererState extends State { Widget _renderError(String type, bool last) { return Padding( padding: EdgeInsets.only(bottom: last ? 0 : defaultSpacing), - child: ErrorContainer( - message: "render.error".trParams({ - "type": type, - }), - expand: true, - ), + child: ErrorContainer(message: "render.error".trParams({"type": type}), expand: true), ); } @@ -215,13 +190,11 @@ class _SSRRendererState extends State { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - children: widgets + + children: + widgets + [ if (widget.ssr.extra?[widget.path] != null) - Padding( - padding: const EdgeInsets.only(top: defaultSpacing), - child: widget.ssr.extra?[widget.path], - ), + Padding(padding: const EdgeInsets.only(top: defaultSpacing), child: widget.ssr.extra?[widget.path]), ], ); } diff --git a/lib/theme/components/transitions/horizontal_transition.dart b/lib/theme/components/transitions/horizontal_transition.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/theme/components/transitions/transition_container.dart b/lib/theme/components/transitions/transition_container.dart deleted file mode 100644 index b27c840d..00000000 --- a/lib/theme/components/transitions/transition_container.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:chat_interface/theme/components/transitions/transition_controller.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:get/get.dart'; - -class TransitionContainer extends StatefulWidget { - final Color? color; - final double? width; - final BorderRadius? borderRadius; - final Widget child; - final String tag; - final bool fade; - - const TransitionContainer({super.key, required this.child, required this.tag, this.borderRadius, this.color, this.width, this.fade = false}); - - @override - State createState() => _AnimatedContainerState(); -} - -class _AnimatedContainerState extends State { - @override - Widget build(BuildContext context) { - Effect mainEffect; - - if (widget.fade) { - mainEffect = FadeEffect( - duration: 250.ms, - begin: 0, - end: 1, - ); - } else { - mainEffect = FadeEffect( - duration: 250.ms, - begin: 0, - end: 1, - ); - } - - return GetX( - builder: (controller) { - return IgnorePointer( - ignoring: controller.transition.value, - child: Hero( - tag: "login", - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: widget.width ?? double.infinity), - child: Container( - decoration: BoxDecoration( - borderRadius: widget.borderRadius, - color: widget.color ?? Theme.of(context).colorScheme.onInverseSurface, - ), - child: Animate( - effects: [ - mainEffect, - ], - target: controller.transition.value ? 0 : 1, - child: widget.child, - ), - ), - ), - ), - ); - }, - ); - } -} diff --git a/lib/theme/components/transitions/transition_container_test.dart b/lib/theme/components/transitions/transition_container_test.dart deleted file mode 100644 index baccd3da..00000000 --- a/lib/theme/components/transitions/transition_container_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:chat_interface/theme/components/transitions/transition_container.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import 'transition_container_test2.dart'; -import 'transition_controller.dart'; - -class AnimatedContainerTestPage extends StatelessWidget { - const AnimatedContainerTestPage({super.key}); - - @override - Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - - return Scaffold( - backgroundColor: theme.colorScheme.inverseSurface, - body: Center( - child: TransitionContainer( - tag: "test", - color: theme.colorScheme.onInverseSurface, - child: InkWell( - onTap: () => Get.find().modelTransition(const AnimatedContainerTestPage2()), - child: const Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Hello world.", style: TextStyle(color: Colors.white)), - Text("Hello world.", style: TextStyle(color: Colors.white)), - Text("Hello world.", style: TextStyle(color: Colors.white)), - Text("Hello world.", style: TextStyle(color: Colors.white)), - Text("Hello world.", style: TextStyle(color: Colors.white)), - ], - )), - ), - ), - ); - } -} diff --git a/lib/theme/components/transitions/transition_container_test2.dart b/lib/theme/components/transitions/transition_container_test2.dart deleted file mode 100644 index ab66892a..00000000 --- a/lib/theme/components/transitions/transition_container_test2.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:chat_interface/theme/components/transitions/transition_container.dart'; -import 'package:chat_interface/theme/components/transitions/transition_container_test.dart'; -import 'package:chat_interface/theme/components/transitions/transition_controller.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -class AnimatedContainerTestPage2 extends StatelessWidget { - const AnimatedContainerTestPage2({super.key}); - - @override - Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - - return Scaffold( - backgroundColor: theme.colorScheme.inverseSurface, - body: Center( - child: TransitionContainer( - tag: "test", - color: theme.colorScheme.onInverseSurface, - child: InkWell( - onTap: () => Get.find().modelTransition(const AnimatedContainerTestPage()), - child: const Text("Hello world.", style: TextStyle(color: Colors.white))), - ), - ), - ); - } -} diff --git a/lib/theme/components/transitions/transition_controller.dart b/lib/theme/components/transitions/transition_controller.dart deleted file mode 100644 index 4321e6f7..00000000 --- a/lib/theme/components/transitions/transition_controller.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:get/get.dart'; - -class TransitionController extends GetxController { - final transitionDuration = 250.ms; // constant - final transition = false.obs; - - Timer? currentTimer; - - void cancelAll() { - transition.value = false; - currentTimer?.cancel(); - } - - void modelTransition(dynamic page) { - transitionTo(page, (page) => Get.offAll(page, transition: Transition.fade)); - } - - void dialogTransition(dynamic page) { - Get.back(); - Get.dialog( - page, - barrierDismissible: false, - ); - } - - void transitionTo(dynamic page, Function(dynamic) goTo) { - // Reset the state - currentTimer?.cancel(); - transition.value = true; - - // Start a timer to give the hero element time to fade out - currentTimer = Timer(transitionDuration, () { - goTo(page); - - currentTimer = Timer(transitionDuration, () { - transition.value = false; - }); - }); - } -} diff --git a/lib/theme/components/user_renderer.dart b/lib/theme/components/user_renderer.dart index e5551e42..7b63a87f 100644 --- a/lib/theme/components/user_renderer.dart +++ b/lib/theme/components/user_renderer.dart @@ -1,18 +1,18 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/theme/ui/profile/status_renderer.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class UserAvatar extends StatefulWidget { final LPHAddress id; final double? size; - final FriendController? controller; final Friend? user; - const UserAvatar({super.key, required this.id, this.size, this.controller, this.user}); + const UserAvatar({super.key, required this.id, this.size, this.user}); @override State createState() => _UserAvatarState(); @@ -30,8 +30,7 @@ class _UserAvatarState extends State { if (widget.user != null) { return widget.user!; } - final controller = widget.controller ?? Get.find(); - return controller.friends[widget.id] ?? Friend.unknown(widget.id); + return FriendController.friends[widget.id] ?? Friend.unknown(widget.id); } @override @@ -41,38 +40,34 @@ class _UserAvatarState extends State { return SizedBox( width: widget.size ?? 45, height: widget.size ?? 45, - child: Obx( - () { - if (friend.profilePictureImage.value != null) { - final image = friend.profilePictureImage.value!; - return ClipOval( - child: RawImage( - fit: BoxFit.contain, - image: image, - ), - ); - } + child: Watch((ctx) { + if (friend.profilePictureImage.value != null) { + final image = friend.profilePictureImage.value!; + return ClipOval(child: RawImage(fit: BoxFit.contain, image: image)); + } - final cuttedDisplayName = friend.displayName.value.substring(0, 1); - return ClipOval( - child: Container( - color: Get.theme.colorScheme.primaryContainer, - child: SelectionContainer.disabled( - child: Center( - child: Text( - cuttedDisplayName, - style: Get.theme.textTheme.labelMedium!.copyWith( - fontSize: (widget.size ?? 45) * 0.5, - fontWeight: FontWeight.bold, - color: widget.id == StatusController.ownAddress ? Get.theme.colorScheme.tertiary : Get.theme.colorScheme.onPrimary, - ), + final cuttedDisplayName = friend.displayName.value.substring(0, 1); + return ClipOval( + child: Container( + color: Get.theme.colorScheme.primaryContainer, + child: SelectionContainer.disabled( + child: Center( + child: Text( + cuttedDisplayName, + style: Get.theme.textTheme.labelMedium!.copyWith( + fontSize: (widget.size ?? 45) * 0.5, + fontWeight: FontWeight.bold, + color: + widget.id == StatusController.ownAddress + ? Get.theme.colorScheme.tertiary + : Get.theme.colorScheme.onPrimary, ), ), ), ), - ); - }, - ), + ), + ); + }), ); } } @@ -85,10 +80,9 @@ class UserRenderer extends StatelessWidget { @override Widget build(BuildContext context) { - var friend = (controller ?? Get.find()).friends[id]; + var friend = FriendController.friends[id]; final own = id == StatusController.ownAddress; - StatusController? statusController = own ? Get.find() : null; - if (own) friend = Friend.me(statusController); + if (own) friend = Friend.me(); friend ??= Friend.unknown(id); return Row( @@ -103,39 +97,39 @@ class UserRenderer extends StatelessWidget { children: [ Row( children: [ - Flexible(child: Text(friend.displayName.value, overflow: TextOverflow.ellipsis, style: Get.theme.textTheme.bodyMedium)), + Flexible( + child: Text( + friend.displayName.value, + overflow: TextOverflow.ellipsis, + style: Get.theme.textTheme.bodyMedium, + ), + ), if (friend.id.server != basePath) Padding( padding: const EdgeInsets.only(left: defaultSpacing), child: Tooltip( waitDuration: const Duration(milliseconds: 500), - message: "friends.different_town".trParams({ - "town": friend.id.server, - }), - child: Icon( - Icons.sensors, - color: Get.theme.colorScheme.onPrimary, - size: 21, - ), + message: "friends.different_town".trParams({"town": friend.id.server}), + child: Icon(Icons.sensors, color: Get.theme.colorScheme.onPrimary, size: 21), ), ), horizontalSpacing(defaultSpacing), - Obx(() => StatusRenderer(status: own ? statusController!.type.value : friend!.statusType.value)), + Watch((ctx) => StatusRenderer(status: own ? StatusController.type.value : friend!.statusType.value)), ], ), - Obx( - () => Visibility( - visible: own ? statusController!.status.value != "" : friend!.status.value != "", + Watch( + (ctx) => Visibility( + visible: own ? StatusController.status.value != "" : friend!.status.value != "", child: Text( - own ? statusController!.status.value : friend!.status.value, + own ? StatusController.status.value : friend!.status.value, style: Get.theme.textTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ), - ) + ), ], ), - ) + ), ], ); } diff --git a/lib/theme/default_theme.dart b/lib/theme/default_theme.dart index 99b4034e..6ae8f231 100644 --- a/lib/theme/default_theme.dart +++ b/lib/theme/default_theme.dart @@ -1,9 +1,5 @@ import 'package:flutter/material.dart'; -final ThemeData defaultLightTheme = ThemeData.light( - useMaterial3: true, -); +final ThemeData defaultLightTheme = ThemeData.light(useMaterial3: true); -final ThemeData defaultDarkTheme = ThemeData.dark( - useMaterial3: true, -); +final ThemeData defaultDarkTheme = ThemeData.dark(useMaterial3: true); diff --git a/lib/theme/desktop_widgets.dart b/lib/theme/desktop_widgets.dart deleted file mode 100644 index b5cc8162..00000000 --- a/lib/theme/desktop_widgets.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:io'; - -import 'package:chat_interface/main.dart'; -import 'package:flutter/widgets.dart'; -import 'package:get/get.dart'; -import 'package:tray_manager/tray_manager.dart'; -import 'package:window_manager/window_manager.dart'; - -class CloseToTray extends StatefulWidget { - final Widget child; - - const CloseToTray({ - super.key, - required this.child, - }); - - @override - State createState() => _CloseToTrayState(); -} - -class _CloseToTrayState extends State with WindowListener, TrayListener { - @override - void initState() { - if (isDesktopPlatform()) { - // Init all the features of the tray - initTray(); - - // Add the listener to listen to window and tray events - windowManager.addListener(this); - trayManager.addListener(this); - windowManager.setPreventClose(true); - } - super.initState(); - } - - @override - void dispose() { - if (isDesktopPlatform()) { - trayManager.destroy(); - windowManager.setPreventClose(false); - windowManager.removeListener(this); - trayManager.removeListener(this); - } - super.dispose(); - } - - /// Adds Liphium to the tray - Future initTray() async { - await trayManager.setIcon(Platform.isWindows - ? "assets/tray/icon_windows.ico" - : Platform.isMacOS - ? "assets/tray/icon_macos.png" - : "assets/tray/icon_linux.png"); - await trayManager.setToolTip("Liphium"); - await trayManager.setContextMenu( - Menu( - items: [ - MenuItem( - key: "show_window", - label: "tray.show_window".tr, - onClick: (item) { - windowManager.show(); - }, - ), - MenuItem( - key: "exit_app", - label: "tray.exit_app".tr, - onClick: (item) { - exit(0); - }, - ), - ], - ), - ); - } - - // Show the app when the tray icon is clicked - @override - Future onTrayIconMouseDown() async { - await windowManager.show(); - } - - // Make sure the context menu opens when the tray icon is clicked - @override - Future onTrayIconRightMouseDown() async { - await trayManager.popUpContextMenu(); - } - - @override - Future onWindowClose() async { - if (isDebug) { - exit(0); - } else { - await windowManager.setPreventClose(true); - await windowManager.hide(); - } - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} diff --git a/lib/theme/dialog_route.dart b/lib/theme/dialog_route.dart index 579e42e5..3ac0fb50 100644 --- a/lib/theme/dialog_route.dart +++ b/lib/theme/dialog_route.dart @@ -29,16 +29,17 @@ class HeroDialogRoute extends PageRoute { String? get barrierLabel => "hi"; @override - Widget buildTransitions(BuildContext context, Animation animation, - Animation secondaryAnimation, Widget child) { - return FadeTransition( - opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), - child: child); + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition(opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child); } @override - Widget buildPage(BuildContext context, Animation animation, - Animation secondaryAnimation) { + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { return builder(context); } } diff --git a/lib/theme/impl/metal_theme.dart b/lib/theme/impl/metal_theme.dart index 278bad82..0163d8e7 100644 --- a/lib/theme/impl/metal_theme.dart +++ b/lib/theme/impl/metal_theme.dart @@ -3,131 +3,130 @@ import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; final ThemeData lightMetalTheme = defaultLightTheme.copyWith( + brightness: Brightness.light, + colorScheme: const ColorScheme( brightness: Brightness.light, - colorScheme: const ColorScheme( - brightness: Brightness.light, - primary: Color(0xFFE5E5E5), - onPrimary: Color(0xFFE5E5E5), - secondary: Color(0xFFE5E5E5), - onSecondary: Color(0xFFE5E5E5), - inverseSurface: Color(0xFFE5E5E5), - onInverseSurface: Color(0xFFE5E5E5), - error: Color(0xFFE5E5E5), - onError: Color(0xFFE5E5E5), - surface: Color(0xFFE5E5E5), - onSurface: Color(0xFFE5E5E5), - )); + primary: Color(0xFFE5E5E5), + onPrimary: Color(0xFFE5E5E5), + secondary: Color(0xFFE5E5E5), + onSecondary: Color(0xFFE5E5E5), + inverseSurface: Color(0xFFE5E5E5), + onInverseSurface: Color(0xFFE5E5E5), + error: Color(0xFFE5E5E5), + onError: Color(0xFFE5E5E5), + surface: Color(0xFFE5E5E5), + onSurface: Color(0xFFE5E5E5), + ), +); final ThemeData darkMetalTheme = defaultDarkTheme.copyWith( + brightness: Brightness.dark, + colorScheme: const ColorScheme( + // Background color brightness: Brightness.dark, - colorScheme: const ColorScheme( - // Background color - brightness: Brightness.dark, - inverseSurface: Color(0xFF292929), - onInverseSurface: Color(0xFF1c1c1c), - primaryContainer: Color(0xFF171717), + inverseSurface: Color(0xFF292929), + onInverseSurface: Color(0xFF1c1c1c), + primaryContainer: Color(0xFF171717), - // Online color - secondary: Color(0xFF7cda81), + // Online color + secondary: Color(0xFF7cda81), - // AFK color - secondaryContainer: Color(0xFFF5C211), + // AFK color + secondaryContainer: Color(0xFFF5C211), - // Primary color - primary: Color(0xFF0d3b54), - onPrimary: Color(0xFF99c1f1), + // Primary color + primary: Color(0xFF0d3b54), + onPrimary: Color(0xFF99c1f1), - // Tertiary color - tertiary: Color(0xFFf7c5db), - onTertiary: Color(0xFFd8749f), - tertiaryContainer: Color(0xFFd8749f), + // Tertiary color + tertiary: Color(0xFFf7c5db), + onTertiary: Color(0xFFd8749f), + tertiaryContainer: Color(0xFFd8749f), - // Error color - error: Color(0xFFda827c), - onError: Color(0xFFcc6d66), - errorContainer: Color.fromARGB(255, 77, 15, 10), + // Error color + error: Color(0xFFda827c), + onError: Color(0xFFcc6d66), + errorContainer: Color.fromARGB(255, 77, 15, 10), - // Unused - onSecondary: Color(0xFFE5E5E5), + // Unused + onSecondary: Color(0xFFE5E5E5), - // Unimportant font colors - surface: Color(0xFFbababa), + // Unimportant font colors + surface: Color(0xFFbababa), - // Important font color - onSurface: Color(0xFFFFFFFF), + // Important font color + onSurface: Color(0xFFFFFFFF), + ), + tooltipTheme: TooltipThemeData( + decoration: BoxDecoration(color: const Color(0xFF171717), borderRadius: BorderRadius.circular(defaultSpacing)), + textStyle: defaultDarkTheme.textTheme.labelMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + color: const Color(0xFFFFFFFF), ), - tooltipTheme: TooltipThemeData( - decoration: BoxDecoration( - color: const Color(0xFF171717), - borderRadius: BorderRadius.circular(defaultSpacing), - ), - textStyle: defaultDarkTheme.textTheme.labelMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: const Color(0xFFFFFFFF), - ), + ), + textSelectionTheme: const TextSelectionThemeData( + cursorColor: Color(0xFF99c1f1), + selectionColor: Color(0xFF5c5c5c), + selectionHandleColor: Color(0xFF99c1f1), + ), + dividerColor: const Color(0xFF5c5c5c), + textTheme: defaultDarkTheme.textTheme.copyWith( + //* Headlines + headlineMedium: defaultDarkTheme.textTheme.headlineMedium!.copyWith( + fontFamily: 'Roboto Mono', + fontWeight: FontWeight.bold, ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: Color(0xFF99c1f1), - selectionColor: Color(0xFF5c5c5c), - selectionHandleColor: Color(0xFF99c1f1), - ), - dividerColor: const Color(0xFF5c5c5c), - textTheme: defaultDarkTheme.textTheme.copyWith( - //* Headlines - headlineMedium: defaultDarkTheme.textTheme.headlineMedium!.copyWith( - fontFamily: 'Roboto Mono', - fontWeight: FontWeight.bold, - ), - //* Normal body text - bodySmall: defaultDarkTheme.textTheme.bodySmall!.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: const Color(0xFFbababa), - ), - bodyMedium: defaultDarkTheme.textTheme.bodyMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: const Color(0xFFbababa), - ), - bodyLarge: defaultDarkTheme.textTheme.bodyLarge!.copyWith( - fontSize: 18, - fontWeight: FontWeight.normal, - color: const Color(0xFFbababa), - ), + //* Normal body text + bodySmall: defaultDarkTheme.textTheme.bodySmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.normal, + color: const Color(0xFFbababa), + ), + bodyMedium: defaultDarkTheme.textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + color: const Color(0xFFbababa), + ), + bodyLarge: defaultDarkTheme.textTheme.bodyLarge!.copyWith( + fontSize: 18, + fontWeight: FontWeight.normal, + color: const Color(0xFFbababa), + ), - //* Labels - labelLarge: defaultDarkTheme.textTheme.labelLarge!.copyWith( - fontSize: 18, - fontWeight: FontWeight.normal, - color: const Color(0xFFFFFFFF), - ), - labelMedium: defaultDarkTheme.textTheme.labelMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: const Color(0xFFFFFFFF), - ), - labelSmall: defaultDarkTheme.textTheme.labelSmall!.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: const Color(0xFFFFFFFF), - ), + //* Labels + labelLarge: defaultDarkTheme.textTheme.labelLarge!.copyWith( + fontSize: 18, + fontWeight: FontWeight.normal, + color: const Color(0xFFFFFFFF), + ), + labelMedium: defaultDarkTheme.textTheme.labelMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + color: const Color(0xFFFFFFFF), + ), + labelSmall: defaultDarkTheme.textTheme.labelSmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.normal, + color: const Color(0xFFFFFFFF), + ), - //* Titles - titleLarge: defaultDarkTheme.textTheme.titleLarge!.copyWith( - fontSize: 20, - fontWeight: FontWeight.w600, - color: const Color(0xFFFFFFFF), - ), - titleMedium: defaultDarkTheme.textTheme.titleMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w500, - color: const Color(0xFFFFFFFF), - ), - titleSmall: defaultDarkTheme.textTheme.titleSmall!.copyWith( - fontSize: 16, - fontWeight: FontWeight.w400, - color: const Color(0xFFFFFFFF), - ), - )); + //* Titles + titleLarge: defaultDarkTheme.textTheme.titleLarge!.copyWith( + fontSize: 20, + fontWeight: FontWeight.w600, + color: const Color(0xFFFFFFFF), + ), + titleMedium: defaultDarkTheme.textTheme.titleMedium!.copyWith( + fontSize: 18, + fontWeight: FontWeight.w500, + color: const Color(0xFFFFFFFF), + ), + titleSmall: defaultDarkTheme.textTheme.titleSmall!.copyWith( + fontSize: 16, + fontWeight: FontWeight.w400, + color: const Color(0xFFFFFFFF), + ), + ), +); diff --git a/lib/theme/theme_manager.dart b/lib/theme/theme_manager.dart index abf57678..e93430eb 100644 --- a/lib/theme/theme_manager.dart +++ b/lib/theme/theme_manager.dart @@ -1,19 +1,21 @@ import 'package:chat_interface/pages/settings/appearance/theme_settings.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; -class ThemeManager extends GetxController { - final currentTheme = getThemeDataFromFactory(buildColorFactoryFromPreset(ThemeSettings.themePresets[0])).obs; +class ThemeManager { + static final currentTheme = signal( + getThemeDataFromFactory(buildColorFactoryFromPreset(ThemeSettings.themePresets[0])), + ); - final brightness = Brightness.dark.obs; + static final brightness = signal(Brightness.dark); // Changes the color theme - void changeTheme(ThemeData theme) { + static void changeTheme(ThemeData theme) { currentTheme.value = theme; } // Changes the brightness (light or dark) - void changeBrightness(Brightness value) { + static void changeBrightness(Brightness value) { brightness.value = value; } } diff --git a/lib/theme/ui/containers/universal_app_bar.dart b/lib/theme/ui/containers/universal_app_bar.dart index 132d4a3f..f5996523 100644 --- a/lib/theme/ui/containers/universal_app_bar.dart +++ b/lib/theme/ui/containers/universal_app_bar.dart @@ -8,11 +8,7 @@ class UniversalAppBar extends StatelessWidget { final String label; final bool applyPadding; - const UniversalAppBar({ - super.key, - required this.label, - this.applyPadding = false, - }); + const UniversalAppBar({super.key, required this.label, this.applyPadding = false}); @override Widget build(BuildContext context) { @@ -23,10 +19,7 @@ class UniversalAppBar extends StatelessWidget { top: applyPadding, right: true, left: true, - padding: const EdgeInsets.symmetric( - vertical: elementSpacing, - horizontal: defaultSpacing, - ), + padding: const EdgeInsets.symmetric(vertical: elementSpacing, horizontal: defaultSpacing), child: Row( children: [ LoadingIconButton( @@ -36,10 +29,7 @@ class UniversalAppBar extends StatelessWidget { color: Get.theme.colorScheme.onPrimary, ), horizontalSpacing(defaultSpacing), - Text( - label.tr, - style: Get.theme.textTheme.labelLarge, - ) + Text(label.tr, style: Get.theme.textTheme.labelLarge), ], ), ), diff --git a/lib/theme/ui/conversation_util.dart b/lib/theme/ui/conversation_util.dart new file mode 100644 index 00000000..9d0e0c67 --- /dev/null +++ b/lib/theme/ui/conversation_util.dart @@ -0,0 +1,94 @@ +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/conversation/square.dart'; +import 'package:chat_interface/pages/chat/components/conversations/conversation_dev_window.dart'; +import 'package:chat_interface/pages/chat/components/conversations/conversation_edit_window.dart'; +import 'package:chat_interface/services/squares/square_container.dart'; +import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; +import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; +import 'package:chat_interface/theme/ui/profile/profile.dart'; +import 'package:chat_interface/util/popups.dart'; +import 'package:chat_interface/util/vertical_spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ConversationUtil { + /// Get an appropriate icon for the current conversation. + static IconData getIconForConversation(Conversation conversation, {String extra = ""}) { + if (extra != "") { + return Icons.numbers; + } + return conversation.getIconForConversation(); + } + + /// Get an appropriate name for the current conversation. + static String getNameForConversation(Conversation conversation, {String extra = ""}) { + // Return the current topic name in case it's a square + if (conversation is Square) { + final container = conversation.containerSub.value as SquareContainer; + if (extra == "") { + return container.name; + } + return container.topics.firstWhereOrNull((t) => t.id == extra)?.name ?? container.name; + } + + // Return the name or the name of the friend depending on the type + return conversation.isGroup ? conversation.containerSub.value.name : conversation.dmName; + } + + /// Open the appropriate dialog for the conversation. + static void openDialogForConversation(Conversation conversation, ContextMenuData data, {String extra = ""}) { + // Open topic specific or conversation edit dialog + if (conversation is Square && extra != "") { + showModal(ConversationEditWindow(position: data, conversation: conversation, extra: extra)); + return; + } + + // Open the profile in case it's a direct message + if (!conversation.isGroup) { + // Create the function that generates the actions for the friend + List buildActions(Friend friend) { + final actions = ProfileDefaults.buildDefaultActions(friend, messageAction: false); + + // Add an action to open the developer window + actions.insert( + 0, + ProfileAction( + icon: Icons.developer_mode, + label: "For developers", + onTap: (_, _) => showModal(ConversationDevWindow(conversation: conversation)), + ), + ); + + // Add an action to leave the conversation + actions.add( + ProfileAction( + icon: Icons.logout, + label: "conversations.leave".tr, + onTap: + (_, _) => showConfirmPopup( + ConfirmWindow( + title: "conversations.leave".tr, + text: "conversations.leave.text".tr, + onConfirm: () { + conversation.delete(); + Get.back(); + }, + onDecline: () => {}, + ), + ), + color: Get.theme.colorScheme.onError, + iconColor: Get.theme.colorScheme.error, + ), + ); + return actions; + } + + showModal(Profile(friend: conversation.otherMember, data: data, actions: buildActions)); + return; + } + + // Show the conversation edit window for everything else + showModal(ConversationEditWindow(position: data, conversation: conversation)); + } +} diff --git a/lib/theme/ui/dialogs/confirm_window.dart b/lib/theme/ui/dialogs/confirm_window.dart index 4ef5deff..80b6f751 100644 --- a/lib/theme/ui/dialogs/confirm_window.dart +++ b/lib/theme/ui/dialogs/confirm_window.dart @@ -18,9 +18,7 @@ class ConfirmWindow extends StatelessWidget { @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text(title, style: Get.theme.textTheme.titleMedium), - ], + title: [Text(title, style: Get.theme.textTheme.titleMedium)], child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -35,9 +33,7 @@ class ConfirmWindow extends StatelessWidget { Get.back(result: true); onConfirm?.call(); }, - child: Center( - child: Text("yes".tr, style: Get.theme.textTheme.titleMedium), - ), + child: Center(child: Text("yes".tr, style: Get.theme.textTheme.titleMedium)), ), ), horizontalSpacing(defaultSpacing), @@ -47,13 +43,11 @@ class ConfirmWindow extends StatelessWidget { Get.back(result: false); onDecline?.call(); }, - child: Center( - child: Text("no".tr, style: Get.theme.textTheme.titleMedium), - ), + child: Center(child: Text("no".tr, style: Get.theme.textTheme.titleMedium)), ), ), ], - ) + ), ], ), ); diff --git a/lib/theme/ui/dialogs/conversation_add_window.dart b/lib/theme/ui/dialogs/conversation_add_window.dart index 5fa5eb10..813f1184 100644 --- a/lib/theme/ui/dialogs/conversation_add_window.dart +++ b/lib/theme/ui/dialogs/conversation_add_window.dart @@ -1,8 +1,12 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'dart:async'; + +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; +import 'package:chat_interface/controller/conversation/message_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/chat/sidebar/friends/friends_page.dart'; import 'package:chat_interface/pages/status/error/error_container.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_textfield.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; @@ -12,6 +16,7 @@ import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import '../../../util/vertical_spacing.dart'; @@ -20,7 +25,7 @@ class ConversationAddWindow extends StatefulWidget { final String action; final bool nameField; final List? initial; - final ContextMenuData position; + final ContextMenuData? position; /// Called when clicking the action button (returns error text or closed on null) final Future Function(List, String?)? onDone; @@ -38,49 +43,54 @@ class ConversationAddWindow extends StatefulWidget { @override State createState() => _ConversationAddWindowState(); + /// Create a conversation using a list of friends. + /// + /// Name is optional as it's not required for direct messages. static Future createConversationAction(List friends, String? name) async { + // Make sure the selection is valid if (friends.isEmpty) { return "choose.members".tr; } - if (friends.length > specialConstants[Constants.specialConstantMaxConversationMembers]!) { - return "choose.members".tr; + return "conversations.too_many_members".trParams({ + "amount": specialConstants[Constants.specialConstantMaxConversationMembers]!.toString(), + }); } - if (name == null && friends.length > 1) { - return "enter.name".tr; + return "conversations.name_needed".tr; } - if (name!.isEmpty && friends.length > 1) { - return "enter.name".tr; + return "conversations.name_needed".tr; } - if (name.length > specialConstants[Constants.specialConstantMaxConversationNameLength]! && friends.length > 1) { - return "too.long".trParams({"limit": specialConstants["max_conversation_name_length"].toString()}); + return "conversations.name.length".trParams({ + "length": specialConstants["max_conversation_name_length"].toString(), + }); } - var result = false; + // Open a group or direct message based on the amount of people in it + String? error; if (friends.length == 1) { - result = await openDirectMessage(friends.first); + Conversation? conv; + (conv, error) = await ConversationService.openDirectMessage(friends.first); + if (conv != null) { + unawaited(MessageController.openConversation(conv)); + } } else { - result = await openGroupConversation(friends, name); + error = await ConversationService.openGroupConversation(friends, name); } - return result ? null : "server.error".tr; + return error; } } class _ConversationAddWindowState extends State { // State - final _members = [].obs; - final _length = 0.obs; - final _conversationLoading = false.obs; - final _errorText = "".obs; - final _search = "".obs; - - // Input - final _searchFocusNode = FocusNode(); - final _searchController = TextEditingController(); + final _members = listSignal([]); + late final _length = computed(() => _members.length); + final _conversationLoading = signal(false); + final _errorText = signal(""); + final _search = signal(""); final _controller = TextEditingController(); @override @@ -90,179 +100,44 @@ class _ConversationAddWindowState extends State { _members.add(friend); } } - if (!isMobileMode()) { - _searchFocusNode.requestFocus(); - } super.initState(); } @override void dispose() { - _searchFocusNode.dispose(); - _searchController.dispose(); _controller.dispose(); + _members.dispose(); + _length.dispose(); + _conversationLoading.dispose(); + _errorText.dispose(); + _search.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - ThemeData theme = Theme.of(context); - FriendController friendController = Get.find(); + final theme = Theme.of(context); - if (friendController.friends.length == 1) { + if (FriendController.friends.length == 1) { return SlidingWindowBase( - title: [ - Text(widget.title.tr, style: Get.theme.textTheme.labelLarge), - ], + title: [Text(widget.title.tr, style: theme.textTheme.labelLarge)], position: widget.position, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("no.friends".tr, style: theme.textTheme.bodyMedium), - verticalSpacing(defaultSpacing), - FJElevatedButton( - onTap: () { - Get.back(); - showModal(const FriendsPage()); - }, - child: Center( - child: Text("open.friends".tr, style: theme.textTheme.labelLarge), - ), - ), - ], - ), + child: NoFriendsMessage(), ); } return SlidingWindowBase( position: widget.position, title: [ - Text(widget.title.tr, style: Get.theme.textTheme.labelLarge), + Text(widget.title.tr, style: theme.textTheme.labelLarge), const Spacer(), - Obx(() => Text("${_members.length}/100", style: Get.theme.textTheme.bodyLarge)), + Watch((ctx) => Text("${_members.length}/100", style: theme.textTheme.bodyLarge)), ], child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - //* Input - Container( - decoration: BoxDecoration( - color: theme.colorScheme.inverseSurface, - borderRadius: BorderRadius.circular(defaultSpacing), - ), - padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), - child: Row( - children: [ - Icon(Icons.search, size: 25, color: theme.colorScheme.onPrimary), - horizontalSpacing(defaultSpacing), - Expanded( - child: TextField( - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'search'.tr, - hintStyle: Get.textTheme.bodyLarge, - ), - cursorColor: theme.colorScheme.onPrimary, - style: theme.textTheme.labelLarge, - onChanged: (value) => _search.value = value, - focusNode: _searchFocusNode, - controller: _searchController, - onSubmitted: (value) { - // Make the first friend that matches the search the selected one - if (friendController.friends.isNotEmpty) { - final member = friendController.friends.values.firstWhere( - (element) => - (element.name.toLowerCase().contains(value.toLowerCase()) || - element.displayName.value.toLowerCase().contains(value.toLowerCase())) && - element.id != StatusController.ownAddress, - orElse: () => Friend.unknown(LPHAddress.error()), - ); - if (member.id.id != "-") { - if (_members.contains(member)) { - _members.remove(member); - } else if (member.id != StatusController.ownAddress) { - _members.add(member); - } - } - _length.value = _members.length; - - _search.value = ""; - _searchController.clear(); - if (!isMobileMode()) { - _searchFocusNode.requestFocus(); - } - } - }, - maxLines: 1, - ), - ), - ], - ), - ), - - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: Obx( - () => ListView.builder( - itemCount: friendController.friends.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: defaultSpacing), - itemBuilder: (context, index) { - Friend friend = friendController.friends.values.elementAt(index); - - if (friend.id == StatusController.ownAddress) { - return const SizedBox(); - } - - return Obx(() { - final search = _search.value; - if (search.isNotEmpty && - !(friend.name.toLowerCase().contains(search.toLowerCase()) || - friend.displayName.value.toLowerCase().contains(search.toLowerCase()))) { - return const SizedBox(); - } - - return Padding( - padding: const EdgeInsets.only(bottom: defaultSpacing), - child: Obx( - () => Material( - color: _members.contains(friend) ? theme.colorScheme.primary : Colors.transparent, - borderRadius: BorderRadius.circular(defaultSpacing), - child: InkWell( - borderRadius: BorderRadius.circular(defaultSpacing), - onTap: () { - if (widget.initial != null && widget.initial!.contains(friend)) { - return; - } - if (_members.contains(friend)) { - _members.remove(friend); - } else { - _members.add(friend); - } - _length.value = _members.length; - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: elementSpacing, vertical: elementSpacing), - child: Row( - children: [ - UserAvatar( - id: friend.id, - size: 35, - ), - horizontalSpacing(defaultSpacing), - Obx(() => Text(friend.displayName.value, style: theme.textTheme.labelLarge)), - ], - ), - ), - ), - ), - ), - ); - }); - }, - ), - ), - ), + // Friend selector for the members of the conversation + FriendSelector(signal: _members, initial: widget.initial), //* Create conversation button Column( @@ -271,8 +146,8 @@ class _ConversationAddWindowState extends State { Visibility( visible: widget.nameField, child: RepaintBoundary( - child: Obx( - () => Animate( + child: Watch( + (ctx) => Animate( effects: [ ExpandEffect( alignment: Alignment.topCenter, @@ -280,17 +155,14 @@ class _ConversationAddWindowState extends State { curve: Curves.ease, axis: Axis.vertical, ), - FadeEffect( - begin: 0, - end: 1, - duration: 250.ms, - ), + FadeEffect(begin: 0, end: 1, duration: 250.ms), ], target: _length.value > 1 ? 1 : 0, child: Padding( padding: const EdgeInsets.only(bottom: defaultSpacing), child: FJTextField( controller: _controller, + maxLength: specialConstants[Constants.specialConstantMaxConversationNameLength], hintText: "conversations.name".tr, ), ), @@ -328,9 +200,191 @@ class _ConversationAddWindowState extends State { loading: _conversationLoading, ), ], - ) + ), ], ), ); } } + +class NoFriendsMessage extends StatelessWidget { + const NoFriendsMessage({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("no.friends".tr, style: theme.textTheme.bodyMedium), + verticalSpacing(defaultSpacing), + FJElevatedButton( + onTap: () { + Get.back(); + showModal(const FriendsPage()); + }, + child: Center(child: Text("open.friends".tr, style: theme.textTheme.labelLarge)), + ), + ], + ); + } +} + +class FriendSelector extends StatefulWidget { + final List? initial; + final ListSignal signal; + + const FriendSelector({super.key, required this.signal, this.initial}); + + @override + State createState() => _FriendSelectorState(); +} + +class _FriendSelectorState extends State { + // Input + final _search = signal(""); + final _searchFocusNode = FocusNode(); + final _searchController = TextEditingController(); + + @override + void initState() { + if (!isMobileMode()) { + _searchFocusNode.requestFocus(); + } + super.initState(); + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _searchController.dispose(); + _search.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Search input + Container( + decoration: BoxDecoration( + color: theme.colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(defaultSpacing), + ), + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), + child: Row( + children: [ + Icon(Icons.search, size: 25, color: theme.colorScheme.onPrimary), + horizontalSpacing(defaultSpacing), + Expanded( + child: TextField( + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'search'.tr, + hintStyle: Get.textTheme.bodyLarge, + ), + cursorColor: theme.colorScheme.onPrimary, + style: theme.textTheme.labelLarge, + onChanged: (value) => _search.value = value, + focusNode: _searchFocusNode, + controller: _searchController, + onSubmitted: (value) { + // Make the first friend that matches the search the selected one + if (FriendController.friends.isNotEmpty) { + final member = FriendController.friends.values.firstWhere( + (element) => + (element.name.toLowerCase().contains(value.toLowerCase()) || + element.displayName.value.toLowerCase().contains(value.toLowerCase())) && + element.id != StatusController.ownAddress, + orElse: () => Friend.unknown(LPHAddress.error()), + ); + if (member.id.id != "-") { + if (widget.signal.contains(member)) { + widget.signal.remove(member); + } else if (member.id != StatusController.ownAddress) { + widget.signal.add(member); + } + } + + _search.value = ""; + _searchController.clear(); + if (!isMobileMode()) { + _searchFocusNode.requestFocus(); + } + } + }, + maxLines: 1, + ), + ), + ], + ), + ), + + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: Watch( + (ctx) => ListView.builder( + itemCount: FriendController.friends.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: defaultSpacing), + itemBuilder: (context, index) { + Friend friend = FriendController.friends.values.elementAt(index); + + if (friend.id == StatusController.ownAddress) { + return const SizedBox(); + } + + return Watch((ctx) { + final search = _search.value; + if (search.isNotEmpty && + !(friend.name.toLowerCase().contains(search.toLowerCase()) || + friend.displayName.value.toLowerCase().contains(search.toLowerCase()))) { + return const SizedBox(); + } + + return Padding( + padding: const EdgeInsets.only(bottom: defaultSpacing), + child: Watch( + (ctx) => Material( + color: widget.signal.contains(friend) ? theme.colorScheme.primary : Colors.transparent, + borderRadius: BorderRadius.circular(defaultSpacing), + child: InkWell( + borderRadius: BorderRadius.circular(defaultSpacing), + onTap: () { + if (widget.initial != null && widget.initial!.contains(friend)) { + return; + } + if (widget.signal.contains(friend)) { + widget.signal.remove(friend); + } else { + widget.signal.add(friend); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: elementSpacing, vertical: elementSpacing), + child: Row( + children: [ + UserAvatar(id: friend.id, size: 35), + horizontalSpacing(defaultSpacing), + Watch((ctx) => Text(friend.displayName.value, style: theme.textTheme.labelLarge)), + ], + ), + ), + ), + ), + ), + ); + }); + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/theme/ui/dialogs/error_window.dart b/lib/theme/ui/dialogs/error_window.dart index 2fd634d7..264dfcbb 100644 --- a/lib/theme/ui/dialogs/error_window.dart +++ b/lib/theme/ui/dialogs/error_window.dart @@ -23,10 +23,8 @@ class ErrorWindow extends StatelessWidget { verticalSpacing(sectionSpacing), FJElevatedButton( onTap: () => Get.back(), - child: Center( - child: Text("ok".tr, style: Get.theme.textTheme.titleMedium), - ), - ) + child: Center(child: Text("ok".tr, style: Get.theme.textTheme.titleMedium)), + ), ], ), ); diff --git a/lib/theme/ui/dialogs/image_preview_window.dart b/lib/theme/ui/dialogs/image_preview_window.dart index 5ebb1b1d..cb240689 100644 --- a/lib/theme/ui/dialogs/image_preview_window.dart +++ b/lib/theme/ui/dialogs/image_preview_window.dart @@ -17,60 +17,63 @@ class ImagePreviewWindow extends StatelessWidget { Widget build(BuildContext context) { return Stack( children: [ - LayoutBuilder(builder: (context, constraints) { - final random = Random(); - final randomOffset = random.nextDouble() * 8 + 5; - final randomHz = random.nextDouble() * 1 + 1; + LayoutBuilder( + builder: (context, constraints) { + final random = Random(); + final randomOffset = random.nextDouble() * 8 + 5; + final randomHz = random.nextDouble() * 1 + 1; - return GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: InteractiveViewer( - maxScale: 5, - child: Center( - child: Animate( - effects: [ - ScaleEffect( - delay: 100.ms, - duration: 500.ms, - begin: const Offset(0, 0), - end: const Offset(1, 1), - alignment: Alignment.center, - curve: const ElasticOutCurve(0.8), - ), - ShakeEffect( - delay: 100.ms, - duration: 400.ms, - hz: randomHz, - offset: Offset(random.nextBool() ? randomOffset : -randomOffset, random.nextBool() ? randomOffset : -randomOffset), - rotation: 0, - curve: Curves.decelerate, - ), - FadeEffect(delay: 100.ms, duration: 250.ms, curve: Curves.easeOut) - ], - child: SizedBox( - height: constraints.maxHeight * 0.6, - child: GestureDetector( - onTap: () => {}, - child: image != null - ? RawImage(image: image) - : url == null - ? XImage(file: file!) - : Image.network(url!), + return GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: InteractiveViewer( + maxScale: 5, + child: Center( + child: Animate( + effects: [ + ScaleEffect( + delay: 100.ms, + duration: 500.ms, + begin: const Offset(0, 0), + end: const Offset(1, 1), + alignment: Alignment.center, + curve: const ElasticOutCurve(0.8), + ), + ShakeEffect( + delay: 100.ms, + duration: 400.ms, + hz: randomHz, + offset: Offset( + random.nextBool() ? randomOffset : -randomOffset, + random.nextBool() ? randomOffset : -randomOffset, + ), + rotation: 0, + curve: Curves.decelerate, + ), + FadeEffect(delay: 100.ms, duration: 250.ms, curve: Curves.easeOut), + ], + child: SizedBox( + height: constraints.maxHeight * 0.6, + child: GestureDetector( + onTap: () => {}, + child: + image != null + ? RawImage(image: image) + : url == null + ? XImage(file: file!) + : Image.network(url!), + ), ), ), ), ), - ), - ); - }), + ); + }, + ), Positioned( top: 0, right: 0, - child: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - ) + child: IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()), + ), ], ); } diff --git a/lib/theme/ui/dialogs/join_space_dialog.dart b/lib/theme/ui/dialogs/join_space_dialog.dart index 6785a69d..4fc00c76 100644 --- a/lib/theme/ui/dialogs/join_space_dialog.dart +++ b/lib/theme/ui/dialogs/join_space_dialog.dart @@ -1,5 +1,5 @@ -import 'package:chat_interface/controller/spaces/space_container.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/services/spaces/space_container.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; @@ -30,22 +30,20 @@ class _JoinSpaceDialogState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ FJElevatedButton( - onTap: () { - Get.find().join(widget.container); - Get.back(); - }, - smallCorners: true, - child: Center( - child: Text("yeah".tr, style: Get.theme.textTheme.labelMedium), - )), + onTap: () { + SpaceController.join(widget.container); + Get.back(); + }, + smallCorners: true, + child: Center(child: Text("yeah".tr, style: Get.theme.textTheme.labelMedium)), + ), FJElevatedButton( - onTap: () => Get.back(), - smallCorners: true, - child: Center( - child: Text("no.got".tr, style: Get.theme.textTheme.labelMedium), - )) + onTap: () => Get.back(), + smallCorners: true, + child: Center(child: Text("no.got".tr, style: Get.theme.textTheme.labelMedium)), + ), ], - ) + ), ], ), ); diff --git a/lib/theme/ui/dialogs/message_info_window.dart b/lib/theme/ui/dialogs/message_info_window.dart index 245d16af..96c24813 100644 --- a/lib/theme/ui/dialogs/message_info_window.dart +++ b/lib/theme/ui/dialogs/message_info_window.dart @@ -1,9 +1,9 @@ import 'dart:convert'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; -import 'package:chat_interface/controller/conversation/member_controller.dart'; -import 'package:chat_interface/controller/conversation/message_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; +import 'package:chat_interface/services/chat/conversation_member.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/profile_button.dart'; import 'package:chat_interface/util/popups.dart'; @@ -12,30 +12,34 @@ import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class MessageInfoWindow extends StatefulWidget { final Message message; final MessageProvider provider; - const MessageInfoWindow({ - super.key, - required this.message, - required this.provider, - }); + const MessageInfoWindow({super.key, required this.message, required this.provider}); @override - State createState() => _ConversationAddWindowState(); + State createState() => _MessageInfoWindowState(); } -class _ConversationAddWindowState extends State { - final messageDeletionLoading = false.obs; +class _MessageInfoWindowState extends State { + final _messageDeletionLoading = signal(false); + + @override + void dispose() { + _messageDeletionLoading.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final provider = widget.provider; Member member; if (provider is ConversationMessageProvider) { - member = Get.find().conversations[provider.conversation.id]!.members[widget.message.senderToken] ?? + member = + ConversationController.conversations[provider.conversation.id]!.members[widget.message.senderToken] ?? Member(LPHAddress("", "removed".tr), widget.message.senderAddress, MemberRole.user); } else { member = Member(LPHAddress("", "removed".tr), widget.message.senderAddress, MemberRole.user); @@ -68,7 +72,6 @@ class _ConversationAddWindowState extends State { Clipboard.setData(ClipboardData(text: widget.message.id)); Get.back(); }, - loading: false.obs, ), verticalSpacing(elementSpacing), ProfileButton( @@ -78,7 +81,6 @@ class _ConversationAddWindowState extends State { Clipboard.setData(ClipboardData(text: member.tokenId.encode())); Get.back(); }, - loading: false.obs, ), verticalSpacing(elementSpacing), ProfileButton( @@ -87,7 +89,6 @@ class _ConversationAddWindowState extends State { onTap: () { showSuccessPopup("success", utf8.decode(base64Decode(widget.message.content))); }, - loading: false.obs, ), verticalSpacing(elementSpacing), ProfileButton( @@ -96,7 +97,6 @@ class _ConversationAddWindowState extends State { icon: Icons.close, label: "close".tr, onTap: () => Get.back(), - loading: false.obs, ), ], ), diff --git a/lib/theme/ui/dialogs/message_options_window.dart b/lib/theme/ui/dialogs/message_options_window.dart index 98f4571a..d83778ca 100644 --- a/lib/theme/ui/dialogs/message_options_window.dart +++ b/lib/theme/ui/dialogs/message_options_window.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/attachment_controller.dart'; import 'package:chat_interface/controller/conversation/message_provider.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; @@ -19,6 +19,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:open_file/open_file.dart'; import 'package:pasteboard/pasteboard.dart'; +import 'package:signals/signals_flutter.dart'; class MessageOptionsWindow extends StatefulWidget { final ContextMenuData data; @@ -41,14 +42,20 @@ class MessageOptionsWindow extends StatefulWidget { } class _ConversationAddWindowState extends State { - final messageDeletionLoading = false.obs; + final _messageDeletionLoading = signal(false); + + @override + void dispose() { + _messageDeletionLoading.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - final friend = Get.find().friends[widget.message.senderAddress]; + final friend = FriendController.friends[widget.message.senderAddress]; return SlidingWindowBase( - lessPadding: true, + padding: defaultSpacing, position: widget.data, maxSize: 250, title: const [], @@ -58,10 +65,7 @@ class _ConversationAddWindowState extends State { // Add extra context menu buttons (copy, etc. (if passed in)) if (widget.extra != null) for (var button in widget.extra!) - Padding( - padding: const EdgeInsets.only(bottom: elementSpacing), - child: button, - ), + Padding(padding: const EdgeInsets.only(bottom: elementSpacing), child: button), if (widget.extra != null) verticalSpacing(elementSpacing), // Render the message info in case there is a message provider @@ -75,7 +79,6 @@ class _ConversationAddWindowState extends State { Get.back(); Get.dialog(MessageInfoWindow(message: widget.message, provider: widget.provider!)); }, - loading: false.obs, ), ), @@ -90,7 +93,6 @@ class _ConversationAddWindowState extends State { Clipboard.setData(ClipboardData(text: widget.message.content)); Get.back(); }, - loading: false.obs, ), ), @@ -119,7 +121,6 @@ class _ConversationAddWindowState extends State { await attachment.file!.saveTo(saveLocation.path); Get.back(); }, - loading: false.obs, ), ), @@ -142,12 +143,13 @@ class _ConversationAddWindowState extends State { unawaited(OpenFile.open(attachment.file!.path)); Get.back(); }, - loading: false.obs, ), ), // Only show the copy button in case it is an attachment (and no mobile cause doesn't work there) - if (widget.message.attachmentsRenderer.length == 1 && widget.message.attachmentsRenderer[0].downloaded.value && isDesktopPlatform()) + if (widget.message.attachmentsRenderer.length == 1 && + widget.message.attachmentsRenderer[0].downloaded.value && + isDesktopPlatform()) Padding( padding: const EdgeInsets.only(bottom: elementSpacing), child: ProfileButton( @@ -158,7 +160,6 @@ class _ConversationAddWindowState extends State { await Pasteboard.writeFiles([widget.message.attachmentsRenderer[0].file!.path]); Get.back(); }, - loading: false.obs, ), ), @@ -170,7 +171,6 @@ class _ConversationAddWindowState extends State { Get.back(); showModal(Profile(friend: friend ?? Friend.unknown(widget.message.senderAddress))); }, - loading: false.obs, ), // Give the user an option to reply in case it's a text message @@ -184,7 +184,6 @@ class _ConversationAddWindowState extends State { MessageSendHelper.addReplyToCurrentDraft(widget.message); Get.back(); }, - loading: false.obs, ), ), @@ -200,11 +199,12 @@ class _ConversationAddWindowState extends State { label: "message.delete".tr, onTap: () async { // Set and check loading state - if (messageDeletionLoading.value) return; - messageDeletionLoading.value = true; + if (_messageDeletionLoading.value) return; + _messageDeletionLoading.value = true; // Check if the message is sent by the current user to ask for file deletions - if (StatusController.ownAddress == widget.message.senderAddress && widget.message.attachments.isNotEmpty) { + if (StatusController.ownAddress == widget.message.senderAddress && + widget.message.attachments.isNotEmpty) { await showConfirmPopup( ConfirmWindow( title: "message.delete.attachments".tr, @@ -216,7 +216,7 @@ class _ConversationAddWindowState extends State { final path = await AttachmentController.getFilePathFor(json["i"]); // Delete the file (also locally in case needed) - await Get.find().deleteFileFromPath( + await AttachmentController.deleteFileFromPath( json["i"], path == null ? null : XFile(path), popup: true, @@ -229,7 +229,7 @@ class _ConversationAddWindowState extends State { // Delete message final result = await widget.message.delete(widget.provider!); - messageDeletionLoading.value = false; + _messageDeletionLoading.value = false; if (result != null) { showErrorPopup("error", result); return; @@ -237,7 +237,7 @@ class _ConversationAddWindowState extends State { Get.back(); }, - loading: messageDeletionLoading, + loading: _messageDeletionLoading, ), ), ], diff --git a/lib/theme/ui/dialogs/profile_picture_window.dart b/lib/theme/ui/dialogs/profile_picture_window.dart index 133feea5..b1a0080f 100644 --- a/lib/theme/ui/dialogs/profile_picture_window.dart +++ b/lib/theme/ui/dialogs/profile_picture_window.dart @@ -1,6 +1,6 @@ import 'dart:ui' as ui; -import 'package:chat_interface/controller/account/profile_picture_helper.dart'; +import 'package:chat_interface/services/chat/profile_picture_helper.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_slider.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; @@ -10,6 +10,7 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:signals/signals_flutter.dart'; class ProfilePictureWindow extends StatefulWidget { final XFile file; @@ -23,17 +24,27 @@ class ProfilePictureWindow extends StatefulWidget { class _ProfilePictureWindowState extends State { double minScale = 0; double maxScale = 0; - final scaleFactor = 1.0.obs; - final moveX = 0.0.obs; - final moveY = 0.0.obs; + late final _scaleFactor = signal(1.0); + late final _moveX = signal(0.0); + late final _moveY = signal(0.0); - final uploading = false.obs; - final _image = Rx(null); + late final _uploading = signal(false); + late final _image = signal(null); @override void initState() { - super.initState(); initImage(); + super.initState(); + } + + @override + void dispose() { + _scaleFactor.dispose(); + _moveX.dispose(); + _moveY.dispose(); + _uploading.dispose(); + _image.dispose(); + super.dispose(); } Future initImage() async { @@ -42,11 +53,11 @@ class _ProfilePictureWindowState extends State { // Calculate the scale factor to fit the image into the window if (image.width < image.height) { - scaleFactor.value = 1.0 / (300.0 / image.width.toDouble()); - maxScale = scaleFactor.value; + _scaleFactor.value = 1.0 / (300.0 / image.width.toDouble()); + maxScale = _scaleFactor.value; } else { - scaleFactor.value = 1.0 / (300.0 / image.height.toDouble()); - maxScale = scaleFactor.value; + _scaleFactor.value = 1.0 / (300.0 / image.height.toDouble()); + maxScale = _scaleFactor.value; } _image.value = image; @@ -55,123 +66,114 @@ class _ProfilePictureWindowState extends State { @override Widget build(BuildContext context) { return DialogBase( - maxWidth: 450, - child: Obx(() { - if (_image.value == null) { - return SizedBox(height: 100, width: 100, child: Center(child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary))); - } - - final scale = scaleFactor.value; - final offset = Offset(moveX.value, moveY.value); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text("settings.data.profile_picture.select".tr, style: Get.theme.textTheme.bodyMedium), - verticalSpacing(sectionSpacing), - Center( - child: ClipOval( - child: SizedBox( - height: 300, - width: 300, - child: RawImage( - fit: BoxFit.none, - scale: scale, - image: _image.value, - alignment: Alignment(offset.dx, offset.dy), - ), + maxWidth: 450, + child: Watch((ctx) { + if (_image.value == null) { + return SizedBox( + height: 100, + width: 100, + child: Center(child: CircularProgressIndicator(color: Get.theme.colorScheme.onPrimary)), + ); + } + + final scale = _scaleFactor.value; + final offset = Offset(_moveX.value, _moveY.value); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text("settings.data.profile_picture.select".tr, style: Get.theme.textTheme.bodyMedium), + verticalSpacing(sectionSpacing), + Center( + child: ClipOval( + child: SizedBox( + height: 300, + width: 300, + child: RawImage( + fit: BoxFit.none, + scale: scale, + image: _image.value, + alignment: Alignment(offset.dx, offset.dy), ), ), ), - verticalSpacing(defaultSpacing), - Row( - children: [ - Text("zoom".tr, style: Get.theme.textTheme.labelMedium), - horizontalSpacing(defaultSpacing), - Expanded( - child: FJSlider( - value: (maxScale - scaleFactor.value) + 0.5, - min: 0.5, - max: maxScale + 0.45, - onChanged: (val) { - scaleFactor.value = (maxScale - val) + 0.5; - }, - ), + ), + verticalSpacing(defaultSpacing), + Row( + children: [ + Text("zoom".tr, style: Get.theme.textTheme.labelMedium), + horizontalSpacing(defaultSpacing), + Expanded( + child: FJSlider( + value: (maxScale - _scaleFactor.value) + 0.5, + min: 0.5, + max: maxScale + 0.45, + onChanged: (val) { + _scaleFactor.value = (maxScale - val) + 0.5; + }, ), - horizontalSpacing(defaultSpacing), - Text(((maxScale - scaleFactor.value) + 0.5).toStringAsFixed(1), style: Get.theme.textTheme.bodyMedium), - ], - ), - Row( - children: [ - Text("x".tr, style: Get.theme.textTheme.labelMedium), - horizontalSpacing(defaultSpacing), - Expanded( - child: FJSlider( - value: moveX.value, - min: -1, - max: 1, - onChanged: (val) => moveX.value = val, - ), - ), - horizontalSpacing(defaultSpacing), - Text(moveX.value.toStringAsFixed(1), style: Get.theme.textTheme.bodyMedium), - ], - ), - Row( - children: [ - Text("y".tr, style: Get.theme.textTheme.labelMedium), - horizontalSpacing(defaultSpacing), - Expanded( - child: FJSlider( - value: moveY.value, - min: -1, - max: 1, - onChanged: (val) => moveY.value = val, + ), + horizontalSpacing(defaultSpacing), + Text(((maxScale - _scaleFactor.value) + 0.5).toStringAsFixed(1), style: Get.theme.textTheme.bodyMedium), + ], + ), + Row( + children: [ + Text("x".tr, style: Get.theme.textTheme.labelMedium), + horizontalSpacing(defaultSpacing), + Expanded(child: FJSlider(value: _moveX.value, min: -1, max: 1, onChanged: (val) => _moveX.value = val)), + horizontalSpacing(defaultSpacing), + Text(_moveX.value.toStringAsFixed(1), style: Get.theme.textTheme.bodyMedium), + ], + ), + Row( + children: [ + Text("y".tr, style: Get.theme.textTheme.labelMedium), + horizontalSpacing(defaultSpacing), + Expanded(child: FJSlider(value: _moveY.value, min: -1, max: 1, onChanged: (val) => _moveY.value = val)), + horizontalSpacing(defaultSpacing), + Text(_moveY.value.toStringAsFixed(1), style: Get.theme.textTheme.bodyMedium), + ], + ), + verticalSpacing(sectionSpacing), + FJElevatedLoadingButton( + loading: _uploading, + onTap: () async { + if (_uploading.value) return; + _uploading.value = true; + + final screenshotController = ScreenshotController(); + final scale = _scaleFactor.value * (300 / 500); + + final image = await screenshotController.captureFromWidget( + SizedBox( + width: 500, + height: 500, + child: RawImage( + fit: BoxFit.none, + scale: scale, + image: _image.value!, + alignment: Alignment(_moveX.value, _moveY.value), ), ), - horizontalSpacing(defaultSpacing), - Text(moveY.value.toStringAsFixed(1), style: Get.theme.textTheme.bodyMedium), - ], - ), - verticalSpacing(sectionSpacing), - FJElevatedLoadingButton( - loading: uploading, - onTap: () async { - if (uploading.value) return; - uploading.value = true; - - final screenshotController = ScreenshotController(); - final scale = scaleFactor.value * (300 / 500); - - final image = await screenshotController.captureFromWidget( - SizedBox( - width: 500, - height: 500, - child: RawImage( - fit: BoxFit.none, - scale: scale, - image: _image.value!, - alignment: Alignment(moveX.value, moveY.value), - ), - ), - ); - final cutFile = XFile("cut-${widget.file.name}"); - final res = await ProfileHelper.uploadProfilePicture(cutFile, widget.file.name, bytes: image); - if (!res) { - uploading.value = false; - sendLog("kinda didn't work"); - return; - } - uploading.value = false; - sendLog("uploaded"); - Get.back(); - }, - label: "select".tr, - ), - ], - ); - })); + ); + final cutFile = XFile("cut-${widget.file.name}"); + final res = await ProfileHelper.uploadProfilePicture(cutFile, widget.file.name, bytes: image); + if (!res) { + _uploading.value = false; + sendLog("kinda didn't work"); + return; + } + _uploading.value = false; + sendLog("uploaded"); + Get.back(); + }, + label: "select".tr, + ), + ], + ); + }), + ); } } diff --git a/lib/theme/ui/dialogs/space_add_window.dart b/lib/theme/ui/dialogs/space_add_window.dart index ceaecaa4..9480316c 100644 --- a/lib/theme/ui/dialogs/space_add_window.dart +++ b/lib/theme/ui/dialogs/space_add_window.dart @@ -1,32 +1,35 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'dart:async'; + +import 'package:chat_interface/controller/account/friend_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/pages/chat/sidebar/friends/friends_page.dart'; import 'package:chat_interface/theme/components/forms/fj_button.dart'; import 'package:chat_interface/theme/components/forms/fj_switch.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import '../../../util/vertical_spacing.dart'; class SpaceAddWindow extends StatefulWidget { - final Offset position; - - const SpaceAddWindow({super.key, required this.position}); + const SpaceAddWindow({super.key}); @override State createState() => _ConversationAddWindowState(); } class _ConversationAddWindowState extends State { - final public = true.obs; - final _conversationLoading = false.obs; + late final _public = signal(true); + late final _conversationLoading = signal(false); final _controller = TextEditingController(); @override void dispose() { _controller.dispose(); + _public.dispose(); + _conversationLoading.dispose(); super.dispose(); } @@ -34,12 +37,10 @@ class _ConversationAddWindowState extends State { Widget build(BuildContext context) { ThemeData theme = Theme.of(context); - if (Get.find().friends.length == 1) { + if (FriendController.friends.length == 1) { return SlidingWindowBase( - title: [ - Text("chat.space.add".tr, style: theme.textTheme.titleMedium), - ], - position: ContextMenuData(widget.position, true, true), + title: [Text("chat.space.add".tr, style: theme.textTheme.titleMedium)], + position: null, child: Column( children: [ Text("no.friends".tr, style: theme.textTheme.bodyMedium), @@ -49,9 +50,7 @@ class _ConversationAddWindowState extends State { Get.back(); showModal(const FriendsPage()); }, - child: Center( - child: Text("open.friends".tr, style: theme.textTheme.labelLarge), - ), + child: Center(child: Text("open.friends".tr, style: theme.textTheme.labelLarge)), ), ], ), @@ -59,17 +58,15 @@ class _ConversationAddWindowState extends State { } return SlidingWindowBase( - title: [ - Text("chat.space.add".tr, style: theme.textTheme.titleMedium), - ], - position: ContextMenuData(widget.position, true, true), + title: [Text("chat.space.add".tr, style: theme.textTheme.titleMedium)], + position: null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ /* verticalSpacing(sectionSpacing), - Obx( - () => FJTextField( + Watch( + (ctx) => FJTextField( controller: _controller, hintText: "Space name".tr, errorText: _errorText.value, @@ -79,12 +76,12 @@ class _ConversationAddWindowState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("Public", style: Get.theme.textTheme.bodyMedium), - Obx( - () => FJSwitch( - value: public.value, + Text("shared".tr, style: Get.theme.textTheme.bodyMedium), + Watch( + (ctx) => FJSwitch( + value: _public.value, onChanged: (p0) { - public.value = p0; + _public.value = p0; }, ), ), @@ -93,12 +90,12 @@ class _ConversationAddWindowState extends State { verticalSpacing(defaultSpacing), FJElevatedLoadingButton( onTap: () async { - Get.find().createSpace(public.value); + unawaited(SpaceController.createSpace(_public.value)); Get.back(); }, label: "create".tr, loading: _conversationLoading, - ) + ), ], ), ); diff --git a/lib/theme/ui/dialogs/upgrade_window.dart b/lib/theme/ui/dialogs/upgrade_window.dart index 0ed49778..6db6e1cc 100644 --- a/lib/theme/ui/dialogs/upgrade_window.dart +++ b/lib/theme/ui/dialogs/upgrade_window.dart @@ -14,11 +14,7 @@ class UpgradeWindow extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Upgrade to a better Liphium.", - style: Get.textTheme.headlineMedium, - textAlign: TextAlign.center, - ), + Text("Upgrade to a better Liphium.", style: Get.textTheme.headlineMedium, textAlign: TextAlign.center), verticalSpacing(sectionSpacing), Text( "Liphium Web has a lot of limitations. If you want a better experience, Liphium is available as a native app on all major platforms.", @@ -87,9 +83,7 @@ class UpgradeWindow extends StatelessWidget { verticalSpacing(defaultSpacing), FJElevatedButton( onTap: () => launchUrlString("https://liphium.com/"), - child: Center( - child: Text("Visit the website", style: Get.textTheme.labelLarge), - ), + child: Center(child: Text("Visit the website", style: Get.textTheme.labelLarge)), ), ], ), diff --git a/lib/theme/ui/dialogs/window_base.dart b/lib/theme/ui/dialogs/window_base.dart index ac4a934c..dc9bd8cc 100644 --- a/lib/theme/ui/dialogs/window_base.dart +++ b/lib/theme/ui/dialogs/window_base.dart @@ -14,15 +14,7 @@ class WindowBase extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - Positioned( - left: position.dx, - top: position.dy, - child: child, - ), - ], - ); + return Stack(children: [Positioned(left: position.dx, top: position.dy, child: child)]); } } @@ -56,12 +48,7 @@ class DialogBase extends StatelessWidget { if (title.isNotEmpty) Padding( padding: const EdgeInsets.all(elementSpacing), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...title, - ], - ), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [...title]), ), if (title.isNotEmpty) verticalSpacing(defaultSpacing), child, @@ -75,11 +62,7 @@ class DialogBase extends StatelessWidget { top: true, right: true, left: true, - padding: EdgeInsets.only( - top: defaultSpacing * 1.5, - right: defaultSpacing * 1.5, - left: defaultSpacing * 1.5, - ), + padding: EdgeInsets.only(top: defaultSpacing * 1.5, right: defaultSpacing * 1.5, left: defaultSpacing * 1.5), child: child, ); } @@ -103,15 +86,14 @@ class DialogBase extends StatelessWidget { delay: 100.ms, duration: 400.ms, hz: randomHz, - offset: Offset(random.nextBool() ? randomOffset : -randomOffset, random.nextBool() ? randomOffset : -randomOffset), + offset: Offset( + random.nextBool() ? randomOffset : -randomOffset, + random.nextBool() ? randomOffset : -randomOffset, + ), rotation: 0, curve: Curves.decelerate, ), - FadeEffect( - delay: 100.ms, - duration: 250.ms, - curve: Curves.easeOut, - ) + FadeEffect(delay: 100.ms, duration: 250.ms, curve: Curves.easeOut), ], target: 1, child: Material( @@ -125,12 +107,7 @@ class DialogBase extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (showTitleDesktop && title.isNotEmpty) - Row( - children: [ - ...title, - ], - ), + if (showTitleDesktop && title.isNotEmpty) Row(children: [...title]), if (showTitleDesktop && title.isNotEmpty) verticalSpacing(defaultSpacing), child, ], @@ -144,8 +121,8 @@ class DialogBase extends StatelessWidget { } class SlidingWindowBase extends StatelessWidget { - final ContextMenuData position; - final bool lessPadding; + final ContextMenuData? position; + final double padding; final Widget child; final List title; final double maxSize; @@ -154,13 +131,29 @@ class SlidingWindowBase extends StatelessWidget { super.key, required this.title, required this.position, - this.lessPadding = false, + this.padding = dialogPadding, required this.child, this.maxSize = 350, }); @override Widget build(BuildContext context) { + // Return only the child in case there is no position data + if (position == null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: elementSpacing), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [...title]), + ), + if (title.isNotEmpty) verticalSpacing(defaultSpacing), + child, + ], + ); + } + // Return without animation on mobile if (isMobileMode()) { return LPHBottomSheet( @@ -170,12 +163,7 @@ class SlidingWindowBase extends StatelessWidget { if (title.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: elementSpacing), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...title, - ], - ), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [...title]), ), if (title.isNotEmpty) verticalSpacing(defaultSpacing), child, @@ -191,17 +179,24 @@ class SlidingWindowBase extends StatelessWidget { return Stack( children: [ Positioned( - left: position.fromLeft ? position.start.dx : null, - right: position.fromLeft ? null : position.start.dx, - top: position.fromTop ? position.start.dy : null, - bottom: position.fromTop ? null : position.start.dy, + left: position!.fromLeft ? position!.start.dx : null, + right: position!.fromLeft ? null : position!.start.dx, + top: position!.fromTop ? position!.start.dy : null, + bottom: position!.fromTop ? null : position!.start.dy, child: Animate( effects: [ - MoveEffect(duration: 400.ms, begin: Offset(0, -100 * (position.fromTop ? 1 : -1)), curve: scaleAnimationCurve), + MoveEffect( + duration: 400.ms, + begin: Offset(0, -100 * (position!.fromTop ? 1 : -1)), + curve: scaleAnimationCurve, + ), ShakeEffect( duration: 350.ms, hz: randomHz, - offset: Offset(random.nextBool() ? randomOffset : -randomOffset, random.nextBool() ? randomOffset : -randomOffset), + offset: Offset( + random.nextBool() ? randomOffset : -randomOffset, + random.nextBool() ? randomOffset : -randomOffset, + ), rotation: 0, curve: Curves.decelerate, ), @@ -214,16 +209,10 @@ class SlidingWindowBase extends StatelessWidget { color: Get.theme.colorScheme.onInverseSurface, borderRadius: BorderRadius.circular(dialogBorderRadius), child: Padding( - padding: EdgeInsets.all(lessPadding ? defaultSpacing : dialogPadding), + padding: EdgeInsets.all(padding), child: Column( mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: title, - ), - if (title.isNotEmpty) verticalSpacing(defaultSpacing), - child, - ], + children: [Row(children: title), if (title.isNotEmpty) verticalSpacing(defaultSpacing), child], ), ), ), @@ -238,10 +227,7 @@ class SlidingWindowBase extends StatelessWidget { class LPHBottomSheet extends StatelessWidget { final Widget child; - const LPHBottomSheet({ - super.key, - required this.child, - }); + const LPHBottomSheet({super.key, required this.child}); @override Widget build(BuildContext context) { @@ -258,15 +244,14 @@ class LPHBottomSheet extends StatelessWidget { ShakeEffect( duration: 400.ms, hz: randomHz, - offset: Offset(random.nextBool() ? randomOffset : -randomOffset, random.nextBool() ? randomOffset : -randomOffset), + offset: Offset( + random.nextBool() ? randomOffset : -randomOffset, + random.nextBool() ? randomOffset : -randomOffset, + ), rotation: 0, curve: Curves.decelerate, ), - ScaleEffect( - duration: 250.ms, - curve: Curves.decelerate, - begin: Offset(0.8, 0.8), - ), + ScaleEffect(duration: 250.ms, curve: Curves.decelerate, begin: Offset(0.8, 0.8)), ], child: Material( color: Get.theme.colorScheme.onInverseSurface, @@ -282,7 +267,10 @@ class LPHBottomSheet extends StatelessWidget { right: sectionSpacing, left: sectionSpacing, top: sectionSpacing, - bottom: Get.mediaQuery.padding.bottom != 0 && GetPlatform.isMobile ? Get.mediaQuery.padding.bottom : sectionSpacing, + bottom: + Get.mediaQuery.padding.bottom != 0 && GetPlatform.isMobile + ? Get.mediaQuery.padding.bottom + : sectionSpacing, ), child: child, ), @@ -336,9 +324,11 @@ class ContextMenuData { } } else { fromLeft = true; - position = above || below ? Offset(position.dx, position.dy) : Offset(position.dx + widgetDimensions.width + defaultSpacing, position.dy); + position = + above || below + ? Offset(position.dx, position.dy) + : Offset(position.dx + widgetDimensions.width + defaultSpacing, position.dy); } - sendLog(fromLeft); return ContextMenuData(position, fromTop, fromLeft); } diff --git a/lib/theme/ui/profile/developer_window.dart b/lib/theme/ui/profile/developer_window.dart index 05a10071..7e655ffc 100644 --- a/lib/theme/ui/profile/developer_window.dart +++ b/lib/theme/ui/profile/developer_window.dart @@ -1,13 +1,17 @@ import 'dart:async'; -import 'package:chat_interface/connection/connection.dart'; -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'package:chat_interface/controller/conversation/message_provider.dart'; +import 'package:chat_interface/controller/conversation/sidebar_controller.dart'; +import 'package:chat_interface/services/chat/vault_versioning_service.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/database/database.dart'; import 'package:chat_interface/main.dart'; import 'package:chat_interface/pages/status/setup/instance_setup.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/profile_button.dart'; +import 'package:chat_interface/util/constants.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; @@ -15,6 +19,7 @@ import 'package:drift/drift.dart' as drift; import 'package:drift_db_viewer/drift_db_viewer.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class DeveloperWindow extends StatefulWidget { const DeveloperWindow({super.key}); @@ -24,21 +29,19 @@ class DeveloperWindow extends StatefulWidget { } class _DeveloperWindowState extends State { - final remoteActionTesting = false.obs; + final _remoteActionTesting = signal(false); /// Perform a remote action test with any instance server Future remoteActionTest(String server) async { - remoteActionTesting.value = true; + _remoteActionTesting.value = true; // Make the post request to the test endpoint final json = await postAddress(server, "/node/actions/send", { "app_tag": appTag, "action": "ping", - "data": { - "echo": "hello world", - } + "data": {"echo": "hello world"}, }); - remoteActionTesting.value = false; + _remoteActionTesting.value = false; // Check if there was an error if (!json["success"]) { @@ -50,12 +53,16 @@ class _DeveloperWindowState extends State { showSuccessPopup("success", json["answer"].toString()); } + @override + void dispose() { + _remoteActionTesting.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return DialogBase( - title: [ - Text("Developer info", style: Get.theme.textTheme.labelLarge), - ], + title: [Text("Developer info", style: Get.theme.textTheme.labelLarge)], child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -66,69 +73,95 @@ class _DeveloperWindowState extends State { verticalSpacing(elementSpacing), Text("Current account: ${StatusController.ownAddress.encode()}", style: Get.textTheme.bodyMedium), verticalSpacing(defaultSpacing), + if (SidebarController.getCurrentProvider() != null) + Padding( + padding: EdgeInsets.all(defaultSpacing), + child: ProfileButton( + icon: Icons.delete, + label: "Load test conversation", + onTap: () async { + final timestamp = await SidebarController.getCurrentProvider()!.getTimestamp(); + for (int i = 0; i <= 50; i++) { + unawaited( + SidebarController.getCurrentProvider()!.sendMessage( + signal(false), + MessageType.text, + [], + "message $i", + "", + timestampInfo: timestamp, + ), + ); + } + }, + ), + ), ProfileButton( icon: Icons.launch, label: 'Local database viewer', onTap: () async { unawaited(Navigator.of(context).push(MaterialPageRoute(builder: (context) => DriftDbViewer(db)))); }, - loading: false.obs, ), verticalSpacing(elementSpacing), ProfileButton( icon: Icons.delete, label: "Delete all conversations (local)", - onTap: () => db.conversation.deleteAll(), - loading: false.obs, - ), - verticalSpacing(elementSpacing), - ProfileButton( - icon: Icons.delete, - label: "Delete all members (local)", - onTap: () => db.member.deleteAll(), - loading: false.obs, + onTap: () { + VaultVersioningService.storeOrUpdateVersion( + VaultVersioningService.vaultTypeGeneral, + Constants.vaultConversationTag, + 0, + ); + db.conversation.deleteAll(); + db.member.deleteAll(); + }, ), verticalSpacing(elementSpacing), ProfileButton( icon: Icons.delete, label: "Delete all friends (local)", - onTap: () => db.friend.deleteAll(), - loading: false.obs, + onTap: () { + VaultVersioningService.storeOrUpdateVersion(VaultVersioningService.vaultTypeFriend, "", 0); + db.friend.deleteAll(); + }, ), verticalSpacing(elementSpacing), - ProfileButton( - icon: Icons.delete, - label: "Delete all messages (local)", - onTap: () => db.message.deleteAll(), - loading: false.obs, - ), + ProfileButton(icon: Icons.delete, label: "Delete all messages (local)", onTap: () => db.message.deleteAll()), verticalSpacing(elementSpacing), ProfileButton( icon: Icons.delete, label: "Delete all library entries (local)", - onTap: () => db.libraryEntry.deleteAll(), - loading: false.obs, + onTap: () { + VaultVersioningService.storeOrUpdateVersion( + VaultVersioningService.vaultTypeGeneral, + Constants.vaultLibraryTag, + 0, + ); + db.libraryEntry.deleteAll(); + }, ), verticalSpacing(elementSpacing), ProfileButton( icon: Icons.hardware, label: "Test remote actions", onTap: () => remoteActionTest(basePath), - loading: remoteActionTesting, + loading: _remoteActionTesting, ), Column( - children: Get.find().friends.values.where((friend) => friend.id.server != basePath).map((friend) { - return Padding( - padding: const EdgeInsets.only(top: elementSpacing), - child: ProfileButton( - icon: Icons.hardware, - label: "Test remote actions (${friend.id.server})", - onTap: () => remoteActionTest(friend.id.server), - loading: remoteActionTesting, - ), - ); - }).toList(), - ) + children: + FriendController.friends.values.where((friend) => friend.id.server != basePath).map((friend) { + return Padding( + padding: const EdgeInsets.only(top: elementSpacing), + child: ProfileButton( + icon: Icons.hardware, + label: "Test remote actions (${friend.id.server})", + onTap: () => remoteActionTest(friend.id.server), + loading: _remoteActionTesting, + ), + ); + }).toList(), + ), ], ), ); diff --git a/lib/theme/ui/profile/own_profile.dart b/lib/theme/ui/profile/own_profile.dart index fa45f6df..7ab1c236 100644 --- a/lib/theme/ui/profile/own_profile.dart +++ b/lib/theme/ui/profile/own_profile.dart @@ -1,24 +1,26 @@ import 'dart:async'; import 'package:chat_interface/controller/current/connection_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/database/database.dart'; import 'package:chat_interface/main.dart'; import 'package:chat_interface/pages/chat/sidebar/friends/friends_page.dart'; import 'package:chat_interface/pages/settings/data/settings_controller.dart'; -import 'package:chat_interface/pages/status/setup/setup_manager.dart'; +import 'package:chat_interface/services/chat/status_service.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/developer_window.dart'; import 'package:chat_interface/theme/ui/profile/profile_button.dart'; import 'package:chat_interface/theme/ui/profile/status_renderer.dart'; import 'package:chat_interface/util/constants.dart'; +import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:drift_db_viewer/drift_db_viewer.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:url_launcher/url_launcher_string.dart'; class OwnProfile extends StatefulWidget { @@ -32,33 +34,35 @@ class OwnProfile extends StatefulWidget { } class _ProfileState extends State { - //* Edit state for buttons - final edit = false.obs; + // Edit state for buttons + final _edit = signal(false); final TextEditingController _status = TextEditingController(); - final statusMessage = "".obs; + final _statusMessage = signal(""); final FocusNode _statusFocus = FocusNode(); // Developer things - final testLoading = false.obs; - final _clicks = 0.obs; + final _testLoading = signal(false); + var _clicks = 0; @override void dispose() { _status.dispose(); _statusFocus.dispose(); + _edit.dispose(); + _testLoading.dispose(); + _statusMessage.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - StatusController controller = Get.find(); ThemeData theme = Theme.of(context); - _status.text = controller.status.value; - statusMessage.value = controller.status.value; + _status.text = StatusController.status.value; + _statusMessage.value = StatusController.status.value; - //* Context menu + // Context menu return SlidingWindowBase( title: const [], position: widget.position, @@ -70,59 +74,55 @@ class _ProfileState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - //* Profile info + // Show display name Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.person, size: 30.0, color: theme.colorScheme.onPrimary), horizontalSpacing(defaultSpacing), Text( - controller.displayName.value, + StatusController.displayName.value, style: theme.textTheme.titleMedium, textHeightBehavior: noTextHeight, ), ], ), - //* Copy button + // Show a button for copying your own name (with secret developer window) LoadingIconButton( - loading: false.obs, onTap: () { - _clicks.value++; - if (_clicks.value > 7) { + _clicks++; + if (_clicks > 7) { Get.dialog(const DeveloperWindow()); } - Clipboard.setData(ClipboardData(text: controller.name.value)); + Clipboard.setData(ClipboardData(text: StatusController.name.value)); }, icon: Icons.copy, - ) + ), ], ), verticalSpacing(defaultSpacing), - //* Status - Obx(() { - if (controller.ownContainer.value != null) { + // Show a button for stopping sharing the current space + Watch((ctx) { + if (StatusController.ownContainer.value != null) { return Padding( padding: const EdgeInsets.only(bottom: elementSpacing), child: ProfileButton( icon: Icons.stop, label: 'profile.stop_sharing'.tr, - onTap: () => controller.stopSharing(), - loading: false.obs, + onTap: () => StatusController.stopSharing(), ), ); } - final spacesController = Get.find(); - if (spacesController.connected.value) { + if (SpaceController.connected.value) { return Padding( padding: const EdgeInsets.only(bottom: elementSpacing), child: ProfileButton( icon: Icons.start, label: 'profile.start_sharing'.tr, - onTap: () => controller.share(spacesController.getContainer()), - loading: false.obs, + onTap: () => StatusController.share(SpaceController.getContainer()), ), ); } else { @@ -130,16 +130,16 @@ class _ProfileState extends State { } }), - //* Current status type + // Current status type RepaintBoundary( - child: GetX(builder: (statusController) { + child: Watch((ctx) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(4, (index) { // Get details Color color = getStatusColor(theme, index); IconData icon = getStatusIcon(index); - final bool selected = statusController.type.value == index; + final bool selected = StatusController.type.value == index; return Padding( padding: const EdgeInsets.only(bottom: elementSpacing), @@ -148,14 +148,19 @@ class _ProfileState extends State { borderRadius: BorderRadius.circular(defaultSpacing), child: InkWell( borderRadius: BorderRadius.circular(defaultSpacing), - onTap: () { - controller.setStatus(type: index, success: () => Get.back()); + onTap: () async { + final error = await StatusService.sendStatus(type: index); + if (error != null) { + showErrorPopup("error", error); + return; + } + Get.back(); }, child: Padding( padding: const EdgeInsets.all(defaultSpacing), child: Row( children: [ - //* Status icon + // Status icon Icon(icon, size: 13.0, color: color), horizontalSpacing(defaultSpacing), Text( @@ -176,23 +181,23 @@ class _ProfileState extends State { }), ), - //* Status message + // Status message Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - //* Profile id + // Profile id Expanded( child: GestureDetector( onTap: () { - edit.value = true; + _edit.value = true; _statusFocus.requestFocus(); }, - child: Obx( - () => Visibility( - visible: edit.value, + child: Watch( + (ctx) => Visibility( + visible: _edit.value, replacement: Text( - controller.status.value == "" ? 'status.message.add'.tr : controller.status.value, + StatusController.status.value == "" ? 'status.message.add'.tr : StatusController.status.value, style: theme.textTheme.bodyMedium, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -200,7 +205,7 @@ class _ProfileState extends State { ), child: TextField( focusNode: _statusFocus, - onChanged: (value) => statusMessage.value = value, + onChanged: (value) => _statusMessage.value = value, decoration: InputDecoration( border: InputBorder.none, hintStyle: theme.textTheme.bodyMedium!, @@ -208,16 +213,18 @@ class _ProfileState extends State { ), style: theme.textTheme.bodyMedium!.copyWith(color: theme.colorScheme.onSurface), - //* Save status - onEditingComplete: () { + // Save status + onEditingComplete: () async { if (_status.text == "") _status.text = ""; - controller.setStatus(message: _status.text); - edit.value = false; + final error = await StatusService.sendStatus(message: _status.text); + if (error != null) { + showErrorPopup("error", error); + return; + } + _edit.value = false; }, - inputFormatters: [ - LengthLimitingTextInputFormatter(70), - ], + inputFormatters: [LengthLimitingTextInputFormatter(70)], controller: _status, ), ), @@ -225,56 +232,59 @@ class _ProfileState extends State { ), ), - //* Close button - Obx( - () => LoadingIconButton( - loading: controller.statusLoading, - onTap: () { - if (controller.status.value == "" && !edit.value) { - edit.value = true; + // Close button + Watch( + (ctx) => LoadingIconButton( + loading: StatusController.statusLoading, + onTap: () async { + if (StatusController.status.value == "" && !_edit.value) { + _edit.value = true; _status.text = ""; _statusFocus.requestFocus(); return; } - if (!edit.value) { - controller.setStatus(message: ""); + if (!_edit.value) { + final error = await StatusService.sendStatus(message: ""); + if (error != null) { + showErrorPopup("error", error); + return; + } _status.text = ""; return; } - edit.value = false; + final error = await StatusService.sendStatus(message: _status.text); + if (error != null) { + showErrorPopup("error", error); + return; + } + _edit.value = false; _statusFocus.unfocus(); - controller.setStatus(message: _status.text); }, - icon: statusMessage.value == "" - ? Icons.add - : edit.value + icon: + _statusMessage.value == "" + ? Icons.add + : _edit.value ? Icons.done : Icons.close, color: theme.colorScheme.onPrimary, ), - ) + ), ], ), verticalSpacing(defaultSpacing), - //* Profile settings + // Profile settings ProfileButton( icon: Icons.settings, label: 'profile.settings'.tr, onTap: () => SettingController.openSettingsPage(), - loading: false.obs, ), verticalSpacing(elementSpacing), - //* Friends page - ProfileButton( - icon: Icons.group, - label: 'profile.friends'.tr, - onTap: () => showModal(const FriendsPage()), - loading: false.obs, - ), + // Friends page + ProfileButton(icon: Icons.group, label: 'profile.friends'.tr, onTap: () => showModal(const FriendsPage())), verticalSpacing(elementSpacing), // For debug only database viewer @@ -285,11 +295,11 @@ class _ProfileState extends State { icon: Icons.hardware, label: 'profile.test'.tr, onTap: () async { - testLoading.value = true; + _testLoading.value = true; unawaited(Navigator.of(context).push(MaterialPageRoute(builder: (context) => DriftDbViewer(db)))); - testLoading.value = false; + _testLoading.value = false; }, - loading: testLoading, + loading: _testLoading, ), ), @@ -301,19 +311,14 @@ class _ProfileState extends State { icon: Icons.restart_alt, label: 'profile.retry'.tr, onTap: () async { - Get.find().restart(); + ConnectionController.restart(); }, - loading: testLoading, + loading: _testLoading, ), ), // Help & resources button - ProfileButton( - icon: Icons.launch, - label: 'help'.tr, - onTap: () => launchUrlString(Constants.docsBase), - loading: false.obs, - ), + ProfileButton(icon: Icons.launch, label: 'help'.tr, onTap: () => launchUrlString(Constants.docsBase)), ], ), ); diff --git a/lib/theme/ui/profile/own_profile_mobile.dart b/lib/theme/ui/profile/own_profile_mobile.dart index cd3f4d63..80472393 100644 --- a/lib/theme/ui/profile/own_profile_mobile.dart +++ b/lib/theme/ui/profile/own_profile_mobile.dart @@ -1,16 +1,19 @@ -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; import 'package:chat_interface/controller/current/status_controller.dart'; import 'package:chat_interface/pages/settings/account/data_settings.dart'; +import 'package:chat_interface/services/chat/status_service.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/theme/components/forms/lph_action_fields.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; import 'package:chat_interface/theme/ui/profile/developer_window.dart'; import 'package:chat_interface/theme/ui/profile/profile_button.dart'; import 'package:chat_interface/theme/ui/profile/status_renderer.dart'; +import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class OwnProfileMobile extends StatefulWidget { const OwnProfileMobile({super.key}); @@ -20,32 +23,13 @@ class OwnProfileMobile extends StatefulWidget { } class _OwnProfileMobileState extends State { - //* Edit state for buttons - final edit = false.obs; - - final TextEditingController _status = TextEditingController(); - final statusMessage = "".obs; - final FocusNode _statusFocus = FocusNode(); - // Developer things - final testLoading = false.obs; - final _clicks = 0.obs; - - @override - void dispose() { - _status.dispose(); - _statusFocus.dispose(); - super.dispose(); - } + var _clicks = 0; @override Widget build(BuildContext context) { - StatusController controller = Get.find(); ThemeData theme = Theme.of(context); - _status.text = controller.status.value; - statusMessage.value = controller.status.value; - return DevicePadding( top: true, right: true, @@ -55,10 +39,7 @@ class _OwnProfileMobileState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "About your profile", - style: Get.textTheme.titleMedium, - ), + Text("About your profile", style: Get.textTheme.titleMedium), verticalSpacing(defaultSpacing * 1.5), SizedBox( @@ -77,19 +58,12 @@ class _OwnProfileMobileState extends State { SizedBox( width: 60, height: 60, - child: Stack( - children: [ - UserAvatar( - id: StatusController.ownAddress, - size: 60, - ), - ], - ), + child: Stack(children: [UserAvatar(id: StatusController.ownAddress, size: 60)]), ), horizontalSpacing(sectionSpacing), Expanded( child: Text( - controller.name.value, + StatusController.name.value, overflow: TextOverflow.ellipsis, style: Get.theme.textTheme.headlineMedium, ), @@ -104,11 +78,11 @@ class _OwnProfileMobileState extends State { children: [ LoadingIconButton( onTap: () { - _clicks.value++; - if (_clicks.value > 7) { + _clicks++; + if (_clicks > 7) { showModal(const DeveloperWindow()); } - Clipboard.setData(ClipboardData(text: controller.name.value)); + Clipboard.setData(ClipboardData(text: StatusController.name.value)); }, background: true, backgroundColor: Get.theme.colorScheme.primary, @@ -134,28 +108,25 @@ class _OwnProfileMobileState extends State { verticalSpacing(defaultSpacing), //* Status - Obx(() { - if (controller.ownContainer.value != null) { + Watch((ctx) { + if (StatusController.ownContainer.value != null) { return Padding( padding: const EdgeInsets.only(bottom: elementSpacing), child: ProfileButton( icon: Icons.stop, label: 'profile.stop_sharing'.tr, - onTap: () => controller.stopSharing(), - loading: false.obs, + onTap: () => StatusController.stopSharing(), ), ); } - final spacesController = Get.find(); - if (spacesController.connected.value) { + if (SpaceController.connected.value) { return Padding( padding: const EdgeInsets.only(bottom: elementSpacing), child: ProfileButton( icon: Icons.start, label: 'profile.start_sharing'.tr, - onTap: () => controller.share(spacesController.getContainer()), - loading: false.obs, + onTap: () => StatusController.share(SpaceController.getContainer()), ), ); } else { @@ -168,14 +139,14 @@ class _OwnProfileMobileState extends State { Text("Set your status", style: Get.textTheme.titleMedium), verticalSpacing(defaultSpacing * 1.5), RepaintBoundary( - child: GetX(builder: (statusController) { + child: Watch((ctx) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(4, (index) { // Get details Color color = getStatusColor(theme, index); IconData icon = getStatusIcon(index); - final bool selected = statusController.type.value == index; + final bool selected = StatusController.type.value == index; return Padding( padding: const EdgeInsets.only(bottom: defaultSpacing), @@ -184,8 +155,13 @@ class _OwnProfileMobileState extends State { borderRadius: BorderRadius.circular(defaultSpacing), child: InkWell( borderRadius: BorderRadius.circular(defaultSpacing), - onTap: () { - controller.setStatus(type: index, success: () => Get.back()); + onTap: () async { + final error = await StatusService.sendStatus(type: index); + if (error != null) { + showErrorPopup("error", error); + return; + } + Get.back(); }, child: Padding( padding: const EdgeInsets.all(defaultSpacing), @@ -211,90 +187,6 @@ class _OwnProfileMobileState extends State { ); }), ), - - //* Status message - if (!isMobileMode()) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - //* Profile id - Expanded( - child: GestureDetector( - onTap: () { - edit.value = true; - _statusFocus.requestFocus(); - }, - child: Obx( - () => Visibility( - visible: edit.value, - replacement: Text( - controller.status.value == "" ? 'status.message.add'.tr : controller.status.value, - style: theme.textTheme.bodyMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textHeightBehavior: noTextHeight, - ), - child: TextField( - focusNode: _statusFocus, - onChanged: (value) => statusMessage.value = value, - decoration: InputDecoration( - border: InputBorder.none, - hintStyle: theme.textTheme.bodyMedium!, - hintText: 'status.message'.tr, - ), - style: theme.textTheme.bodyMedium!.copyWith(color: theme.colorScheme.onSurface), - - //* Save status - onEditingComplete: () { - if (_status.text == "") _status.text = ""; - controller.setStatus(message: _status.text); - edit.value = false; - }, - - inputFormatters: [ - LengthLimitingTextInputFormatter(70), - ], - controller: _status, - ), - ), - ), - ), - ), - - //* Close button - if (!isMobileMode()) - Obx( - () => LoadingIconButton( - loading: controller.statusLoading, - onTap: () { - if (controller.status.value == "" && !edit.value) { - edit.value = true; - _status.text = ""; - _statusFocus.requestFocus(); - return; - } - - if (!edit.value) { - controller.setStatus(message: ""); - _status.text = ""; - return; - } - - edit.value = false; - _statusFocus.unfocus(); - controller.setStatus(message: _status.text); - }, - icon: statusMessage.value == "" - ? Icons.add - : edit.value - ? Icons.done - : Icons.close, - color: theme.colorScheme.onPrimary, - ), - ) - ], - ), ], ), ); diff --git a/lib/theme/ui/profile/profile.dart b/lib/theme/ui/profile/profile.dart index b7913a58..e05313c0 100644 --- a/lib/theme/ui/profile/profile.dart +++ b/lib/theme/ui/profile/profile.dart @@ -1,75 +1,90 @@ -import 'package:chat_interface/controller/account/friends/friend_controller.dart'; +import 'dart:async'; + +import 'package:chat_interface/controller/account/friend_controller.dart'; import 'package:chat_interface/controller/conversation/conversation_controller.dart'; import 'package:chat_interface/controller/conversation/message_controller.dart'; -import 'package:chat_interface/controller/spaces/spaces_controller.dart'; +import 'package:chat_interface/controller/spaces/space_controller.dart'; +import 'package:chat_interface/services/chat/conversation_message_provider.dart'; +import 'package:chat_interface/services/chat/conversation_service.dart'; import 'package:chat_interface/theme/components/forms/icon_button.dart'; import 'package:chat_interface/theme/components/user_renderer.dart'; import 'package:chat_interface/theme/ui/dialogs/confirm_window.dart'; import 'package:chat_interface/theme/ui/dialogs/window_base.dart'; import 'package:chat_interface/theme/ui/profile/profile_button.dart'; +import 'package:chat_interface/util/dispose_hook.dart'; import 'package:chat_interface/util/popups.dart'; import 'package:chat_interface/util/vertical_spacing.dart'; import 'package:chat_interface/util/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; class ProfileDefaults { - static Function(Friend, RxBool) deleteAction = (Friend friend, RxBool loading) async { + static Function(Friend, Signal) deleteAction = (Friend friend, Signal loading) async { // Show a confirm popup - final result = await showConfirmPopup(ConfirmWindow( - title: "friends.remove.confirm".tr, - text: "friends.remove.desc".tr, - )); + final result = await showConfirmPopup( + ConfirmWindow(title: "friends.remove.confirm".tr, text: "friends.remove.desc".tr), + ); if (!result) { return; } - await friend.remove(loading); - Get.back(); + // Remove the friend + loading.value = true; + final error = await friend.remove(); + if (error != null) { + showErrorPopup("error", error); + } else { + Get.back(); + } + loading.value = false; }; - static Function(Friend, RxBool) openAction = (Friend friend, RxBool loading) async { + static Function(Friend, Signal) openAction = (Friend friend, Signal loading) async { loading.value = true; - await openDirectMessage(friend); + final (conv, error) = await ConversationService.openDirectMessage(friend); + if (conv != null) { + unawaited(MessageController.openConversation(conv)); + Get.back(); + } + if (error != null) { + showErrorPopup("error", error); + } loading.value = false; - Get.back(); }; - static List buildDefaultActions(Friend friend) { - final removeLoading = false.obs; + static List buildDefaultActions(Friend friend, {bool messageAction = true}) { + final removeLoading = signal(false); if (friend.unknown) { - return [ - ProfileAction(icon: Icons.person_add, category: true, label: 'friends.add'.tr, loading: false.obs, onTap: (f, l) => {}), - ]; + return [ProfileAction(icon: Icons.person_add, category: true, label: 'friends.add'.tr, onTap: (f, l) => {})]; } return [ - ProfileAction( - category: true, - icon: Icons.message, - label: 'friends.message'.tr, - onTap: openAction, - loading: friend.openConversationLoading, - ), - if (Get.find().inSpace.value) + if (messageAction) + ProfileAction( + category: true, + icon: Icons.message, + label: 'friends.message'.tr, + onTap: openAction, + loading: friend.openConversationLoading, + ), + if (SpaceController.connected.value) ProfileAction( icon: Icons.forward_to_inbox, label: 'friends.invite_to_space'.tr, - loading: false.obs, + loading: signal(false), onTap: (friend, l) { - final controller = Get.find(); - // Check if there even is a conversation with the guy - final conversation = controller.conversations.values.toList().firstWhereOrNull( - (c) => c.members.values.any((m) => m.address == friend.id), - ); + final conversation = ConversationController.conversations.values.toList().firstWhereOrNull( + (c) => c.members.values.any((m) => m.address == friend.id), + ); if (conversation == null) { showErrorPopup("error", "profile.conversation_not_found".tr); return; } - Get.find().inviteToCall(ConversationMessageProvider(conversation)); + SpaceController.inviteToCall(ConversationMessageProvider(conversation)); Get.back(); }, ), @@ -89,24 +104,31 @@ class ProfileDefaults { class ProfileAction { final IconData icon; final bool category; - final RxBool loading; + final Signal? loading; final String label; final Color? color; final Color? iconColor; - final Function(Friend, RxBool) onTap; + final Function(Friend, Signal) onTap; - const ProfileAction( - {required this.icon, required this.label, required this.loading, required this.onTap, this.category = false, this.color, this.iconColor}); + const ProfileAction({ + required this.icon, + required this.label, + this.loading, + required this.onTap, + this.category = false, + this.color, + this.iconColor, + }); } class Profile extends StatefulWidget { final bool leftAligned; - final Offset? position; + final ContextMenuData? data; final Friend friend; final int size; final List Function(Friend)? actions; - const Profile({super.key, this.position, required this.friend, this.size = 300, this.leftAligned = true, this.actions}); + const Profile({super.key, this.data, required this.friend, this.size = 350, this.leftAligned = true, this.actions}); @override State createState() => _ProfileState(); @@ -114,36 +136,27 @@ class Profile extends StatefulWidget { class _ProfileState extends State { //* Loading state for buttons - final removeLoading = false.obs; final List actions = []; @override Widget build(BuildContext context) { actions.clear(); if (widget.actions == null) { - if (widget.friend.unknown) { - } else { - actions.addAll(ProfileDefaults.buildDefaultActions(widget.friend)); - } + actions.addAll(ProfileDefaults.buildDefaultActions(widget.friend)); } else { actions.addAll(widget.actions!(widget.friend)); } //* Context menu - if (widget.position != null) { + if (widget.data != null) { return SlidingWindowBase( title: const [], - lessPadding: true, - position: ContextMenuData(widget.position!, true, widget.leftAligned), + position: widget.data, maxSize: widget.size.toDouble(), child: buildProfile(), ); } else { - return DialogBase( - title: const [], - maxWidth: widget.size.toDouble(), - child: buildProfile(), - ); + return DialogBase(title: const [], maxWidth: widget.size.toDouble(), child: buildProfile()); } } @@ -174,14 +187,8 @@ class _ProfileState extends State { padding: const EdgeInsets.only(left: defaultSpacing), child: Tooltip( waitDuration: const Duration(milliseconds: 500), - message: "friends.different_town".trParams({ - "town": widget.friend.id.server, - }), - child: Icon( - Icons.sensors, - color: Get.theme.colorScheme.onPrimary, - size: 21, - ), + message: "friends.different_town".trParams({"town": widget.friend.id.server}), + child: Icon(Icons.sensors, color: Get.theme.colorScheme.onPrimary, size: 21), ), ), ], @@ -190,38 +197,33 @@ class _ProfileState extends State { // Start space button LoadingIconButton( - loading: false.obs, onTap: () { - final controller = Get.find(); - // Check if there even is a conversation with the guy - final conversation = controller.conversations.values.toList().firstWhereOrNull( - (c) => c.members.values.any((m) => m.address == widget.friend.id), - ); + final conversation = ConversationController.conversations.values.toList().firstWhereOrNull( + (c) => c.members.values.any((m) => m.address == widget.friend.id), + ); if (conversation == null) { showErrorPopup("error", "profile.conversation_not_found".tr); return; } // Make sure to invite the guy in case the current user is in a space - if (Get.find().inSpace.value) { - Get.find().inviteToCall(ConversationMessageProvider(conversation)); + if (SpaceController.connected.value) { + SpaceController.inviteToCall(ConversationMessageProvider(conversation)); } else { - Get.find().createAndConnect(ConversationMessageProvider(conversation)); + SpaceController.createAndConnect(ConversationMessageProvider(conversation)); } Get.back(); }, - icon: Get.find().inSpace.value ? Icons.forward_to_inbox : Icons.rocket_launch, - ) + icon: SpaceController.connected.value ? Icons.forward_to_inbox : Icons.rocket_launch, + ), ], ), - Obx( - () => widget.friend.status.value != "" - ? Text( - widget.friend.status.value, - style: Get.theme.textTheme.bodyMedium, - ) - : const SizedBox(), + Watch( + (ctx) => + widget.friend.status.value != "" + ? Text(widget.friend.status.value, style: Get.theme.textTheme.bodyMedium) + : const SizedBox(), ), verticalSpacing(defaultSpacing), ListView.builder( @@ -229,22 +231,38 @@ class _ProfileState extends State { itemCount: actions.length, itemBuilder: (context, index) { ProfileAction action = actions[index]; - return Padding( + + // Create a function that takes in a loading signal to create the button + button(loading) => Padding( padding: EdgeInsets.only( - top: index == 0 - ? 0 - : action.category - ? defaultSpacing - : elementSpacing), + top: + index == 0 + ? 0 + : action.category + ? defaultSpacing + : elementSpacing, + ), child: ProfileButton( icon: action.icon, label: action.label, - onTap: () => action.onTap.call(widget.friend, action.loading), + onTap: () => action.onTap.call(widget.friend, loading), loading: action.loading, color: action.color, iconColor: action.iconColor, ), ); + + // Make sure to manually add the loading state in case not there + if (action.loading == null) { + return SignalHook( + value: false, + builder: (loading) { + return button(loading); + }, + ); + } + + return button(action.loading); }, ), ], diff --git a/lib/theme/ui/profile/profile_button.dart b/lib/theme/ui/profile/profile_button.dart index f93d7e54..82c41a98 100644 --- a/lib/theme/ui/profile/profile_button.dart +++ b/lib/theme/ui/profile/profile_button.dart @@ -1,22 +1,43 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:signals/signals_flutter.dart'; import '../../../util/vertical_spacing.dart'; -class ProfileButton extends StatelessWidget { +class ProfileButton extends StatefulWidget { final IconData icon; final String label; final Function() onTap; final Color? color; final Color? iconColor; - final RxBool loading; + final Signal? loading; - const ProfileButton({super.key, required this.icon, required this.label, required this.onTap, required this.loading, this.color, this.iconColor}); + const ProfileButton({ + super.key, + required this.icon, + required this.label, + required this.onTap, + this.loading, + this.color, + this.iconColor, + }); + + @override + State createState() => _ProfileButtonState(); +} + +class _ProfileButtonState extends State with SignalsMixin { + late final Signal _loading; + + @override + void initState() { + _loading = widget.loading ?? createSignal(false); + super.initState(); + } @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); - Color backgroundColor = (color ?? theme.colorScheme.primary).withAlpha(150); + Color backgroundColor = (widget.color ?? theme.colorScheme.primary).withAlpha(150); return Material( borderRadius: BorderRadius.circular(defaultSpacing), @@ -26,7 +47,7 @@ class ProfileButton extends StatelessWidget { hoverColor: backgroundColor, //* Button - onTap: () => loading.value ? null : onTap(), + onTap: () => _loading.value ? null : widget.onTap(), //* Button content child: Padding( @@ -34,29 +55,32 @@ class ProfileButton extends StatelessWidget { child: Row( children: [ //* Loading indicator - Obx(() => loading.value - ? SizedBox( - width: 25, - height: 25, - child: Padding( - padding: const EdgeInsets.all(defaultSpacing * 0.25), - child: CircularProgressIndicator( - strokeWidth: 2, - color: iconColor ?? theme.colorScheme.onPrimary, - ), - ), - ) - : Icon(icon, size: 25, color: iconColor ?? theme.colorScheme.onPrimary)), + Watch( + (ctx) => + _loading.value + ? SizedBox( + width: 25, + height: 25, + child: Padding( + padding: const EdgeInsets.all(defaultSpacing * 0.25), + child: CircularProgressIndicator( + strokeWidth: 2, + color: widget.iconColor ?? theme.colorScheme.onPrimary, + ), + ), + ) + : Icon(widget.icon, size: 25, color: widget.iconColor ?? theme.colorScheme.onPrimary), + ), //* Label horizontalSpacing(defaultSpacing), Flexible( child: Text( - label, + widget.label, style: theme.textTheme.bodyMedium!.copyWith(color: theme.colorScheme.onSurface), overflow: TextOverflow.ellipsis, ), - ) + ), ], ), ), diff --git a/lib/theme/ui/profile/status_renderer.dart b/lib/theme/ui/profile/status_renderer.dart index 58c46abe..21ba310c 100644 --- a/lib/theme/ui/profile/status_renderer.dart +++ b/lib/theme/ui/profile/status_renderer.dart @@ -15,38 +15,26 @@ class StatusRenderer extends StatelessWidget { final IconData icon = getStatusIcon(status); if (!text) { - return Icon( - icon, - color: color, - size: 16, - ); + return Icon(icon, color: color, size: 16); } return Container( - decoration: BoxDecoration( - color: color.withAlpha(100), - borderRadius: BorderRadius.circular(50), - ), - padding: EdgeInsets.symmetric(horizontal: elementSpacing, vertical: text ? elementSpacing * 0.5 : elementSpacing), - child: Row( - children: [ - Icon( - icon, - color: color, - size: 13, - ), - if (text) - Padding( - padding: const EdgeInsets.only(left: elementSpacing, right: elementSpacing * 0.5), - child: Text( - "status.${status.toString().toLowerCase()}".tr, - style: theme.textTheme.bodySmall!.copyWith( - color: theme.colorScheme.onSurface, - ), - ), + decoration: BoxDecoration(color: color.withAlpha(100), borderRadius: BorderRadius.circular(50)), + padding: EdgeInsets.symmetric(horizontal: elementSpacing, vertical: text ? elementSpacing * 0.5 : elementSpacing), + child: Row( + children: [ + Icon(icon, color: color, size: 13), + if (text) + Padding( + padding: const EdgeInsets.only(left: elementSpacing, right: elementSpacing * 0.5), + child: Text( + "status.${status.toString().toLowerCase()}".tr, + style: theme.textTheme.bodySmall!.copyWith(color: theme.colorScheme.onSurface), ), - ], - )); + ), + ], + ), + ); } } diff --git a/lib/theme/ui/text_renderer/impl/formatting_patterns.dart b/lib/theme/ui/text_renderer/impl/formatting_patterns.dart deleted file mode 100644 index 249f645d..00000000 --- a/lib/theme/ui/text_renderer/impl/formatting_patterns.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:chat_interface/theme/ui/text_renderer/text_pattern_manager.dart'; -import 'package:chat_interface/util/vertical_spacing.dart'; -import 'package:flutter/material.dart'; - -class BoldPattern extends TextPattern { - BoldPattern() : super('**'); - - @override - TextStyle process(TextStyle style) { - return style.copyWith(fontWeight: FontWeight.bold); - } -} - -class ItalicPattern extends TextPattern { - ItalicPattern() : super('*'); - - @override - TextStyle process(TextStyle style) { - return style.copyWith(fontStyle: FontStyle.italic); - } -} - -class UnderlinePattern extends TextPattern { - UnderlinePattern() : super('_'); - - @override - TextStyle process(TextStyle style) { - return style.copyWith( - decoration: TextDecoration.underline, - decorationThickness: 4, - decorationColor: style.color, - ); - } -} - -class StrokePattern extends TextPattern { - StrokePattern() : super('~'); - - @override - TextStyle process(TextStyle style) { - return style.copyWith( - decoration: TextDecoration.lineThrough, - decorationThickness: 4, - decorationColor: style.color, - ); - } -} - -class TrollPattern extends TextPattern { - TrollPattern() : super('---'); - - @override - TextStyle process(TextStyle style) { - return style.copyWith( - letterSpacing: defaultSpacing * 0.5, - ); - } -} diff --git a/lib/theme/ui/text_renderer/text_pattern_manager.dart b/lib/theme/ui/text_renderer/text_pattern_manager.dart deleted file mode 100644 index 92d0476b..00000000 --- a/lib/theme/ui/text_renderer/text_pattern_manager.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:chat_interface/util/logging_framework.dart'; -import 'package:flutter/material.dart'; - -import 'impl/formatting_patterns.dart'; - -final TextPatternManager textPatternManager = TextPatternManager(); - -class TextPatternManager { - late final List patterns; - - TextPatternManager() { - patterns = []; - - // Add patterns - patterns.add(BoldPattern()); - patterns.add(ItalicPattern()); - patterns.add(UnderlinePattern()); - patterns.add(StrokePattern()); - patterns.add(TrollPattern()); - } - - List process(String text, TextStyle style, {bool renderPatterns = false}) { - List spans = []; - - // Scan text for patterns - Map> patternMap = {}; - for (TextPattern pattern in patterns) { - List indices = pattern.scan(text); - for (int index in indices) { - if (!patternMap.containsKey(index)) { - patternMap[index] = []; - } - patternMap[index]!.add(pattern); - } - } - - // Sort patterns - patternMap = Map.fromEntries(patternMap.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); - - // Process text - int lastIndex = 0; - int lastLength = 0; - int mapIndex = 0; - Map patternState = {}; - - while (lastIndex != text.length) { - // Grab current index - int index; - int lengthAfter = 0; - if (mapIndex >= patternMap.length) { - index = text.length; - } else { - index = patternMap.keys.elementAt(mapIndex); - if (mapIndex != patternMap.length - 1) { - lengthAfter = patternMap[patternMap.keys.elementAt(mapIndex + 1)]!.first.pattern.length; - } - } - - // Build style - TextStyle currentStyle = style; - int length = 0; - for (TextPattern pattern in patternState.keys) { - if (patternState[pattern]!) { - currentStyle = pattern.process(currentStyle); - if (pattern.pattern.length > length) { - length = pattern.pattern.length; - } - } - } - - // Check if span is nessecary - if (lastIndex != index) { - if (currentStyle == style && !renderPatterns) { - spans.add(ProcessedText(text.substring(lastIndex + lastLength, index - lengthAfter), currentStyle)); - } else { - spans.add(ProcessedText(text.substring(lastIndex, index), currentStyle)); - } - } - - // Check for patterns - if (patternMap.containsKey(index)) { - for (TextPattern pattern in patternMap[index]!) { - if (patternState.containsKey(pattern)) { - patternState[pattern] = !patternState[pattern]!; - } else { - patternState[pattern] = true; - } - } - } - - lastIndex = index; - lastLength = length; - mapIndex++; - } - - return spans; - } -} - -class ProcessedText { - final String text; - final TextStyle style; - - ProcessedText(this.text, this.style); -} - -abstract class TextPattern { - final String pattern; - - TextPattern(this.pattern); - - // scan returns a list of indices where the pattern is found in the text - List scan(String text) { - sendLog(pattern); - List indices = []; - - int length = 0; - bool enable = false; - while (length < text.length) { - int index = text.indexOf(pattern, length); - if (index == -1) { - break; - } - - // Prevent patterns right next to each other - if (index != length) { - indices.add(index + (!enable ? pattern.length : 0)); - enable = !enable; - } else { - sendLog("Removed double pattern $index $length"); - indices.remove(index + (enable ? pattern.length : 0) - 1); - } - - length = index + 1; - } - - return indices; - } - - TextStyle process(TextStyle style); -} diff --git a/lib/theme/ui/text_renderer/text_renderer.dart b/lib/theme/ui/text_renderer/text_renderer.dart deleted file mode 100644 index 5b6715e0..00000000 --- a/lib/theme/ui/text_renderer/text_renderer.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:chat_interface/theme/ui/text_renderer/text_pattern_manager.dart'; -import 'package:flutter/material.dart'; - -class TextRenderer extends StatelessWidget { - final String text; - final TextStyle? style; - - const TextRenderer({super.key, required this.text, this.style}); - - @override - Widget build(BuildContext context) { - ThemeData theme = Theme.of(context); - - // Build text - List processed = textPatternManager.process(text, style ?? theme.textTheme.bodyMedium!, renderPatterns: false); - List spans = []; - for (ProcessedText span in processed) { - spans.add(TextSpan(text: span.text, style: span.style)); - } - - // Render text - return Text.rich(TextSpan(children: spans)); - } -} diff --git a/lib/translations/chat.dart b/lib/translations/chat.dart index 5a27b772..5ded9610 100644 --- a/lib/translations/chat.dart +++ b/lib/translations/chat.dart @@ -3,180 +3,197 @@ import 'package:get/get.dart'; class ChatPageTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // App - 'app.title': 'The chat app', - 'app.welcome': 'Thanks for joining me on this journey!', - 'app.build': 'Current version: @build', - - // Profile - 'status.0': 'Offline', - 'status.1': 'Online', - 'status.2': 'Away', - 'status.3': 'Busy', - 'status.message': 'Status message', - 'status.message.add': 'Add a status message', - 'profile.stop_sharing': "Stop sharing space", - 'profile.start_sharing': 'Start sharing space', - 'profile.settings': 'Settings', - 'profile.friends': 'Friends', - 'profile.files': 'Files', - 'profile.test': "Test something (DON'T CLICK)", - 'profile.retry': 'Local restart', + //* English US + 'en_US': { + // App + 'app.title': 'Liphium', + 'app.welcome.1': 'Welcome! Your chats are on the left, what are you waiting for?', + 'app.welcome.2': 'Is it funny that I had the idea for this with Lithiuman?', + 'app.welcome.3': 'And no, Liphium doesn\'t go into batteries!', + 'app.welcome.4': 'Welcome to your town! Yes, that\'s really how we call it.', + 'app.welcome.5': 'Have you changed the amount of dots yet?', + 'app.welcome.6': 'Please don\'t make your audio bitrate too high!!', + 'app.welcome.7': 'We\'ve got nothing to hide. Or do we?', + 'app.welcome.8': 'If we ever make an AI assistant we should call it Liph...', + 'app.welcome.9': 'Why is the logo in pixel art? I don\'t know either.', + 'app.welcome.10': 'YOU THINK THIS IS IMPORTANT? Well, it isn\'t...', + 'app.welcome.11': 'How many GIFs does your library contain, my slayer?', + 'app.welcome.12': 'Tabletop exists because of an anime card game...', + 'app.welcome.13': 'Why is this called a town? Because you\'re in the Liphiuverse.', + 'app.welcome.14': 'Why are you weak? Have you considered donating?', + 'app.welcome.15': 'For your failure there is only one explanation: No donation to Liphium.', + 'app.welcome.16': 'We have Squares? When do we get Circles?', + 'app.welcome.17': 'When you connect to Spaces, you use libspaceship to connect to space-station.', + 'app.welcome.18': 'A very friendly reminder to donate to Liphium :D ... Or is it friendly??', + 'app.welcome.19': ':DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD', + 'app.welcome.20': '.___________. <- my reaction when you haven\'t donated.', + 'app.build': 'Current version: @build', - // Friends - 'friends': 'Friends', - 'friends.placeholder': 'Search friends', - 'friends.different_town': 'Lives in a different town than you (@town).', - 'friends.remove': 'Remove friend', - 'friends.remove.confirm': "Confirm removing friend", - 'friends.remove.desc': - 'Do you really want to remove this friend? This will also delete the conversation with them, the chat history and everything related to them.', - 'friend.removed': 'You removed this person.', - 'friends.add': 'Add friend', - 'friends.add.desc': - 'To add someone as a friend from your town, provide their username.\nTo add a friend outside of your town you\'ll need their address. It can be obtained by clicking the "Copy" button in Settings > Town > Address.', - 'friends.add.button': 'Send friend request', - 'friends.name_placeholder': 'some_guy or id@town', - 'friends.message': 'Start direct message', - 'friends.invite_to_space': 'Invite to current space', - 'friends.requests': 'Requests', - 'friends.requests_sent': 'Sent requests', - 'request.sent': 'Request successfully sent!', - 'friends.empty': - 'Seems like you don\'t have any friends with that name. You can add friends by clicking the icon on the right inside of the input field.', - 'request.confirm.title': 'Confirm request', - 'request.confirm.text': - 'By sending a friend request, the other person will be able to permantently see your profile (profile picture, description, etc.) unless you change your keys. Are you sure you want to give them this information?', - 'request.already.exists': 'Already exists', - 'request.already.exists.text': - 'You already sent a request to this person. We know you\'re excited, but please wait for them to accept your request.', + // Profile + 'status.0': 'Offline', + 'status.1': 'Online', + 'status.2': 'Away', + 'status.3': 'Busy', + 'status.message': 'Status message', + 'status.message.add': 'Add a status message', + 'profile.stop_sharing': "Stop sharing space", + 'profile.start_sharing': 'Start sharing space', + 'profile.settings': 'Settings', + 'profile.friends': 'Friends', + 'profile.files': 'Files', + 'profile.test': "Test something (DON'T CLICK)", + 'profile.retry': 'Local restart', - // Conversations - 'conversation.error': 'Conversation loading error', - 'conversations.different_town': 'Conversation takes place outside of your town (@town).', - 'conversation.info.encrypted': - 'This conversation is end-to-end encrypted with the chat history decryptable by all members (both current and past).', - 'conversation.info.town': 'This conversation is hosted on @town.', - 'conversations.placeholder': 'Search', - 'conversations.create': 'Create conversation', - 'conversations.name': 'Conversation name', - 'chat.welcome.title': 'Welcome to this new chat!', - 'chat.welcome.desc': - 'You could start by saying something like "Hello!" or maybe "Good morning!". Greetings based on the time are always good, I would know as a certified social expert sitting at home currently developing this app.', - 'chat.message': 'Say something', - 'chat.members': '@count members', - 'chat.start_space': 'Start a private Space', - 'chat.search': 'Search this conversation', - 'chat.invite_to_space': 'Invite to Space', - 'conversations.leave': 'Leave conversation', - 'conversations.leave.text': - 'Are you sure you want to leave this conversation? You will not be able to rejoin unless someone invites you back. If this is a private chat with someone, it will be deleted forever.', - 'chat.not.signed': 'This message could have been sent by someone else or modified by the server.', - 'conversations.add': 'Add a member', - 'conversations.add.create': 'New conversation', - 'choose.members': 'Choose more than one member to create a group chat.', + // Friends + 'friends': 'Friends', + 'friends.placeholder': 'Search friends', + 'friends.different_town': 'Lives in a different town than you (@town).', + 'friends.remove': 'Remove friend', + 'friends.remove.confirm': "Confirm removing friend", + 'friends.remove.desc': + 'Do you really want to remove this friend? This will also delete the conversation with them, the chat history and everything related to them.', + 'friend.removed': 'You removed this person.', + 'friends.add': 'Add friend', + 'friends.add.desc': + 'To add someone as a friend from your town, provide their username.\nTo add a friend outside of your town you\'ll need their address. It can be obtained by clicking the "Copy" button in Settings > Town > Address.', + 'friends.add.button': 'Send friend request', + 'friends.name_placeholder': 'some_guy or id@town', + 'friends.message': 'Start direct message', + 'friends.invite_to_space': 'Invite to current space', + 'friends.requests': 'Requests', + 'friends.requests_sent': 'Sent requests', + 'request.sent': 'Request successfully sent!', + 'friends.empty': + 'Seems like you don\'t have any friends with that name. You can add friends by clicking the icon on the right inside of the input field.', + 'request.confirm.title': 'Confirm request', + 'request.confirm.text': + 'By sending a friend request, the other person will be able to permantently see your profile (profile picture, description, etc.) unless you change your keys. Are you sure you want to give them this information?', + 'request.already.exists': 'Already exists', + 'request.already.exists.text': + 'You already sent a request to this person. We know you\'re excited, but please wait for them to accept your request.', - // Conversation members - 'chat.make_moderator': 'Make moderator', - 'chat.remove_moderator': 'Remove moderator', - 'chat.remove_admin': 'Remove admin', - 'chat.make_admin': 'Make admin', - 'chat.remove_member': 'Remove member', - 'chat.add_member': 'Add member', - 'chat.admin': 'Admin', - 'chat.moderator': 'Moderator', - 'chat.user': 'User', + // Conversations + 'conversation.error': 'Conversation loading error', + 'conversations.different_town': 'Conversation takes place outside of your town (@town).', + 'conversation.info.encrypted': + 'This conversation is end-to-end encrypted with the chat history decryptable by all members (both current and past).', + 'conversation.info.town': 'This conversation is hosted on @town.', + 'conversations.placeholder': 'Search', + 'conversations.create': 'Create Conversation', + 'conversations.name.edit': 'Edit title', + 'conversations.name.placeholder': 'the gang', + 'conversations.name': 'Conversation name', + 'chat.welcome.title': 'Welcome to this new chat!', + 'chat.welcome.desc': + 'You could start by saying something like "Hello!" or maybe "Good morning!". Greetings based on the time are always good, I would know as a certified social expert sitting at home currently developing this app.', + 'chat.message': 'Say something', + 'chat.members': '@count members', + 'chat.start_space': 'Start a private Space', + 'chat.search': 'Search this conversation', + 'chat.invite_to_space': 'Invite to Space', + 'conversations.leave': 'Leave conversation', + 'conversations.leave.text': + 'Are you sure you want to leave this conversation? You will not be able to rejoin unless someone invites you back. If this is a private chat with someone, it will be deleted forever.', + 'chat.not.signed': 'This message could have been sent by someone else or modified by the server.', + 'conversations.add': 'Add a member', + 'conversations.add.create': 'New Conversation', + 'choose.members': 'Choose more than one member to create a group chat.', - // System messages - 'chat.rank_change.0->1': '@name was promoted to Moderator by @sender.', - 'chat.rank_change.1->2': '@name was promoted to Admin by @sender.', - 'chat.rank_change.1->0': '@name has been demoted to a normal member by @sender.', - 'chat.rank_change.2->1': '@name has been demoted to Moderator by @sender.', - 'chat.token_change': '@name has generated a new conversation invite.', - 'chat.member_join': '@name has joined the conversation.', - 'chat.member_leave': '@name has left the conversation.', - 'chat.new_admin': '@name is now an Admin because the original Admin left the conversation.', - 'chat.member_invite': '@invitor invited @name to join the group.', - 'chat.kick': '@issuer removed @name from the group.', - 'conv.edit_data': '@name updated the conversation.', + // Conversation members + 'chat.make_moderator': 'Make moderator', + 'chat.remove_moderator': 'Remove moderator', + 'chat.remove_admin': 'Remove admin', + 'chat.make_admin': 'Make admin', + 'chat.remove_member': 'Remove member', + 'chat.add_member': 'Add member', + 'chat.admin': 'Admin', + 'chat.moderator': 'Moderator', + 'chat.user': 'User', - // Message menu - 'message.info': 'Info', - 'message.reply': 'Reply', - 'message.reply.text': 'Reply to @name', - 'message.copy': 'Copy content', - 'message.save_to': 'Save file to directory', - 'message.open': 'Open file with default', - 'message.copy_file': 'Copy file to clipboard', - 'message.profile': 'Open profile', - 'message.delete': 'Delete message', - 'message.info.text': 'This message was sent by @account (@token) at @hour:@minute on @day/@month/@year.', - 'message.info.copy_id': 'Copy ID', - 'message.info.copy_signature': 'Copy signature', - 'message.info.copy_sender': 'Copy sender ID', - 'message.info.read_old': 'Read old message', - 'message.empty': 'An empty message.', - 'message.delete.attachments': 'Should the attachments be deleted?', - 'message.delete.attachments.desc': 'Do you want to also delete all of the files attached to this message?', + // System messages + 'chat.rank_change.0->1': '@name was promoted to Moderator by @sender.', + 'chat.rank_change.1->2': '@name was promoted to Admin by @sender.', + 'chat.rank_change.1->0': '@name has been demoted to a normal member by @sender.', + 'chat.rank_change.2->1': '@name has been demoted to Moderator by @sender.', + 'chat.token_change': '@name has generated a new conversation invite.', + 'chat.member_join': '@name has joined the conversation.', + 'chat.member_leave': '@name has left the conversation.', + 'chat.new_admin': '@name is now an Admin because the original Admin left the conversation.', + 'chat.member_invite': '@invitor invited @name to join the group.', + 'chat.kick': '@issuer removed @name from the group.', + 'conv.edit_data': '@name updated the conversation.', - // Conversation info - 'conversation.info.version': 'Conversation version: @version', - 'conversation.info.id': 'Conversation ID: @id', - 'conversation.info.read': 'Read at @clock on @date', - 'conversation.info.update': 'Updated at @clock on @date', - 'conversation.info.members': 'Members: @count', - 'conversation.info.copy_id': 'Copy ID', - 'conversation.info.copy_token': 'Copy token', + // Message menu + 'message.info': 'Info', + 'message.reply': 'Reply', + 'message.reply.text': 'Reply to @name', + 'message.copy': 'Copy content', + 'message.save_to': 'Save file to directory', + 'message.open': 'Open file with default', + 'message.copy_file': 'Copy file to clipboard', + 'message.profile': 'Open profile', + 'message.delete': 'Delete message', + 'message.info.text': 'This message was sent by @account (@token) at @hour:@minute on @day/@month/@year.', + 'message.info.copy_id': 'Copy ID', + 'message.info.copy_signature': 'Copy signature', + 'message.info.copy_sender': 'Copy sender ID', + 'message.info.read_old': 'Read old message', + 'message.empty': 'An empty message.', + 'message.delete.attachments': 'Should the attachments be deleted?', + 'message.delete.attachments.desc': 'Do you want to also delete all of the files attached to this message?', - // Files - 'file.dialog': '@name is @size MB large. Choose an option to download it:', - 'download.folder': 'Download into folder', - 'download.app': 'Download into app', - 'image.loading': 'Image is loading..', - 'file.unknown_size': 'Unknown size', - 'file.bytes': '@count B', - 'file.kilobytes': '@count KB', - 'file.megabytes': '@count MB', - 'file.gigabytes': '@count GB', + // Conversation info + 'conversation.info.version': 'Conversation version: @version', + 'conversation.info.id': 'Conversation ID: @id', + 'conversation.info.read': 'Read at @clock on @date', + 'conversation.info.update': 'Updated at @clock on @date', + 'conversation.info.members': 'Members: @count', + 'conversation.info.copy_id': 'Copy ID', + 'conversation.info.copy_token': 'Copy token', - // Live share - 'chat.zapshare': 'Share any file (Zap)', - 'chat.zapshare_request': 'Request to share file', - 'chat.zapshare.not_found': 'The request has expired.', - 'chat.zapshare.creation_failed': 'Failed to create a live share request. Maybe you already have one active?', - 'chat.zapshare.not_send_self': 'You cannot accept your own request.', - 'chat.zapshare.waiting': 'Waiting..', - 'chat.zapshare.finishing': 'Finishing up..', - 'chat.zapshare.compressing': 'Compressing..', - 'chat.zapshare.uploading': 'Uploading..', - 'chat.zapshare.downloading': 'Downloading..', + // Files + 'file.dialog': '@name is @size MB large. Choose an option to download it:', + 'download.folder': 'Download into folder', + 'download.app': 'Download into app', + 'image.loading': 'Image is loading..', + 'file.unknown_size': 'Unknown size', + 'file.bytes': '@count B', + 'file.kilobytes': '@count KB', + 'file.megabytes': '@count MB', + 'file.gigabytes': '@count GB', - // Library - 'library.all': 'Everything', - 'library.images': 'Images', - 'library.gifs': 'GIFs', - 'library.empty': 'It\'s pretty empty in here. You can add stuff to it by favoriting images or GIFs in conversations.', + // Live share + 'chat.zapshare': 'Share any file (Zap)', + 'chat.zapshare_request': 'Request to share file', + 'chat.zapshare.not_found': 'The request has expired.', + 'chat.zapshare.creation_failed': 'Failed to create a live share request. Maybe you already have one active?', + 'chat.zapshare.not_send_self': 'You cannot accept your own request.', + 'chat.zapshare.waiting': 'Waiting..', + 'chat.zapshare.finishing': 'Finishing up..', + 'chat.zapshare.compressing': 'Compressing..', + 'chat.zapshare.uploading': 'Uploading..', + 'chat.zapshare.downloading': 'Downloading..', - // Spaces - 'chat.space.add': 'New space', - 'join.space': 'Join Space', - 'join.space.popup': 'Some people click this on accident, so do you really want to join this space?', - 'chat.space_invite': 'Space invitation', - 'chat.space.not_found': 'This space already ended.', - 'chat.space.loading': 'Loading space..', - 'chat.space.leave': 'Do you really want to leave your current space?', + // Library + 'library.all': 'Everything', + 'library.images': 'Images', + 'library.gifs': 'GIFs', + 'library.empty': + 'It\'s pretty empty in here. You can add stuff to it by favoriting images or GIFs in conversations.', - // Townsquare - 'townsquare.connection_error': "Couldn't connect to Townsquare. Please contact the admins of your instance about this or try again later.", - 'townsquare.connecting': "Connecting..", - "townsquare.viewing": "@count/@total on the square", - }, + // Spaces + 'chat.space.add': 'Create Space', + 'join.space': 'Join Space', + 'join.space.popup': 'Some people click this on accident, so do you really want to join this space?', + 'chat.space_invite': 'Space invitation', + 'chat.space.not_found': 'This space already ended.', + 'chat.space.loading': 'Loading space..', + 'chat.space.leave': 'Do you really want to leave your current space?', + }, - //* German - 'de_DE': { - /* + //* German + 'de_DE': { + /* // App 'app.title': 'The chat app', 'app.welcome': 'Danke, dass du mich auf dieser Reise begleitest!', @@ -259,6 +276,6 @@ class ChatPageTranslations extends Translations { 'join.space': 'Space beitreten', 'join.space.popup': 'Manche klicken darauf ausversehen, also willst du wirklich diesem Space beitreten?', */ - } - }; + }, + }; } diff --git a/lib/translations/errors.dart b/lib/translations/errors.dart index 993baeda..5776217c 100644 --- a/lib/translations/errors.dart +++ b/lib/translations/errors.dart @@ -3,100 +3,132 @@ import 'package:get/get.dart'; class ErrorTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - 'error': 'Error', - 'error.no_connection': - 'There was an error while connecting to the server. Please make sure you are connected to the internet and have a stable connection to your town.', - 'render.error': 'Elements of type @type aren\'t supported.', - 'server.not_found': 'The server couldn\'t be reached. Make sure you have the right domain.', - 'error.network': 'Seems like you are offline. Please try to check the connection of your device.', - 'server.error': 'Something went wrong on the server, please try again later.', - 'other.server.error': - 'It seems like the town this conversation is hosted on is currently down. Please try again later. We\'ll automatically try restoring a connection.', - 'server.error.code': 'Something went wrong on the server, please try again later. Status code: @code', - 'friends.error': 'There was an error while loading your friends. Please try again later or contact your administrator.', - 'node.error': 'The chat server didn\'t respond, please try again later.', - 'mail.error': 'There was an error with our mail servers. Please try again later or contact your administrator.', - 'app.error': 'There was an error with the app. Please report this to the developers.', - 'not.setup': 'The chat server is not set up yet, maybe try updating to the newest version?', - 'not.found': 'This wasn\'t found. Maybe it has already been deleted?', - 'new.version': 'A new version is available, please update the app.', - 'key.error': 'Something went wrong with your keys. Maybe try restarting the app or contacting support?', - 'keys.invalid': 'Invalid keys found. This means that the server could\'ve been hacked or someone is trolling you.', - 'already.deleted': 'This object was already deleted.', - 'no.permission': 'You don\'t have permission to do that.', - 'spaces.connection_error': 'Something went wrong with the spaces connection. Please try again later.', - 'invalid.method': 'This is incorrect. Please try again.', - 'sessions.limit': 'You are already registered with 5 devices. Please log out of one of them to log in here.', - 'password.incorrect': 'Your password is incorrect. Please try again.', - 'protocol.error.server': - 'The town you are trying to connect to runs an outdated version of Liphium. Please contact the owners of that town to update to the latest version.', - 'protocol.error.client': - 'The town you are trying to connect to runs a more up to date version of Liphium. Please update your app to make sure everything works fine.', - 'spaces.not.setup': 'Spaces is not supported in your town. Please contact the owners of this town and ask them to set up Spaces.', - 'not.supported': 'This feature is not supported on this platform.', - 'profile.conversation_not_found': 'You are not in a conversation with this person. Please create one first.', + //* English US + 'en_US': { + 'error': 'Error', + 'error.no_connection': + 'There was an error while connecting to the server. Please make sure you are connected to the internet and have a stable connection to your town.', + 'render.error': 'Elements of type @type aren\'t supported.', + 'server.not_found': 'The server couldn\'t be reached. Make sure you have the right domain.', + 'error.network': 'Seems like you are offline. Please try to check the connection of your device.', + 'error.untrusted_server': + 'This action couldn\'t be completed because @domain isn\'t trusted. Check your settings if you want to trust this server.', + 'server.error': 'Something went wrong on the server, please try again later.', + 'other.server.error': + 'It seems like the town this conversation is hosted on is currently down. Please try again later. We\'ll automatically try restoring a connection.', + 'server.error.code': 'Something went wrong on the server, please try again later. Status code: @code', + 'friends.error': + 'There was an error while loading your friends. Please try again later or contact your administrator.', + 'node.error': 'The chat server didn\'t respond, please try again later.', + 'mail.error': 'There was an error with our mail servers. Please try again later or contact your administrator.', + 'app.error': 'There was an error with the app. Please report this to the developers.', + 'not.setup': 'The chat server is not set up yet, maybe try updating to the newest version?', + 'not.found': 'This wasn\'t found. Maybe it has already been deleted?', + 'new.version': 'A new version is available, please update the app.', + 'key.error': 'Something went wrong with your keys. Maybe try restarting the app or contacting support?', + 'keys.invalid': + 'Invalid keys found. This means that the server could\'ve been hacked or someone is trolling you.', + 'already.deleted': 'This object was already deleted.', + 'no.permission': 'You don\'t have permission to do that.', + 'spaces.connection_error': 'Something went wrong with the spaces connection. Please try again later.', + 'invalid.method': 'This is incorrect. Please try again.', + 'sessions.limit': 'You are already registered with 5 devices. Please log out of one of them to log in here.', + 'password.incorrect': 'Your password is incorrect. Please try again.', + 'protocol.error.server': + 'The town you are trying to connect to runs an outdated version of Liphium. Please contact the owners of that town to update to the latest version.', + 'protocol.error.client': + 'The town you are trying to connect to runs a more up to date version of Liphium. Please update your app to make sure everything works fine.', + 'spaces.not.setup': + 'Spaces is not supported in your town. Please contact the owners of this town and ask them to set up Spaces.', + 'not.supported': 'This feature is not supported on this platform.', + 'profile.conversation_not_found': 'You are not in a conversation with this person. Please create one first.', + 'secure_storage.not_supported': 'Secure storage is not supported on your platform.', + 'secure_storage.unlock_failed': 'Couldn\'t unlock keyring. Please make sure everything is set up correctly.', - // Friends - 'request.self': 'Are you trying to add yourself as a friend?', - 'request.self.text': "I know you're lonely, but you can't be your own friend. Sorry.", - 'requests.already.exists': 'Already sent', - 'requests.already.exists.text': - "I know you want to be this person's friend, but you already sent them a request. So please chill a little bit.", - 'request.not.found': 'User not found', - 'request.not.found.text': "You sure this is your friend? Maybe you just met them in your dreams?", - 'requests.error': 'There was an error with accepting this request. Please try again later or update to the latest version.', + // Friends + 'request.friend.exists': 'Already added', + 'request.friend.exists.text': + 'If you want more friends you can\'t just add the same person, that\'s not how this works.', + 'request.self': 'Are you trying to add yourself as a friend?', + 'request.self.text': "I know you're lonely, but you can't be your own friend. Sorry.", + 'requests.already.exists': 'Already sent', + 'requests.already.exists.text': + "I know you want to be this person's friend, but you already sent them a request. So please chill a little bit.", + 'request.not.found': 'User not found', + 'request.not.found.text': "You sure this is your friend? Maybe you just met them in your dreams?", + 'requests.error': + 'There was an error with accepting this request. Please try again later or update to the latest version.', - // Chat - 'conversations.error': 'Conversation error', - 'conversations.amount': 'You have reached the maximum amount of @amount conversations. Please delete old ones to create more.', - 'conversations.name.length': 'The conversation name you specified is longer than allowed. Please use less than @length characters.', - 'error.not_delete_conversation': 'Couldn\'t delete conversation. Try restarting the app if this conversation was just created.', - 'file.not_uploaded': 'File not found.', - 'file.too_large': 'The maximum file size is @1MB.', - 'file.too_many': 'You can\'t attach more than 5 files to a message.', - 'file.unsafe': 'The provider of this file (@domain) isn\'t trusted.', - 'file.no_save_location': 'Please select a save location for your file.', - 'chat.add_file': 'Attach a file', - 'message.delete_error': 'Couldn\'t delete message. Please try again later.', - 'group.data_too_long': - 'The data of this conversation became too long. This shouldn\'t normally happen. You should probably contact the developers of this app.', - 'zap.no_save_location': 'Please select a save location for your file to use Zap.', - 'zap.already_exists': 'This file already exists. Please choose a different place to store this file.', - 'zap.error': 'Zap Error', - 'zap.no_mobile': - 'Zap is currently not supported on mobile. We still have some things we need to figure out. Please wait until the app gets a little more stable. We\'ll announce once we have an estimated time when Zap will be available.', + // Chat + 'conversations.error': 'Conversation error', + 'conversations.amount': + 'You have reached the maximum amount of @amount conversations. Please delete old ones to create more.', + 'conversations.name.length': + 'The conversation name you specified is longer than allowed. Please use less than @length characters.', + 'conversations.name_needed': 'Please specify a name for this conversation.', + 'conversations.too_many_members': 'This conversation allows a maximum of @amount members.', + 'squares.name_needed': 'Please specify a name for this Square.', + 'squares.name.length': + 'The Square name you specified is longer than allowed. Please use less than @length characters.', + 'topic.name_needed': 'Please specify a name for this topic.', + 'topic.name.length': + 'The topic name you specified is longer than allowed. Please use less than @length characters.', + 'squares.space.name_needed': 'Please specify a name for this Space.', + 'squares.space.name.length': + 'The Space name length you specified is longer than allowed. Please use less than @length characters.', + 'squares.space.already_added': 'This Space has already been added.', + 'squares.too_many_members': 'This Square allows a maximum of @amount members.', + 'conversation.delete_error': 'You can\'t delete this conversation yet. Please try again later.', + 'error.not_delete_conversation': + 'Couldn\'t delete conversation. Try restarting the app if this conversation was just created.', + 'file.not_uploaded': 'File not found.', + 'file.too_large': 'The maximum file size is @1MB.', + 'file.too_many': 'You can\'t attach more than 5 files to a message.', + 'file.unsafe': 'The provider of this file (@domain) isn\'t trusted.', + 'file.no_save_location': 'Please select a save location for your file.', + 'chat.add_file': 'Attach a file', + 'message.delete_error': 'Couldn\'t delete message. Please try again later.', + 'group.data_too_long': + 'The data of this conversation became too long. This shouldn\'t normally happen. You should probably contact the developers of this app.', + 'zap.no_save_location': 'Please select a save location for your file to use Zap.', + 'zap.already_exists': 'This file already exists. Please choose a different place to store this file.', + 'zap.error': 'Zap Error', + 'zap.no_mobile': + 'Zap is currently not supported on mobile. We still have some things we need to figure out. Please wait until the app gets a little more stable. We\'ll announce once we have an estimated time when Zap will be available.', - // Settings - 'profile_picture.not_uploaded': 'Your profile picture couldn\'t be uploaded. Please try again later or contact support.', - 'profile_picture.not_set': 'Your profile picture couldn\'t be set. Please try again later or contact support.', - 'username.invalid': 'Your username doesn\'t match the requirements. Please make it longer than 3 characters.', - 'display_name.invalid': 'Your display name doesn\'t match the requirements. Please make it longer than 3 characters.', - 'username.taken': 'This username is taken, please choose a different one.', - 'password.mismatch': 'The passwords don\'t match.', + // Settings + 'profile_picture.not_uploaded': + 'Your profile picture couldn\'t be uploaded. Please try again later or contact support.', + 'profile_picture.not_set': 'Your profile picture couldn\'t be set. Please try again later or contact support.', + 'username.invalid': 'Your username doesn\'t match the requirements. Please make it longer than 3 characters.', + 'display_name.invalid': + 'Your display name doesn\'t match the requirements. Please make it longer than 3 characters.', + 'username.taken': 'This username is taken, please choose a different one.', + 'password.mismatch': 'The passwords don\'t match.', - // Game - 'tabletop.not_found': 'The table wasn\'t found for some reason, maybe try rejoining the Space?', - 'tabletop.already_joined': 'You are already in tabletop. You can\'t join again.', - 'tabletop.couldnt_create': 'There was an issue during the table creation. Please report this to the developers.', - 'tabletop.object_not_found': 'This object doesn\'t exist anymore, maybe it has already been deleted?', - 'tabletop.object_already_held': 'This is object is already being held by someone else. Please try to modify it again later.', - 'tabletop.object_not_in_queue': - 'You didn\'t ask to modify this object before the actual modification. This is an issue with the app, please contact the developers.', - 'tabletop.invalid_action': 'You can\'t do that right now. Please try again later.', - 'no.start': - 'The game couldn\'t be started. We\'re sorry for the inconvenience, please message support about this issue if you encounter it.', + // Spaces, Studio and Tabletop + 'tabletop.not_found': 'The table wasn\'t found for some reason, maybe try rejoining the Space?', + 'tabletop.already_joined': 'You are already in tabletop. You can\'t join again.', + 'tabletop.couldnt_create': 'There was an issue during the table creation. Please report this to the developers.', + 'tabletop.object_not_found': 'This object doesn\'t exist anymore, maybe it has already been deleted?', + 'tabletop.object_already_held': + 'This is object is already being held by someone else. Please try to modify it again later.', + 'tabletop.object_not_in_queue': + 'You didn\'t ask to modify this object before the actual modification. This is an issue with the app, please contact the developers.', + 'tabletop.invalid_action': 'You can\'t do that right now. Please try again later.', + 'no.start': + 'The game couldn\'t be started. We\'re sorry for the inconvenience, please message support about this issue if you encounter it.', + 'error.studio.rtc': 'RTC couldn\'t connect (@code). Please check your internet connection.', - // Message errors - 'error.message.timestamp': 'A timestamp for your message could not be generated.', - 'error.message.empty': 'Your message is empty, please add some content to send it.', - 'error.message.loading': 'A message is still waiting to be sent, please wait for it to finish.', - }, + // Message errors + 'error.message.timestamp': 'A timestamp for your message could not be generated.', + 'error.message.empty': 'Your message is empty, please add some content to send it.', + 'error.message.loading': 'A message is still waiting to be sent, please wait for it to finish.', + }, - //* German - 'de_DE': { - /* + //* German + 'de_DE': { + /* 'error': 'Fehler', 'server.error': 'Auf dem Server ist ein Fehler aufgetreten, bitte versuche es später erneut.', 'node.error': 'Der Chat-Server hat nicht geantwortet, bitte versuche es später erneut.', @@ -132,6 +164,6 @@ class ErrorTranslations extends Translations { // Game 'no.start': 'Das Spiel konnte nicht gestartet werden. Wir entschuldigen uns für die Unannehmlichkeiten, bitte melde dich bei Support, wenn du auf dieses Problem stößt.', */ - }, - }; + }, + }; } diff --git a/lib/translations/general.dart b/lib/translations/general.dart index 86f9470c..f56f7445 100644 --- a/lib/translations/general.dart +++ b/lib/translations/general.dart @@ -3,111 +3,123 @@ import 'package:get/get.dart'; class GeneralTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // Actions - 'connect': 'Connect', - 'learn_more': 'Learn more', - 'dev.details': 'Developer details', - 'offline': 'Offline', - 'loading': 'Loading..', - 'preparing': 'Preparing..', - 'rendering': 'Rendering..', - 'success': 'Success', - 'create': 'Create', - 'ok': 'Okay', - 'edit': 'Edit', - 'copy': 'Copy', - 'retry': 'Retry', - 'back': 'Back', - 'no.got': 'No, you got me', - 'yeah': 'Yeah', - 'yes': 'Yes', - 'accept': 'Accept', - 'no': 'No', - 'change': 'Change', - 'save': 'Save', - 'view': 'View', - 'delete': 'Delete', - 'cancel': 'Cancel', - 'remove': 'Remove', - 'removed': 'Removed', - 'add': 'Add', - 'select': 'Select', - 'search': 'Search', - 'zoom': 'Zoom', - 'x': 'X', - 'y': 'Y', - 'close': 'Close', - 'open': 'Open', - 'username': 'Username', - 'username.description': 'Your username can only contain lowercase characters, numbers and "_" or "-".', - 'display_name': "Display name", - 'display_name.description': 'Your display name is the name everyone sees. No special requirements.', - 'password': 'Password', - 'password.current': 'Current password', - 'invite': 'Invite', - 'email': 'Email', - 'code': 'Code', - 'under.dev': 'This feature is still under development. Sorry for the inconvenience.', - 'no.friends': 'You have no friends yet. If you want to add some, you can do that in the friends page.', - 'open.friends': 'Open friends page', - 'reset': 'Reset', - 'app.settings': 'App settings', - 'spaces': 'Spaces', - 'page_switcher': 'Page @count/@max', - 'rank': 'Rank', - 'liphium_address': 'Your Liphium address', - 'help': 'Help & resources', + //* English US + 'en_US': { + // Actions + 'connect': 'Connect', + 'learn_more': 'Learn more', + 'dev.details': 'Developer details', + 'offline': 'Offline', + 'loading': 'Loading..', + 'preparing': 'Preparing..', + 'rendering': 'Rendering..', + 'success': 'Success', + 'create': 'Create', + 'ok': 'Okay', + 'edit': 'Edit', + 'copy': 'Copy', + 'retry': 'Retry', + 'back': 'Back', + 'no.got': 'No, you got me', + 'yeah': 'Yeah', + 'yes': 'Yes', + 'accept': 'Accept', + 'no': 'No', + 'change': 'Change', + 'save': 'Save', + 'view': 'View', + 'delete': 'Delete', + 'cancel': 'Cancel', + 'remove': 'Remove', + 'removed': 'Removed', + 'add': 'Add', + 'select': 'Select', + 'search': 'Search', + 'zoom': 'Zoom', + 'x': 'X', + 'y': 'Y', + 'close': 'Close', + 'open': 'Open', + 'username': 'Username', + 'username.description': 'Your username can only contain lowercase characters, numbers and "_" or "-".', + 'display_name': "Display name", + 'display_name.description': 'Your display name is the name everyone sees. No special requirements.', + 'password': 'Password', + 'password.current': 'Current password', + 'invite': 'Invite', + 'email': 'Email', + 'code': 'Code', + 'under.dev': 'This feature is still under development. Sorry for the inconvenience.', + 'no.friends': 'You have no friends yet. If you want to add some, you can do that in the friends page.', + 'open.friends': 'Open friends page', + 'reset': 'Reset', + 'app.settings': 'App settings', + 'spaces': 'Spaces', + 'page_switcher': 'Page @count/@max', + 'rank': 'Rank', + 'liphium_address': 'Your Liphium address', + 'help': 'Help & resources', + 'pinned': 'Pinned', - // Spaces - 'spaces.not_supported': 'Not supported', - 'spaces.not_supported.desc': 'We\'re sorry for the inconvenience, but as of now Spaces is only available on Desktop as a preview.', + // Spaces + 'shared': 'Shared', + 'spaces.not_supported': 'Not supported', + 'spaces.not_supported.desc': + 'We\'re sorry for the inconvenience, but as of now Spaces is only available on Desktop as a preview.', - // Placeholder - 'placeholder.domain': 'example.com', + // Placeholder + 'placeholder.domain': 'example.com', - // Time TODO: Differentiate between PM and AM - 'message.time': '@hour:@minute', - 'time.yesterday': 'Yesterday', - 'time.today': 'Today', - 'time': '@day/@month/@year', - 'general_time': '@day/@month/@year @hour:@minute', + // Time TODO: Differentiate between PM and AM + 'message.time': '@hour:@minute', + 'time.yesterday': 'Yesterday', + 'time.today': 'Today', + 'time': '@day/@month/@year', + 'general_time': '@day/@month/@year @hour:@minute', + 'unread.messages': 'Unread messages', + + // Log out thing + 'log_out': 'Log out', + 'log_out.dialog': + 'This will delete all the data on this device. If you don\'t have your keys as a file or on another logged in device, you will not be able to recover your account. Are you sure you want to log out?', + 'log_out.delete_files': 'Delete files', - // Log out thing - 'log_out': 'Log out', - 'log_out.dialog': - 'This will delete all the data on this device. If you don\'t have your keys as a file or on another logged in device, you will not be able to recover your account. Are you sure you want to log out?', - 'log_out.delete_files': 'Delete files', + // File things + 'file.uploading': 'Uploading @index of @total..', + 'file.links.title': 'Unknown location found', + 'file.links.description': + 'You are trying to connect to a town at @domain. This could potentially lead to your IP address or personal information being exposed. Do you want to add @domain to your list of trusted towns?', - // File things - 'file.uploading': 'Uploading @index of @total..', - 'file.links.title': 'Unknown location found', - 'file.links.description': - 'You are trying to connect to a town at @domain. This could potentially lead to your IP address or personal information being exposed. Do you want to add @domain to your list of trusted towns?', + // For specifically adding the links to GIFs or images on a different server + 'file.images.trust.title': 'Add to trusted links', + 'file.images.trust.description': + 'If you add @domain to your list of trusted providers, this means they will be able to see your IP address or other personal information that may be exposed using a web request. Do you want to add @domain to your list of trusted providers?', - // For specifically adding the links to GIFs or images on a different server - 'file.images.trust.title': 'Add to trusted links', - 'file.images.trust.description': - 'If you add @domain to your list of trusted providers, this means they will be able to see your IP address or other personal information that may be exposed using a web request. Do you want to add @domain to your list of trusted providers?', + // Context menu + 'context_menu.cut': 'Cut', + 'context_menu.copy': 'Copy', + 'context_menu.paste': 'Paste', + 'context_menu.selectAll': 'Select all', + 'context_menu.share': 'Share', + 'context_menu.delete': 'Delete', + 'context_menu.custom': 'Custom', - // Context menu - 'context_menu.cut': 'Cut', - 'context_menu.copy': 'Copy', - 'context_menu.paste': 'Paste', - 'context_menu.selectAll': 'Select all', - 'context_menu.share': 'Share', - 'context_menu.delete': 'Delete', - 'context_menu.custom': 'Custom', + // Tray icon context menu + 'tray.show_window': 'Show window', + 'tray.exit_app': 'Exit app', - // Tray icon context menu - 'tray.show_window': 'Show window', - 'tray.exit_app': 'Exit app', - }, + // Features + 'square': 'Square', + 'square.desc': 'A place to hang out and chat.', + 'conversation': 'Conversation', + 'conversation.desc': 'A regular conversation for chatting.', + 'space': 'Space', + 'space.desc': 'Talk to your friends and have fun.', + }, - //* German - 'de_DE': { - /* + //* German + 'de_DE': { + /* // Actions 'success': 'Erfolg', 'create': 'Erstellen', @@ -139,6 +151,6 @@ class GeneralTranslations extends Translations { 'time.today': 'Heute', 'time': '@day.@month.@year', */ - } - }; + }, + }; } diff --git a/lib/translations/settings/tr_account_settings.dart b/lib/translations/settings/tr_account_settings.dart index 6c71d1cf..b6c43e20 100644 --- a/lib/translations/settings/tr_account_settings.dart +++ b/lib/translations/settings/tr_account_settings.dart @@ -3,62 +3,66 @@ import 'package:get/get.dart'; class AccountSettingsTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // Data settings - 'settings.data.social': 'Social features', - 'settings.data.social.text': - 'Liphium\'s social features are currently in the experimental phase. They open up new ways to share information with your friends. Kind of like what other platforms are doing, but it\'s all end-to-end encrypted and private.', - 'social.enable': 'Enable social features', - 'settings.data.profile_picture': 'Profile picture', - 'settings.data.profile_picture.select': - 'Now just zoom and move your image into the perfect spot! So it makes your beauty shine, if you even have any...', - 'settings.data.profile_picture.requirements': 'Can only be a JPEG or PNG and can\'t be larger than 10 MB.', - 'settings.data.profile_picture.remove': 'Remove profile picture', - 'settings.data.profile_picture.remove.confirm': 'Are you sure you want to remove your profile picture?', - 'settings.data.key_requests': 'Synchronization requests', - 'settings.data.key_requests.description': 'If we ask you to accept a key request on another device, you can find them here.', - 'settings.data.permissions': 'Permissions', - 'settings.data.permissions.description': - 'If you don\'t know what this is, it\'s fine. This is just data from the server that we can ask you for in case of problems. Here\'s which permissions you have:', - 'settings.data.account': 'Account data', - 'settings.data.email.description': 'Showing your email would be work. And I don\'t like that, you know.', - 'settings.data.log_out': 'Log out of your account', - 'settings.data.log_out.description': - 'If you log out of your account, we\'ll delete all your data from this device. This includes the keys we use to encrypt stuff on our servers. If you don\'t have these on another device, you will NEVER be able to recover your friends.', - 'settings.data.danger_zone': 'Danger zone', - 'settings.data.danger_zone.description': - 'Hello, and welcome down here! Hope you haven\'t come here to delete your account. If you have, you can do that here. But please don\'t. We\'ll miss you. :(', - 'settings.data.danger_zone.delete_account': 'Delete account', - 'settings.data.danger_zone.delete_account.confirm': - 'This is just a request and your actual data will be deleted in 30 days. We do this to make sure you didn\'t just accidentally click this button and that you are the actual owner of this account. Are you sure you want to delete your account?', - 'settings.data.change_name.dialog': 'Want to change your username? Just provide it below and we\'ll handle it for you.', - 'settings.data.change_display_name.dialog': 'Want to change your display name? We\'ll handle your request right here.', + //* English US + 'en_US': { + // Data settings + 'settings.data.social': 'Social features', + 'settings.data.social.text': + 'Liphium\'s social features are currently in the experimental phase. They open up new ways to share information with your friends. Kind of like what other platforms are doing, but it\'s all end-to-end encrypted and private.', + 'social.enable': 'Enable social features', + 'settings.data.profile_picture': 'Profile picture', + 'settings.data.profile_picture.select': + 'Now just zoom and move your image into the perfect spot! So it makes your beauty shine, if you even have any...', + 'settings.data.profile_picture.requirements': 'Can only be a JPEG or PNG and can\'t be larger than 10 MB.', + 'settings.data.profile_picture.remove': 'Remove profile picture', + 'settings.data.profile_picture.remove.confirm': 'Are you sure you want to remove your profile picture?', + 'settings.data.key_requests': 'Synchronization requests', + 'settings.data.key_requests.description': + 'If we ask you to accept a key request on another device, you can find them here.', + 'settings.data.permissions': 'Permissions', + 'settings.data.permissions.description': + 'If you don\'t know what this is, it\'s fine. This is just data from the server that we can ask you for in case of problems. Here\'s which permissions you have:', + 'settings.data.account': 'Account data', + 'settings.data.email.description': 'Showing your email would be work. And I don\'t like that, you know.', + 'settings.data.log_out': 'Log out of your account', + 'settings.data.log_out.description': + 'If you log out of your account, we\'ll delete all your data from this device. This includes the keys we use to encrypt stuff on our servers. If you don\'t have these on another device, you will NEVER be able to recover your friends.', + 'settings.data.danger_zone': 'Danger zone', + 'settings.data.danger_zone.description': + 'Hello, and welcome down here! Hope you haven\'t come here to delete your account. If you have, you can do that here. But please don\'t. We\'ll miss you. :(', + 'settings.data.danger_zone.delete_account': 'Delete account', + 'settings.data.danger_zone.delete_account.confirm': + 'This is just a request and your actual data will be deleted in 30 days. We do this to make sure you didn\'t just accidentally click this button and that you are the actual owner of this account. Are you sure you want to delete your account?', + 'settings.data.change_name.dialog': + 'Want to change your username? Just provide it below and we\'ll handle it for you.', + 'settings.data.change_display_name.dialog': + 'Want to change your display name? We\'ll handle your request right here.', - // Key sync requests - 'key_requests.empty': 'There are currently no requests to exchange keys with another device.', - 'key_requests.code.title': 'Enter verification code', - 'key_requests.code.description': - 'Please enter the verification code that\'s displayed on the other device. We\'ll use it to verify that the request hasn\'t been modified by the server.', - 'key_requests.code.placeholder': 'abcdef', - 'key_requests.code.error': 'This code is invalid. Please try again.', - 'key_requests.code.button': 'Verify code', + // Key sync requests + 'key_requests.empty': 'There are currently no requests to exchange keys with another device.', + 'key_requests.code.title': 'Enter verification code', + 'key_requests.code.description': + 'Please enter the verification code that\'s displayed on the other device. We\'ll use it to verify that the request hasn\'t been modified by the server.', + 'key_requests.code.placeholder': 'abcdef', + 'key_requests.code.error': 'This code is invalid. Please try again.', + 'key_requests.code.button': 'Verify code', - // Authentication settings - 'settings.authentication.first_factor': 'First factor', - 'settings.authentication.password.description': 'We\'ll not show your password here. That would be stupid.', - 'settings.authentication.change_password.dialog': - 'Let\'s make sure your account is secure again. All your devices (also this one) will be logged out after you click "Save".', - 'settings.authentication.second_factor': 'Second factor', + // Authentication settings + 'settings.authentication.first_factor': 'First factor', + 'settings.authentication.password.description': 'We\'ll not show your password here. That would be stupid.', + 'settings.authentication.change_password.dialog': + 'Let\'s make sure your account is secure again. All your devices (also this one) will be logged out after you click "Save".', + 'settings.authentication.second_factor': 'Second factor', - // Invite settings (this is mostly alpha only) - 'settings.invites.description': - "Invites are a token required for creating an account on the chat app. If you want one of your friends to be on here, send them an invite! They are distributed randomly in waves to prevent an influx of too many new users at once and also guarantee that the new users getting in are actually your friends.", - 'settings.invites.generate': 'Generate invite', - 'settings.invites.generated': 'Invite generated! It was copied to your clipboard.', - 'settings.invites.history': 'History', - 'settings.invites.history.description': 'Here are all the invites you already generated. Hover over them to see the token.', - 'settings.invites.history.empty': 'You haven\'t generated any invites yet or they have all been redeemed.', - }, - }; + // Invite settings (this is mostly alpha only) + 'settings.invites.description': + "Invites are a token required for creating an account on the chat app. If you want one of your friends to be on here, send them an invite! They are distributed randomly in waves to prevent an influx of too many new users at once and also guarantee that the new users getting in are actually your friends.", + 'settings.invites.generate': 'Generate invite', + 'settings.invites.generated': 'Invite generated! It was copied to your clipboard.', + 'settings.invites.history': 'History', + 'settings.invites.history.description': + 'Here are all the invites you already generated. Hover over them to see the token.', + 'settings.invites.history.empty': 'You haven\'t generated any invites yet or they have all been redeemed.', + }, + }; } diff --git a/lib/translations/settings/tr_app_settings.dart b/lib/translations/settings/tr_app_settings.dart index 56052073..1e00325f 100644 --- a/lib/translations/settings/tr_app_settings.dart +++ b/lib/translations/settings/tr_app_settings.dart @@ -3,26 +3,48 @@ import 'package:get/get.dart'; class AppSettingsTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // General settings - 'settings.general.notifications': 'Notifications', - 'settings.general.ringtone': 'Spaces ringtone', - 'settings.general.language': 'Choose the app language', - 'settings.general.ringtone.disabled': - 'The Spaces ringtone is currently disabled. It will be re-introduced when voice chat returns to Spaces some time in 2025. Together with Liphium Ring, but that\'s not even on the roadmap yet.', - 'notification_sounds.tooltip': 'This only plays the sound when your settings allow it to.', - 'notification_sounds.enabled': 'Enable notification sounds', - 'notification_sounds.do_not_disturb': 'Play notification sounds even when in do-not-disturb mode', - 'notification_sounds.only_when_tray': 'Only play notification sounds when minimized to tray', - 'ring.desc': - 'The ringtone will follow all settings from the notification sounds above. You can also make it so the ringtone still plays when Liphium is not minimized by using the settings below.', - 'ring.enable': 'Play a ring sound when being invited to a Space', - 'ring.ignore_tray': 'Also play a ring sound when Liphium is not minimized to tray', + //* English US + 'en_US': { + // General settings + 'settings.general.behavior': 'Behavior', + 'behavior.minimize_to_tray': 'Minimize Liphium to the system tray when closed', + 'settings.general.notifications': 'Notifications', + 'settings.general.ringtone': 'Spaces ringtone', + 'settings.general.language': 'Choose the app language', + 'settings.general.ringtone.disabled': + 'The Spaces ringtone is currently disabled. It will be re-introduced when voice chat returns to Spaces some time in 2025. Together with Liphium Ring, but that\'s not even on the roadmap yet.', + 'notification_sounds.tooltip': 'This only plays the sound when your settings allow it to.', + 'notification_sounds.enabled': 'Enable notification sounds', + 'notification_sounds.do_not_disturb': 'Play notification sounds even when in do-not-disturb mode', + 'notification_sounds.only_when_tray': 'Only play notification sounds when minimized to tray', + 'ring.desc': + 'The ringtone will follow all settings from the notification sounds above. You can also make it so the ringtone still plays when Liphium is not minimized by using the settings below.', + 'ring.enable': 'Play a ring sound when being invited to a Space', + 'ring.ignore_tray': 'Also play a ring sound when Liphium is not minimized to tray', - // Logging settings - 'logging.amount.desc': 'Amount of logs to keep in the history', - 'logging.launch': 'Open log folder', - }, - }; + // Logging settings + 'logging.amount.desc': 'Amount of logs to keep in the history', + 'logging.launch': 'Open log folder', + + // Audio settings + 'settings.audio.microphone': 'Choose your microphone', + 'settings.audio.output_device': 'Choose your output device', + 'settings.audio.devices_empty': 'No devices found or still loading devices.', + 'settings.audio.activation_mode': 'Microphone activation mode', + 'settings.audio.activation_mode.desc': + 'When the square on the right is a bright color, your voice will be transmitted.', + 'microphone.mode.voice_activity': 'Detect when I speak', + 'microphone.mode.always_on': 'Always transmit my speech', + 'microphone.auto_activity': 'Automatically determine when I speak', + 'settings.audio.microphone.sensitivity.desc': + 'You will be heard by other people when your volume is over the threshold defined below.', + 'settings.audio.advanced': 'Advanced audio settings', + 'audio.encoding.mode': 'Audio encoding mode', + 'audio.encoding.mode.auto': 'Auto (based on network conditions)', + 'audio.encoding.mode.max': 'Maximum (over 500 kbit/s)', + 'audio.encoding.mode.high': 'High (144 kbit/s)', + 'audio.encoding.mode.medium': 'Medium (96 kbit/s)', + 'audio.encoding.mode.low': 'Low (40 kbit/s)', + }, + }; } diff --git a/lib/translations/settings/tr_appearance_settings.dart b/lib/translations/settings/tr_appearance_settings.dart index 53fc7f7c..ebb45ac5 100644 --- a/lib/translations/settings/tr_appearance_settings.dart +++ b/lib/translations/settings/tr_appearance_settings.dart @@ -3,32 +3,34 @@ import 'package:get/get.dart'; class AppearanceSettingsTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // Theme settings - 'theme.presets': 'Presets', - 'theme.default_dark': 'Default dark', - 'theme.default_light': 'Default light', - 'theme.winter': 'Winter', - 'theme.custom': 'Create your own', - 'theme.custom.title': 'Custom theme', - 'theme.primary': 'Primary color', - 'theme.secondary': 'Secondary color', - 'custom.primary_hue': 'Primary hue', - 'custom.secondary_hue': 'Secondary hue', - 'custom.base_saturation': 'Base saturation', - 'custom.theme_mode': 'Theme brightness', - 'custom.dark': 'Dark', - 'custom.light': 'Light', - 'custom.background_mode': 'What color should the background have?', - 'custom.none': 'None', - 'custom.colored': 'Primary color', - 'theme.apply': 'Apply your theme', + //* English US + 'en_US': { + // Theme settings + 'theme.presets': 'Presets', + 'theme.default_dark': 'Default dark', + 'theme.default_light': 'Default light', + 'theme.winter': 'Winter', + 'theme.custom': 'Create your own', + 'theme.custom.title': 'Custom theme', + 'theme.primary': 'Primary color', + 'theme.secondary': 'Secondary color', + 'custom.primary_hue': 'Primary hue', + 'custom.secondary_hue': 'Secondary hue', + 'custom.base_saturation': 'Base saturation', + 'custom.theme_mode': 'Theme brightness', + 'custom.dark': 'Dark', + 'custom.light': 'Light', + 'custom.background_mode': 'What color should the background have?', + 'custom.none': 'None', + 'custom.colored': 'Primary color', + 'theme.apply': 'Apply your theme', - // Chat theme settings - 'appearance.chat.theme': 'Chat theme', - 'appearance.chat.theme.material': 'Material', - 'appearance.chat.theme.bubbles': 'Chat bubbles', - }, - }; + // Chat theme settings + 'appearance.chat.dot_amount.title': 'Choose how many dots appear', + 'appearance.chat.dot_amount': 'Amount of dots', + 'appearance.chat.theme': 'Chat theme', + 'appearance.chat.theme.material': 'Material', + 'appearance.chat.theme.bubbles': 'Chat bubbles', + }, + }; } diff --git a/lib/translations/settings/tr_general_settings.dart b/lib/translations/settings/tr_general_settings.dart index 134f9104..177ee5e4 100644 --- a/lib/translations/settings/tr_general_settings.dart +++ b/lib/translations/settings/tr_general_settings.dart @@ -3,69 +3,69 @@ import 'package:get/get.dart'; class GeneralSettingsTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // Categories - 'settings.tab.account': 'Account', - 'settings.tab.appearance': 'Appearance', - 'settings.tab.app': 'App', - 'settings.tab.security': 'Security', - 'settings.tab.town': 'Your town', - 'settings.town': 'Town', - 'settings.town.desc': 'The capabilities of your town.', - 'settings.accounts': 'Accounts', - 'settings.accounts.desc': 'Manage all the accounts in your town.', - 'settings.data': 'Data', - 'settings.data.desc': 'Manage your account.', - 'settings.profile': 'Profile', - 'settings.profile.desc': '', - 'settings.security': 'Security', - 'settings.devices': 'Devices', - 'settings.camera': 'Camera', - 'settings.camera.desc': 'Try out your camera.', - 'settings.general': 'General', - 'settings.general.desc': 'Language, notifications and more.', - 'settings.audio': 'Audio', - 'settings.audio.desc': 'Microphone & headphones.', - 'settings.notifications': 'Notifications', - 'settings.colors': 'Colors', - 'settings.colors.desc': 'The colors of the app.', - 'settings.call_app': 'Call appearance', - 'settings.requests': 'Friend requests', - 'settings.encryption': 'Encryption', - 'settings.tabletop': 'Tabletop', - 'settings.tabletop.desc': 'Smooth scrolling and more.', - 'settings.chat': 'Chat', - 'settings.chat.desc': 'How the messages look.', - 'settings.files': 'Files', - 'settings.files.desc': 'How files are stored.', - 'settings.invites': 'Invites', - 'settings.invites.desc': 'Invite people to Liphium.', - 'settings.trusted_links': 'Trusted Links', - 'settings.trusted_links.desc': 'The websites you trust.', - 'settings.invites.title': 'You have @count invites left.', - 'settings.invites.title.admin': 'You have unlimited invites left.', - 'settings.experimental': 'Experimental', - 'settings.logging': 'Logging', - 'settings.logging.desc': 'The logs the app collects.', - 'settings.authentication': 'Authentication', - 'settings.authentication.desc': 'Setup multi-factor-authentication.', + //* English US + 'en_US': { + // Categories + 'settings.tab.account': 'Account', + 'settings.tab.appearance': 'Appearance', + 'settings.tab.app': 'App', + 'settings.tab.security': 'Security', + 'settings.tab.town': 'Your town', + 'settings.town': 'Town', + 'settings.town.desc': 'The capabilities of your town.', + 'settings.accounts': 'Accounts', + 'settings.accounts.desc': 'Manage all the accounts in your town.', + 'settings.data': 'Data', + 'settings.data.desc': 'Manage your account.', + 'settings.profile': 'Profile', + 'settings.profile.desc': '', + 'settings.security': 'Security', + 'settings.devices': 'Devices', + 'settings.camera': 'Camera', + 'settings.camera.desc': 'Try out your camera.', + 'settings.general': 'General', + 'settings.general.desc': 'Language, notifications and more.', + 'settings.audio': 'Audio', + 'settings.audio.desc': 'Microphone & headphones.', + 'settings.notifications': 'Notifications', + 'settings.colors': 'Colors', + 'settings.colors.desc': 'The colors of the app.', + 'settings.call_app': 'Call appearance', + 'settings.requests': 'Friend requests', + 'settings.encryption': 'Encryption', + 'settings.tabletop': 'Tabletop', + 'settings.tabletop.desc': 'Smooth scrolling and more.', + 'settings.chat': 'Chat', + 'settings.chat.desc': 'How the messages look.', + 'settings.files': 'Files', + 'settings.files.desc': 'How files are stored.', + 'settings.invites': 'Invites', + 'settings.invites.desc': 'Invite people to Liphium.', + 'settings.trusted_links': 'Trusted Links', + 'settings.trusted_links.desc': 'The websites you trust.', + 'settings.invites.title': 'You have @count invites left.', + 'settings.invites.title.admin': 'You have unlimited invites left.', + 'settings.experimental': 'Experimental', + 'settings.logging': 'Logging', + 'settings.logging.desc': 'The logs the app collects.', + 'settings.authentication': 'Authentication', + 'settings.authentication.desc': 'Setup multi-factor-authentication.', - // Trusted links - 'links.warning': - 'This an advanced section. Changing the default behavior of the app might result in leaks of your data or other various things. Only change things here if you know what you\'re doing.', - 'links.locations': 'Settings for locations', - 'links.unsafe_sources': 'Allow accessing resources from unsafe locations (e.g. websites with HTTP)', - 'links.trusted_domains': 'Trusted domains', - 'links.trust_mode': 'Select which domains you want to trust.', - 'links.trust_mode.all': 'All domains', - 'links.trust_mode.list_verified': 'A verified list of providers', - 'links.trust_mode.list': 'A custom list of domains (defined below)', - 'links.trust_mode.none': 'No domains', - 'links.trusted_list': 'Here\'s the list of domains you trust.', - 'links.trusted_list.add': 'Add a trusted domain', - 'links.trusted_list.placeholder': 'liphium.app', - 'links.trusted_list.empty': 'You currently don\'t trust any domains.', - }, - }; + // Trusted links + 'links.warning': + 'This an advanced section. Changing the default behavior of the app might result in leaks of your data or other various things. Only change things here if you know what you\'re doing.', + 'links.locations': 'Settings for locations', + 'links.unsafe_sources': 'Allow accessing resources from unsafe locations (e.g. websites with HTTP)', + 'links.trusted_domains': 'Trusted domains', + 'links.trust_mode': 'Select which domains you want to trust.', + 'links.trust_mode.all': 'All domains', + 'links.trust_mode.list_verified': 'A verified list of providers', + 'links.trust_mode.list': 'A custom list of domains (defined below)', + 'links.trust_mode.none': 'No domains', + 'links.trusted_list': 'Here\'s the list of domains you trust.', + 'links.trusted_list.add': 'Add a trusted domain', + 'links.trusted_list.placeholder': 'liphium.app', + 'links.trusted_list.empty': 'You currently don\'t trust any domains.', + }, + }; } diff --git a/lib/translations/settings/tr_town_settings.dart b/lib/translations/settings/tr_town_settings.dart index 206e9412..298fdd78 100644 --- a/lib/translations/settings/tr_town_settings.dart +++ b/lib/translations/settings/tr_town_settings.dart @@ -3,86 +3,88 @@ import 'package:get/get.dart'; class TownSettingTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // Town management - 'settings.town.info': 'Town info', - 'settings.town.own_town': 'Info about your own town', - 'settings.town.own_town.desc': 'Connected to @domain on version @version (protocol version: @protocol)', - 'settings.town.address': 'Your address', - 'settings.town.address.desc': 'This address can be used to add you as a friend by people outside of your town.', - 'settings.town.address.copied': 'Your address has been copied. Anyone can use it add you as a friend.', - 'settings.town.settings': 'Town settings', - 'settings.town.help': 'Get help with your town setup', - 'settings.town.help.desc': - 'Have any questions about your town or just want to read a little bit about the interals of Liphium? You can find all of it in our documentation for contributors & town admins. Everything like how to set up a town and even how some of Liphium works can be found there. You\'ll also find migration guides and more there as well.', + //* English US + 'en_US': { + // Town management + 'settings.town.info': 'Town info', + 'settings.town.own_town': 'Info about your own town', + 'settings.town.own_town.desc': 'Connected to @domain on version @version (protocol version: @protocol)', + 'settings.town.address': 'Your address', + 'settings.town.address.desc': 'This address can be used to add you as a friend by people outside of your town.', + 'settings.town.address.copied': 'Your address has been copied. Anyone can use it add you as a friend.', + 'settings.town.settings': 'Town settings', + 'settings.town.help': 'Get help with your town setup', + 'settings.town.help.desc': + 'Have any questions about your town or just want to read a little bit about the interals of Liphium? You can find all of it in our documentation for contributors & town admins. Everything like how to set up a town and even how some of Liphium works can be found there. You\'ll also find migration guides and more there as well.', - // Admin accounts page - 'settings.accounts.count': 'Accounts created (@count)', - 'settings.accounts.none': 'No accounts found.', - 'settings.accounts.created': 'Created on @date at @time', - 'settings.accounts.delete.confirm': 'Do you really want to delete this account?', - 'settings.accounts.delete.desc': - 'This will get rid of every last thing they uploaded to your town. Please understand that conversations, messages and all chat-related content can only be deleted by the person themself because Liphium doesn\'t know which conversations you are a part of.', - 'settings.accounts.search': 'Search accounts', + // Admin accounts page + 'settings.accounts.count': 'Accounts created (@count)', + 'settings.accounts.none': 'No accounts found.', + 'settings.accounts.created': 'Created on @date at @time', + 'settings.accounts.delete.confirm': 'Do you really want to delete this account?', + 'settings.accounts.delete.desc': + 'This will get rid of every last thing they uploaded to your town. Please understand that conversations, messages and all chat-related content can only be deleted by the person themself because Liphium doesn\'t know which conversations you are a part of.', + 'settings.accounts.search': 'Search accounts', - // Admin account profile - 'settings.acc_profile.title': 'Profile for @name', - 'settings.acc_profile.tab.info': 'Info', - 'settings.acc_profile.tab.actions': 'Actions', - 'settings.acc_profile.info.id': 'Account ID', - 'settings.acc_profile.info.email': 'Email address', - 'settings.acc_profile.info.username': 'Username', - 'settings.acc_profile.info.display_name': 'Display name', - 'settings.rank_change.desc': - 'Select one of the ranks below to be the new rank of the user. The permission level of the rank is in the brackets behind the name.', + // Admin account profile + 'settings.acc_profile.title': 'Profile for @name', + 'settings.acc_profile.tab.info': 'Info', + 'settings.acc_profile.tab.actions': 'Actions', + 'settings.acc_profile.info.id': 'Account ID', + 'settings.acc_profile.info.email': 'Email address', + 'settings.acc_profile.info.username': 'Username', + 'settings.acc_profile.info.display_name': 'Display name', + 'settings.rank_change.desc': + 'Select one of the ranks below to be the new rank of the user. The permission level of the rank is in the brackets behind the name.', - // Tabletop settings - 'settings.tabletop.decks': 'Decks', - 'settings.tabletop.decks.error': - 'An error occurred while loading your decks. This is probably something you\'ll need to report to us or it\'s just your connection. You can also try to see if there\'s a new version of the app available or try again later.', - 'settings.tabletop.decks.limit': 'Decks (@count/@limit)', - 'decks.description': - 'Decks allow you to instantly add a whole bunch of cards to a tabletop session. If you have a pack of cards you want to use often, create a deck for it!', - 'decks.create': 'Create a new deck', - 'decks.dialog.delete.title': 'Delete deck', - 'decks.dialog.delete': 'Are you sure you want to delete this deck? Think about all the cards you\'ll lose!', - 'decks.dialog.new_name': 'Type a new name for your deck here. This won\'t delete the cards in it, it\'ll just change the name.', - 'decks.dialog.name': 'First of all, please give your deck a nice name. You know, something actually good.', - 'decks.dialog.name.placeholder': 'Deck name', - 'decks.dialog.name.error': 'Please make the name for your deck longer than 3 characters.', - 'decks.limit_reached': - 'You have reached the maximum amount of decks you can create. Please delete one of your existing decks to create a new one.', - 'decks.cards': '@count cards', - 'decks.view_cards': 'View cards', - 'decks.cards.empty': 'This deck is empty. You can add cards to it by clicking the button above.', - 'settings.tabletop.general': 'General', - 'tabletop.general.framerate': 'Framerate', - 'tabletop.general.framerate.description': - 'The framerate at which the table is rendered. This should be roughly equivalent to the refresh rate of your monitor.', - 'tabletop.general.framerate.unit': 'Hz', - 'tabletop.general.color': 'The color of your cursor', - 'tabletop.general.color.description': 'This will be the color everyone sees when you are selecting something or moving your cursor.', + // Tabletop settings + 'settings.tabletop.decks': 'Decks', + 'settings.tabletop.decks.error': + 'An error occurred while loading your decks. This is probably something you\'ll need to report to us or it\'s just your connection. You can also try to see if there\'s a new version of the app available or try again later.', + 'settings.tabletop.decks.limit': 'Decks (@count/@limit)', + 'decks.description': + 'Decks allow you to instantly add a whole bunch of cards to a tabletop session. If you have a pack of cards you want to use often, create a deck for it!', + 'decks.create': 'Create a new deck', + 'decks.dialog.delete.title': 'Delete deck', + 'decks.dialog.delete': 'Are you sure you want to delete this deck? Think about all the cards you\'ll lose!', + 'decks.dialog.new_name': + 'Type a new name for your deck here. This won\'t delete the cards in it, it\'ll just change the name.', + 'decks.dialog.name': 'First of all, please give your deck a nice name. You know, something actually good.', + 'decks.dialog.name.placeholder': 'Deck name', + 'decks.dialog.name.error': 'Please make the name for your deck longer than 3 characters.', + 'decks.limit_reached': + 'You have reached the maximum amount of decks you can create. Please delete one of your existing decks to create a new one.', + 'decks.cards': '@count cards', + 'decks.view_cards': 'View cards', + 'decks.cards.empty': 'This deck is empty. You can add cards to it by clicking the button above.', + 'settings.tabletop.general': 'General', + 'tabletop.general.framerate': 'Framerate', + 'tabletop.general.framerate.description': + 'The framerate at which the table is rendered. This should be roughly equivalent to the refresh rate of your monitor.', + 'tabletop.general.framerate.unit': 'Hz', + 'tabletop.general.color': 'The color of your cursor', + 'tabletop.general.color.description': + 'This will be the color everyone sees when you are selecting something or moving your cursor.', - // File settings - 'auto_download.images': 'Automatically download images', - 'auto_download.videos': 'Automatically download videos', - 'auto_download.audio': 'Automatically download audio', - 'settings.file.auto_download.types': 'Types of files to automatically download', - 'settings.file.max_size': 'Maximum file size for automatic downloads', - 'settings.file.max_size.description': 'Files larger than this will not be downloaded automatically.', - 'settings.file.cache': 'File cache', - 'settings.file.cache.description': - 'The file cache stores all files that have been automatically downloaded. This includes profile pictures and all other data you\'ve selected above. When it is full old files will automatically be deleted. You can select the size with the slider below or make it unlimited.', - 'settings.file.cache_type.unlimited': 'Unlimited', - 'settings.file.cache_type.size': 'Size', - 'settings.file.cache.open_cache': 'Open cache folder', - 'settings.file.cache.open_files': 'Open file folder', - 'settings.file.cache.open_saved_files': 'Open save folder', - 'settings.file.uploaded.title': 'Uploaded files (@count)', - 'settings.file.uploaded.description': 'You are currently using @current out of your available @max.', - 'settings.file.uploaded.none': 'No uploaded files. Try to send messages or create a deck to upload files.', - 'settings.file.mb': 'MB', - }, - }; + // File settings + 'auto_download.images': 'Automatically download images', + 'auto_download.videos': 'Automatically download videos', + 'auto_download.audio': 'Automatically download audio', + 'settings.file.auto_download.types': 'Types of files to automatically download', + 'settings.file.max_size': 'Maximum file size for automatic downloads', + 'settings.file.max_size.description': 'Files larger than this will not be downloaded automatically.', + 'settings.file.cache': 'File cache', + 'settings.file.cache.description': + 'The file cache stores all files that have been automatically downloaded. This includes profile pictures and all other data you\'ve selected above. When it is full old files will automatically be deleted. You can select the size with the slider below or make it unlimited.', + 'settings.file.cache_type.unlimited': 'Unlimited', + 'settings.file.cache_type.size': 'Size', + 'settings.file.cache.open_cache': 'Open cache folder', + 'settings.file.cache.open_files': 'Open file folder', + 'settings.file.cache.open_saved_files': 'Open save folder', + 'settings.file.uploaded.title': 'Uploaded files (@count)', + 'settings.file.uploaded.description': 'You are currently using @current out of your available @max.', + 'settings.file.uploaded.none': 'No uploaded files. Try to send messages or create a deck to upload files.', + 'settings.file.mb': 'MB', + }, + }; } diff --git a/lib/translations/setup.dart b/lib/translations/setup.dart index 6b05b2b6..c7bfdad2 100644 --- a/lib/translations/setup.dart +++ b/lib/translations/setup.dart @@ -3,59 +3,60 @@ import 'package:get/get.dart'; class SetupTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // Error page - 'retry.text.1': 'Trying again in', - 'retry.text.2': 'seconds.', + //* English US + 'en_US': { + // Error page + 'retry.text.1': 'Trying again in', + 'retry.text.2': 'seconds.', - // General setup - 'setup.choose.instance': 'Choose an instance.', - 'setup.instance.name': 'Enter a new name', - 'setup.choose.town': 'Choose a town.', - 'setup.choose.town.desc': - 'A town is the place where you create your Liphium account. If you don\'t know any town, you\'re out of luck until more options are available. For now, you can click the link below to learn more.', - 'setup.choose.town.selector': 'Enter the domain of your town', - 'setup.policy': 'Your privacy on Liphium.', - 'setup.policy.text': - 'By pressing \'Accept\', you acknowledge that you have carefully reviewed and accepted our Privacy Policy and Terms of Service which you can read by clicking on \'View agreements\' below, after which the \'Accept\' button will appear.', - 'setup.policy.error': - 'It seems like we couldn\'t open a browser on your device. Please check your internet connection or contact the developers of this app.', + // General setup + 'setup.choose.instance': 'Choose an instance.', + 'setup.instance.name': 'Enter a new name', + 'setup.choose.town': 'Choose a town.', + 'setup.choose.town.desc': + 'A town is the place where you create your Liphium account. If you don\'t know any town, you\'re out of luck until more options are available. For now, you can click the link below to learn more.', + 'setup.choose.town.selector': 'Enter the domain of your town', + 'setup.policy': 'Your privacy on Liphium.', + 'setup.policy.text': + 'By pressing \'Accept\', you acknowledge that you have carefully reviewed and accepted our Privacy Policy and Terms of Service which you can read by clicking on \'View agreements\' below, after which the \'Accept\' button will appear.', + 'setup.policy.error': + 'It seems like we couldn\'t open a browser on your device. Please check your internet connection or contact the developers of this app.', - // Login/register - 'register.title': 'Register an account.', - 'placeholder.username': 'test123', - 'placeholder.display_name': 'Test 123', - 'placeholder.password': 'yourmum123 (don\'t use this)', - 'placeholder.invite': 'your-invite-code', - 'password.invalid': 'Please enter a password that is longer than 8 characters.', - 'invite.invalid': 'Please enter a valid invite code.', - 'invite.info': 'Invite codes are your way to get into the app. You can get one from a friend or from official sources.', - 'placeholder.email': 'your@email.com', - 'register.verify': 'Verify your email.', - 'register.final': 'Finish your account.', - 'register.email_validation': - 'We sent an email to @email. Please check your inbox and put the code we sent you into the input box below. Oh, and don\'t forget to check the spam folder!', - 'placeholder.code': 'abcdef', - 'email.invalid': 'Please enter a valid email.', - 'register.register': 'Register', - 'register.account.text': 'Already have an account?', - 'register.login': 'Login instead', - 'input.email': 'Your email, please', - 'login.next': 'Next step', - 'login.register_reminder': 'Don\'t have an account? There is a register button below the next button.', - 'login.no_account': 'Register an account', - 'input.password': 'Your password, please', - 'login.forgot': 'Reset your password', + // Login/register + 'register.title': 'Register an account.', + 'placeholder.username': 'test123', + 'placeholder.display_name': 'Test 123', + 'placeholder.password': 'yourmum123 (don\'t use this)', + 'placeholder.invite': 'your-invite-code', + 'password.invalid': 'Please enter a password that is longer than 8 characters.', + 'invite.invalid': 'Please enter a valid invite code.', + 'invite.info': + 'Invite codes are your way to get into the app. You can get one from a friend or from official sources.', + 'placeholder.email': 'your@email.com', + 'register.verify': 'Verify your email.', + 'register.final': 'Finish your account.', + 'register.email_validation': + 'We sent an email to @email. Please check your inbox and put the code we sent you into the input box below. Oh, and don\'t forget to check the spam folder!', + 'placeholder.code': 'abcdef', + 'email.invalid': 'Please enter a valid email.', + 'register.register': 'Register', + 'register.account.text': 'Already have an account?', + 'register.login': 'Login instead', + 'input.email': 'Your email, please', + 'login.next': 'Next step', + 'login.register_reminder': 'Don\'t have an account? There is a register button below the next button.', + 'login.no_account': 'Register an account', + 'input.password': 'Your password, please', + 'login.forgot': 'Reset your password', - // Key setup - 'key.sync.title': 'Your keys aren\'t synchronized.', - 'key.sync.desc': - 'If you are logging in for the first time on this device or changed your keys, this is completely normal. You can ask another device to grab the keys from there, don\'t worry, we\'ll encrypt them in transfer.', - 'key.sync.ask_device': 'Ask another device', - 'key.code': 'Code: @code', - 'key.code.desc': - 'On any device where you are currently logged in, go to Settings > Data > Synchronization requests. Click on the correct request and then input the code above into the dialog that pops up. We\'ll check if you did automatically.', - }, - }; + // Key setup + 'key.sync.title': 'Your keys aren\'t synchronized.', + 'key.sync.desc': + 'If you are logging in for the first time on this device or changed your keys, this is completely normal. You can ask another device to grab the keys from there, don\'t worry, we\'ll encrypt them in transfer.', + 'key.sync.ask_device': 'Ask another device', + 'key.code': 'Code: @code', + 'key.code.desc': + 'On any device where you are currently logged in, go to Settings > Data > Synchronization requests. Click on the correct request and then input the code above into the dialog that pops up. We\'ll check if you did automatically.', + }, + }; } diff --git a/lib/translations/spaces.dart b/lib/translations/spaces.dart index d19878f3..202f0818 100644 --- a/lib/translations/spaces.dart +++ b/lib/translations/spaces.dart @@ -3,74 +3,84 @@ import 'package:get/get.dart'; class SpacesTranslations extends Translations { @override Map> get keys => { - //* English US - 'en_US': { - // General - 'spaces.already_calling': 'You are already in a Space. Leave it if you want to open a new one.', - 'spaces.calling': 'is calling..', - 'spaces.sharing_other_device': 'Sharing with friends', - 'spaces.count': '@count members', - 'spaces.toggle_people': 'Toggle showing people', - 'spaces.tab.space': 'Space', - 'spaces.tab.table': 'Tabletop', - 'spaces.sidebar.chat': 'Chat', - 'spaces.sidebar.people': 'People', - 'spaces.member.not_verified': 'Identity could not be verified.', + //* English US + 'en_US': { + // General + 'spaces.already_calling': 'You are already in a Space. Leave it if you want to open a new one.', + 'spaces.calling': 'is calling..', + 'spaces.sharing_other_device': 'Sharing with friends', + 'spaces.count': '@count members', + 'spaces.toggle_people': 'Toggle showing people', + 'spaces.tab.space': 'Space', + 'spaces.tab.table': 'Tabletop', + 'spaces.sidebar.chat': 'Chat', + 'spaces.sidebar.people': 'People', + 'spaces.member.not_verified': 'Identity could not be verified.', - // Welcome screen - 'spaces.welcome': 'Welcome to this Space!', - 'spaces.welcome.desc': - 'Spaces is Liphium\'s version of a temporary gathering where you can do all kinds of things with your friends. Click the arrow in the bottom right to open the chat. Click "Tabletop" in the tab selector right above this text to enjoy our Tabletop emulator for playing card games. Have fun!', + // Welcome screen + 'spaces.welcome': 'Welcome to this Space!', + 'spaces.welcome.desc': + 'Spaces is Liphium\'s version of a temporary gathering where you can do all kinds of things with your friends. Click the arrow in the bottom right to open the chat. Click "Tabletop" in the tab selector right above this text to enjoy our Tabletop emulator for playing card games. Have fun!', - // Warp - 'warp.title': 'Warp', - 'warp.desc': 'Warp is Liphium\'s way to share stuff like Minecraft servers and more. You\'ll need the server\'s port though.', - 'warp.share': 'Create a Warp', + // Warp + 'warp.title': 'Warp', + 'warp.desc': + 'Warp is Liphium\'s way to share stuff like Minecraft servers and more. You\'ll need the server\'s port though.', + 'warp.share': 'Create a Warp', - // Translations for the Warp creation window - 'warp.create.title': 'Create a Warp', - 'warp.create.desc': - 'When you create a Warp, that port on your system will be accessible to all people and devices in the Space. Please make sure to not share important stuff.', - 'warp.port.placeholder': '25565 (default MC port)', - 'warp.create.button': 'Share this port', - 'warp.error.port_invalid': 'A port can only be between 1024 and 65535, no higher and no lower.', - 'warp.error.port_not_used': 'This port can\'t be shared because there is no server on it.', + // Translations for the Warp creation window + 'warp.create.title': 'Create a Warp', + 'warp.create.desc': + 'When you create a Warp, that port on your system will be accessible to all people and devices in the Space. Please make sure to not share important stuff.', + 'warp.port.placeholder': '25565 (default MC port)', + 'warp.create.button': 'Share this port', + 'warp.error.port_invalid': 'A port can only be between 1024 and 65535, no higher and no lower.', + 'warp.error.port_not_used': 'This port can\'t be shared because there is no server on it.', + 'warp.error.port_already_shared': 'You are already sharing this port.', - // Translations for currently shared Warps - 'warp.shared.title': 'Shared Warps', + // Translations for currently shared Warps + 'warp.shared.title': 'Shared Warps', - // Translations for currently connected Warps - 'warp.connected.title': 'Connected Warps', - 'warp.connected.item': '@origin > @goal', + // Translations for currently connected Warps + 'warp.connected.title': 'Connected Warps', + 'warp.connected.item': '@origin > @goal', - // Translations for Warps that are listed - 'warp.list.sharing': '@name is sharing..', - 'warp.list.empty': 'No shared Warps found.', + // Translations for Warps that are listed + 'warp.list.sharing': '@name is sharing..', + 'warp.list.empty': 'No shared Warps found.', - // Game hub - 'game.lobby': 'Ready to start. (@count/@max)', - 'game.lobby_waiting': 'Waiting for more players. (@count/@min)', + // Game hub + 'game.lobby': 'Ready to start. (@count/@max)', + 'game.lobby_waiting': 'Waiting for more players. (@count/@min)', + + // Tabletop + 'tabletop.object.create': 'Create object', + 'tabletop.object.deck': 'Deck', + 'tabletop.object.deck.choose': 'Choose a deck', + 'tabletop.object.deck.choose_empty': 'No decks available. You can create one in the settings.', + 'tabletop.match_viewport': 'Rotate to viewport', + 'tabletop.object.text': 'Text', + 'tabletop.object.text.create': 'Create text object', + 'tabletop.object.text.placeholder': 'Enter text here', + 'tabletop.object.deck.incompatible': + 'This deck is incompatible with the newest version of the standard. Please create it again and try again.', + + // Space Studio + 'spaces.studio.connecting': 'Connecting to Studio..', - // Tabletop - 'tabletop.object.create': 'Create object', - 'tabletop.object.deck': 'Deck', - 'tabletop.object.deck.choose': 'Choose a deck', - 'tabletop.object.deck.choose_empty': 'No decks available. You can create one in the settings.', - 'tabletop.match_viewport': 'Rotate to viewport', - 'tabletop.object.text': 'Text', - 'tabletop.object.text.create': 'Create text object', - 'tabletop.object.text.placeholder': 'Enter text here', - 'tabletop.object.deck.incompatible': - 'This deck is incompatible with the newest version of the standard. Please create it again and try again.', - }, + // Media profiles + 'media_profile.static': 'Static', + 'media_profile.motion': 'Motion', + 'media_profile.balanced': 'Balanced', + }, - //* German - 'de_DE': { - /* + //* German + 'de_DE': { + /* // Game hub 'game.lobby': 'Bereit zum Start. (@count/@max)', 'game.lobby_waiting': 'Warte auf mehr Spieler. (@count/@min)', */ - }, - }; + }, + }; } diff --git a/lib/translations/squares.dart b/lib/translations/squares.dart new file mode 100644 index 00000000..f42624e2 --- /dev/null +++ b/lib/translations/squares.dart @@ -0,0 +1,21 @@ +import 'package:get/get.dart'; + +class SquareTranslations extends Translations { + @override + Map> get keys => { + //* English US + 'en_US': { + // Square management + 'squares.create': 'Create Square', + 'squares.name.placeholder': 'Square name', + 'squares.topics.create': 'Create topic', + 'squares.topics.edit': 'Edit topic', + 'squares.topics.name.placeholder': 'Some chat', + 'squares.topics.delete': 'Delete topic', + 'squares.spaces.create': 'Create new Space', + 'squares.spaces.add': 'Add current Space', + 'squares.spaces.edit': 'Edit Space', + 'squares.spaces.name.placeholder': 'Hangout #1', + }, + }; +} diff --git a/lib/translations/translations.dart b/lib/translations/translations.dart index ffb77c2b..f97b831e 100644 --- a/lib/translations/translations.dart +++ b/lib/translations/translations.dart @@ -8,6 +8,7 @@ import 'package:chat_interface/translations/settings/tr_general_settings.dart'; import 'package:chat_interface/translations/settings/tr_town_settings.dart'; import 'package:chat_interface/translations/setup.dart'; import 'package:chat_interface/translations/spaces.dart'; +import 'package:chat_interface/translations/squares.dart'; import 'package:get/get.dart'; class MainTranslations extends Translations { @@ -17,6 +18,7 @@ class MainTranslations extends Translations { GeneralTranslations(), SetupTranslations(), ErrorTranslations(), + SquareTranslations(), ChatPageTranslations(), SpacesTranslations(), diff --git a/lib/util/constants.dart b/lib/util/constants.dart index 63e709a6..c919e7f3 100644 --- a/lib/util/constants.dart +++ b/lib/util/constants.dart @@ -37,5 +37,5 @@ class Constants { // Documentation static const String docsAdminBase = "https://docs.liphium.com"; static const String docsBase = "https://liphium.com/docs"; - static const String docsUsageFAQ = "https://liphium.com/docs/usage/faq"; + static const String docsEncryptionAndPrivacy = "https://liphium.dev/encryption"; } diff --git a/lib/util/dispose_hook.dart b/lib/util/dispose_hook.dart new file mode 100644 index 00000000..cade0e72 --- /dev/null +++ b/lib/util/dispose_hook.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:signals/signals_flutter.dart'; + +class DisposeHook extends StatefulWidget { + final Function() dispose; + final Widget child; + + const DisposeHook({super.key, required this.dispose, required this.child}); + + @override + State createState() => _DisposeHookState(); +} + +class _DisposeHookState extends State { + @override + void dispose() { + widget.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} + +/// Create a Signal of type T that will be disposed automatically. +class SignalHook extends StatefulWidget { + final T value; + final Widget Function(Signal) builder; + + const SignalHook({super.key, required this.value, required this.builder}); + + @override + State> createState() => _SignalHookState(); +} + +class _SignalHookState extends State> with SignalsMixin { + late final signal = createSignal(widget.value); + + @override + Widget build(BuildContext context) { + return widget.builder(signal); + } +} diff --git a/lib/connection/encryption/aes.dart b/lib/util/encryption/aes.dart similarity index 100% rename from lib/connection/encryption/aes.dart rename to lib/util/encryption/aes.dart diff --git a/lib/connection/encryption/asymmetric_sodium.dart b/lib/util/encryption/asymmetric_sodium.dart similarity index 91% rename from lib/connection/encryption/asymmetric_sodium.dart rename to lib/util/encryption/asymmetric_sodium.dart index 689ed169..4b2af25f 100644 --- a/lib/connection/encryption/asymmetric_sodium.dart +++ b/lib/util/encryption/asymmetric_sodium.dart @@ -16,7 +16,12 @@ String encryptAsymmetricAuth(Uint8List publicKey, SecureKey secureKey, String me final plainTextBytes = message.toCharArray().unsignedView(); final nonce = sodium.randombytes.buf(sodium.crypto.secretBox.nonceBytes); - final encrypted = sodium.crypto.box.easy(message: plainTextBytes, nonce: nonce, publicKey: publicKey, secretKey: secureKey); + final encrypted = sodium.crypto.box.easy( + message: plainTextBytes, + nonce: nonce, + publicKey: publicKey, + secretKey: secureKey, + ); return base64Encode(nonce + encrypted); } @@ -79,11 +84,9 @@ String decryptAsymmetricAnonymous(Uint8List publicKey, SecureKey secretKey, Stri final cipherText = base64Decode(message); var decrypted = ""; try { - decrypted = utf8.decode(sodium.crypto.box.sealOpen( - cipherText: cipherText, - publicKey: publicKey, - secretKey: secretKey, - )); + decrypted = utf8.decode( + sodium.crypto.box.sealOpen(cipherText: cipherText, publicKey: publicKey, secretKey: secretKey), + ); } catch (e) { sendLog("WARNING: couldn't decrypt message"); return ""; @@ -110,8 +113,5 @@ bool verifySignature(Uint8List publicKey, String signature, String message, [Sod */ KeyPair toKeyPair(String publicKey, String privateKey, [Sodium? sd]) { - return KeyPair( - publicKey: unpackagePublicKey(publicKey), - secretKey: unpackagePrivateKey(privateKey, sd), - ); + return KeyPair(publicKey: unpackagePublicKey(publicKey), secretKey: unpackagePrivateKey(privateKey, sd)); } diff --git a/lib/connection/encryption/hash.dart b/lib/util/encryption/hash.dart similarity index 100% rename from lib/connection/encryption/hash.dart rename to lib/util/encryption/hash.dart diff --git a/lib/connection/encryption/rsa.dart b/lib/util/encryption/rsa.dart similarity index 95% rename from lib/connection/encryption/rsa.dart rename to lib/util/encryption/rsa.dart index f3bba149..1f1ebe78 100644 --- a/lib/connection/encryption/rsa.dart +++ b/lib/util/encryption/rsa.dart @@ -46,7 +46,11 @@ String packageRSAPrivateKey(RSAPrivateKey key) { RSAPrivateKey unpackageRSAPrivateKey(String key) { final parts = key.split(":"); return RSAPrivateKey( - BigInt.parse(parts[0], radix: 36), BigInt.parse(parts[2], radix: 36), BigInt.parse(parts[3], radix: 36), BigInt.parse(parts[4], radix: 36)); + BigInt.parse(parts[0], radix: 36), + BigInt.parse(parts[2], radix: 36), + BigInt.parse(parts[3], radix: 36), + BigInt.parse(parts[4], radix: 36), + ); } /// Turn a public and private key into an [AsymmetricKeyPair]. diff --git a/lib/connection/encryption/signatures.dart b/lib/util/encryption/signatures.dart similarity index 86% rename from lib/connection/encryption/signatures.dart rename to lib/util/encryption/signatures.dart index 30587f14..d250ee44 100644 --- a/lib/connection/encryption/signatures.dart +++ b/lib/util/encryption/signatures.dart @@ -20,5 +20,9 @@ String signMessage(SecureKey privateKey, String message, [Sodium? sd]) { bool checkSignature(String signature, Uint8List publicKey, String message, [Sodium? sd]) { final Sodium sodium = sd ?? sodiumLib; final plainTextBytes = message.toCharArray().unsignedView(); - return sodium.crypto.sign.verifyDetached(signature: base64Decode(signature), message: plainTextBytes, publicKey: publicKey); + return sodium.crypto.sign.verifyDetached( + signature: base64Decode(signature), + message: plainTextBytes, + publicKey: publicKey, + ); } diff --git a/lib/connection/encryption/symmetric_sodium.dart b/lib/util/encryption/symmetric_sodium.dart similarity index 100% rename from lib/connection/encryption/symmetric_sodium.dart rename to lib/util/encryption/symmetric_sodium.dart diff --git a/lib/util/platform_callback.dart b/lib/util/platform_callback.dart index e1d115f0..16e8ea29 100644 --- a/lib/util/platform_callback.dart +++ b/lib/util/platform_callback.dart @@ -7,13 +7,7 @@ class PlatformCallback extends StatefulWidget { final bool preventDoubleCalling; final Widget child; - const PlatformCallback({ - super.key, - this.mobile, - this.desktop, - this.preventDoubleCalling = true, - required this.child, - }); + const PlatformCallback({super.key, this.mobile, this.desktop, this.preventDoubleCalling = true, required this.child}); @override State createState() => _PlatformCallbackState(); diff --git a/lib/util/vertical_spacing.dart b/lib/util/vertical_spacing.dart index 35835cfc..bdbdfc0e 100644 --- a/lib/util/vertical_spacing.dart +++ b/lib/util/vertical_spacing.dart @@ -4,10 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; -const noTextHeight = TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, -); +const noTextHeight = TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: false); Widget verticalSpacing(double height) { return SizedBox(height: height); @@ -27,6 +24,12 @@ bool isMobileMode() { return Get.width < 800 || Get.height < 500; } +void popAllAndPush(BuildContext context, Route route) { + final nav = Navigator.of(context); + nav.popUntil((_) => false); + nav.push(route); +} + double fittedIconSize(double size) { return Get.mediaQuery.textScaler.scale(size); } @@ -44,10 +47,7 @@ Future? showModal(Widget widget, {mobileSliding = false}) async { builder: (context) { return LayoutBuilder( builder: (context, constraints) { - return Padding( - padding: EdgeInsets.only(bottom: Get.mediaQuery.viewInsets.bottom), - child: widget, - ); + return Padding(padding: EdgeInsets.only(bottom: Get.mediaQuery.viewInsets.bottom), child: widget); }, ); }, @@ -75,17 +75,27 @@ String formatDay(DateTime time) { } else if (time.day == now.day - 1) { return "time.yesterday".tr; } else { - return "time" - .trParams({"day": time.day.toString().padLeft(2, "0"), "month": time.month.toString().padLeft(2, "0"), "year": time.year.toString()}); + return "time".trParams({ + "day": time.day.toString().padLeft(2, "0"), + "month": time.month.toString().padLeft(2, "0"), + "year": time.year.toString(), + }); } } String formatOnlyYear(DateTime time) { - return "time".trParams({"day": time.day.toString().padLeft(2, "0"), "month": time.month.toString().padLeft(2, "0"), "year": time.year.toString()}); + return "time".trParams({ + "day": time.day.toString().padLeft(2, "0"), + "month": time.month.toString().padLeft(2, "0"), + "year": time.year.toString(), + }); } String formatMessageTime(DateTime time) { - return "message.time".trParams({"hour": time.hour.toString().padLeft(2, "0"), "minute": time.minute.toString().padLeft(2, "0")}); + return "message.time".trParams({ + "hour": time.hour.toString().padLeft(2, "0"), + "minute": time.minute.toString().padLeft(2, "0"), + }); } String formatGeneralTime(DateTime time) { @@ -100,7 +110,8 @@ String formatGeneralTime(DateTime time) { class ExpandEffect extends CustomEffect { ExpandEffect({super.curve, super.duration, Axis? axis, Alignment? alignment, double? customHeightFactor, super.delay}) - : super(builder: (context, value, child) { + : super( + builder: (context, value, child) { return ClipRect( child: Align( alignment: alignment ?? Alignment.topCenter, @@ -109,23 +120,24 @@ class ExpandEffect extends CustomEffect { child: child, ), ); - }); + }, + ); } class ReverseExpandEffect extends CustomEffect { ReverseExpandEffect({super.curve, super.duration, Axis? axis, Alignment? alignment, super.delay}) - : super( - builder: (context, value, child) { - return ClipRect( - child: Align( - alignment: alignment ?? Alignment.topCenter, - heightFactor: axis == Axis.vertical ? max(1 - value, 0.0) : null, - widthFactor: axis == Axis.horizontal ? max(1 - value, 0.0) : null, - child: child, - ), - ); - }, - ); + : super( + builder: (context, value, child) { + return ClipRect( + child: Align( + alignment: alignment ?? Alignment.topCenter, + heightFactor: axis == Axis.vertical ? max(1 - value, 0.0) : null, + widthFactor: axis == Axis.horizontal ? max(1 - value, 0.0) : null, + child: child, + ), + ); + }, + ); } class DevicePadding extends StatelessWidget { @@ -177,9 +189,6 @@ class DevicePadding extends StatelessWidget { } } - return Padding( - padding: finalPadding, - child: child, - ); + return Padding(padding: finalPadding, child: child); } } diff --git a/lib/util/web.dart b/lib/util/web.dart index c1944995..ee9400c3 100644 --- a/lib/util/web.dart +++ b/lib/util/web.dart @@ -1,8 +1,8 @@ import 'dart:convert'; -import 'package:chat_interface/connection/connection.dart'; -import 'package:chat_interface/connection/encryption/aes.dart'; -import 'package:chat_interface/connection/encryption/rsa.dart'; +import 'package:chat_interface/services/connection/connection.dart'; +import 'package:chat_interface/util/encryption/aes.dart'; +import 'package:chat_interface/util/encryption/rsa.dart'; import 'package:chat_interface/database/trusted_links.dart'; import 'package:chat_interface/main.dart'; import 'package:chat_interface/pages/status/setup/server_setup.dart'; @@ -24,10 +24,7 @@ void loadTokensFromPayload(Map payload) { } String tokensToPayload() { - Map payload = { - 'token': sessionToken, - 'refresh_token': refreshToken, - }; + Map payload = {'token': sessionToken, 'refresh_token': refreshToken}; return jsonEncode(payload); } @@ -50,6 +47,10 @@ String authorizationValue() { return "Bearer $sessionToken"; } +String localeString(Locale locale) { + return "${locale.languageCode}_${locale.countryCode ?? "US"}"; +} + /// Get the path to your own server String ownServer(String path) { return '$basePath/$apiVersion$path'; @@ -92,7 +93,11 @@ class LPHAddress { // Needed for hashCode to work @override bool operator ==(Object other) => - identical(this, other) || other is LPHAddress && runtimeType == other.runtimeType && server == other.server && id == other.id; + identical(this, other) || + other is LPHAddress && + runtimeType == other.runtimeType && + TrustedLinkHelper.extractDomain(server) == TrustedLinkHelper.extractDomain(other.server) && + id == other.id; // So it works properly with HashMaps @override @@ -109,7 +114,11 @@ String serverPath(String server, String path, {bool noApiVersion = false}) { } /// Grab the public key from the server -Future grabServerPublicURL(String server, {String defaultError = "server.error", bool checkProtocol = true}) async { +Future grabServerPublicURL( + String server, { + String defaultError = "server.error", + bool checkProtocol = true, +}) async { final Response res; try { res = await post(Uri.parse(serverPath(server, "/pub", noApiVersion: true))); @@ -149,27 +158,13 @@ Future> postJSON( } /// Post request to any server (with Through Cloudflare Protection) -Future> postAddress(String server, String path, Map body, - {String defaultError = "server.error", String? token, bool noApiVersion = false, bool checkProtocol = true}) async { - // Try to get the server public key - if (serverPublicKeys[server] == null) { - final result = await grabServerPublicURL(server, checkProtocol: checkProtocol); - if (result != null) { - return { - "success": false, - "error": result, - }; - } - } - - // Do the request - return _postTCP(serverPublicKeys[server]!, serverPath(server, path, noApiVersion: noApiVersion).toString(), body, - defaultError: defaultError, token: token); -} - -/// Post request to any server (with Through Cloudflare Protection) -Future> _postTCP(RSAPublicKey key, String url, Map body, - {String defaultError = "server.error", String? token}) async { +Future> _postTCP( + RSAPublicKey key, + String url, + Map body, { + String defaultError = "server.error", + String? token, +}) async { final aesKey = randomAESKey(); final aesBase64 = base64Encode(aesKey); Response? res; @@ -203,8 +198,32 @@ Future> _postTCP(RSAPublicKey key, String url, Map> postAddress( + String server, + String path, + Map body, { + String defaultError = "server.error", + String? token, + bool noApiVersion = false, + bool checkProtocol = true, +}) async { + // Try to get the server public key + if (serverPublicKeys[server] == null) { + final result = await grabServerPublicURL(server, checkProtocol: checkProtocol); + if (result != null) { + return {"success": false, "error": result}; + } + } + + // Do the request + return _postTCP( + serverPublicKeys[server]!, + serverPath(server, path, noApiVersion: noApiVersion).toString(), + body, + defaultError: defaultError, + token: token, + ); } // Post request to node-backend with any token (new) @@ -213,12 +232,20 @@ Future> postAuthJSON(String path, Map body } // Post request to node-backend with session token (new) -Future> postAuthorizedJSON(String path, Map body, {bool checkProtocol = true}) async { +Future> postAuthorizedJSON( + String path, + Map body, { + bool checkProtocol = true, +}) async { return postJSON(path, body, token: sessionToken, checkProtocol: checkProtocol); } // Post request to chat-node with any token (node needs to be connected already) (new) -Future> postNodeJSON(String path, Map body, {String defaultError = "server.error"}) async { +Future> postNodeJSON( + String path, + Map body, { + String defaultError = "server.error", +}) async { if (connector.nodePublicKey == null) { return {"success": false, "error": defaultError.tr}; } @@ -229,19 +256,23 @@ Future> postNodeJSON(String path, Map body body["data"] ??= ""; } - return _postTCP(connector.nodePublicKey!, "${nodeProtocol()}$nodeDomain$path", body, defaultError: defaultError, token: sessionToken); + return _postTCP( + connector.nodePublicKey!, + "${nodeProtocol()}$nodeDomain$path", + body, + defaultError: defaultError, + token: sessionToken, + ); } // Post request to any domain -Future> postAny(String url, Map body, {String defaultError = "server.error"}) async { +Future> postAny( + String url, + Map body, { + String defaultError = "server.error", +}) async { try { - final res = await dio.post( - url, - data: jsonEncode(body), - options: d.Options( - validateStatus: (status) => true, - ), - ); + final res = await dio.post(url, data: jsonEncode(body), options: d.Options(validateStatus: (status) => true)); if (res.statusCode != 200) { return {"success": false, "error": defaultError.tr}; } @@ -267,9 +298,7 @@ String getSessionFromJWT(String token) { // Creates a stored action with the given name and payload String storedAction(String name, Map payload) { - final prefixJson = { - "a": name, - }; + final prefixJson = {"a": name}; prefixJson.addAll(payload); return jsonEncode(prefixJson); @@ -277,9 +306,7 @@ String storedAction(String name, Map payload) { // Creates an authenticated stored action with the given name and payload Map authenticatedStoredAction(String name, Map payload) { - final prefixJson = { - "a": name, - }; + final prefixJson = {"a": name}; prefixJson.addAll(payload); return prefixJson; diff --git a/libspaceship/.gitignore b/libspaceship/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/libspaceship/.gitignore @@ -0,0 +1 @@ +/target diff --git a/libspaceship/Cargo.lock b/libspaceship/Cargo.lock new file mode 100644 index 00000000..b3922832 --- /dev/null +++ b/libspaceship/Cargo.lock @@ -0,0 +1,1813 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "allo-isolate" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550" +dependencies = [ + "anyhow", + "atomic", + "backtrace", +] + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.9.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" +dependencies = [ + "android_log-sys", + "env_logger", + "log", + "once_cell", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "audiopus" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3743519567e9135cf6f9f1a509851cb0c8e4cb9d66feb286668afb1923bec458" +dependencies = [ + "audiopus_sys", +] + +[[package]] +name = "audiopus_sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927791de46f70facea982dbfaf19719a41ce6064443403be631a85de6a58fff9" +dependencies = [ + "log", + "pkg-config", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dart-sys" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" +dependencies = [ + "cc", +] + +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if", + "num_cpus", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "delegate-attr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flutter_rust_bridge" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8c0dee6249225e815dcff3f3a39b98d9f66fdb3c392a432715b646bfa4da02" +dependencies = [ + "allo-isolate", + "android_logger", + "anyhow", + "build-target", + "bytemuck", + "byteorder", + "console_error_panic_hook", + "dart-sys", + "delegate-attr", + "flutter_rust_bridge_macros", + "futures", + "js-sys", + "lazy_static", + "log", + "oslog", + "portable-atomic", + "threadpool", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "flutter_rust_bridge_macros" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e88d604908d9eccb4ca9c26640ce41033165cbef041460e704ae28bd5208bce" +dependencies = [ + "hex", + "md-5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-channel-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e5f4df964fa9c1c2f8bddeb5c3611631cacd93baf810fc8bb2fb4b495c263a" +dependencies = [ + "futures-core-preview", + "futures-sink-preview", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-core-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35b6263fb1ef523c3056565fa67b1d16f0a8604ff12b11b08c25f28a734c60a" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-executor-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75236e88bd9fe88e5e8bfcd175b665d0528fe03ca4c5207fabc028c8f9d93e98" +dependencies = [ + "futures-core-preview", + "futures-util-preview", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-io-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4914ae450db1921a56c91bde97a27846287d062087d4a652efc09bb3a01ebda" + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1dce2a0267ada5c6ff75a8ba864b4e679a9e2aa44262af7a3b5516d530d76e" +dependencies = [ + "futures-channel-preview", + "futures-core-preview", + "futures-executor-preview", + "futures-io-preview", + "futures-sink-preview", + "futures-util-preview", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-sink-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f148ef6b69f75bb610d4f9a2336d4fc88c4b5b67129d1a340dd0fd362efeec" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-timer" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9eb554aa23143abc64ec4d0016f038caf53bb7cbc3d91490835c54edc96550" +dependencies = [ + "futures-preview", + "pin-utils", +] + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "futures-util-preview" +version = "0.3.0-alpha.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce968633c17e5f97936bd2797b6e38fb56cf16a7422319f7ec2e30d3c470e8d" +dependencies = [ + "futures-channel-preview", + "futures-core-preview", + "futures-io-preview", + "futures-sink-preview", + "memchr", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "jittr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad552c6754ed07cc48a69aa771149331214a1c60ff5633a9853ef790204723d" +dependencies = [ + "futures", + "futures-timer", + "log", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libspaceship" +version = "0.1.0" +dependencies = [ + "audiopus", + "cpal", + "flutter_rust_bridge", + "jittr", + "lazy_static", + "rand", + "rodio", + "rubato", + "tokio", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.9.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oslog" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8343ce955f18e7e68c0207dd0ea776ec453035685395ababd2ea651c569728b3" +dependencies = [ + "cc", + "dashmap", + "log", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha", + "rand_core", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "realfft" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390252372b7f2aac8360fc5e72eba10136b166d6faeed97e6d0c8324eb99b2b1" +dependencies = [ + "rustfft", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rodio" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "symphonia", +] + +[[package]] +name = "rubato" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd96992d7e24b3d7f35fdfe02af037a356ac90d41b466945cf3333525a86eea" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/libspaceship/Cargo.toml b/libspaceship/Cargo.toml new file mode 100644 index 00000000..1d6b85d8 --- /dev/null +++ b/libspaceship/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "libspaceship" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +audiopus = "0.2.0" +cpal = "0.15.3" +flutter_rust_bridge = "=2.9.0" +jittr = "0.2.0" +lazy_static = "1.5.0" +rand = "0.9.0" +rodio = "0.20.1" +rubato = "0.16.1" +tokio = { version = "1.44.1", features = ["full"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/libspaceship/src/api/audio_devices.rs b/libspaceship/src/api/audio_devices.rs new file mode 100644 index 00000000..fad7761b --- /dev/null +++ b/libspaceship/src/api/audio_devices.rs @@ -0,0 +1,90 @@ +use cpal::traits::HostTrait; +use rodio::DeviceTrait; + +use crate::lightwire::{self}; + +pub struct AudioInputDevice { + pub name: String, + pub system_default: bool, +} + +/// Get all audio input devices on the system. +pub fn get_input_devices() -> Vec { + let host = lightwire::get_preferred_host(); + let default_device_name = match host.default_input_device() { + Some(device) => device.name().expect("Couldn't get default device name"), + None => "-".to_string(), + }; + + // Parse all of the devices + let mut parsed_devices = Vec::::new(); + for device in host.input_devices().expect("Couldn't get input devices") { + let name = device.name().expect("Couldn't get device name"); + let is_default = name == default_device_name; + + // Add the parsed device to the list + parsed_devices.push(AudioInputDevice { + name: name, + system_default: is_default, + }); + } + + return parsed_devices; +} + +// Get the default input device +pub fn get_default_input_device() -> AudioInputDevice { + let host = lightwire::get_preferred_host(); + let default_device = host + .default_input_device() + .expect("No default device found!"); + return AudioInputDevice { + name: default_device + .name() + .expect("No name found for default device"), + system_default: true, + }; +} + +pub struct AudioOuputDevice { + pub name: String, + pub system_default: bool, +} + +/// Get all audio output devices on the system. +pub fn get_output_devices() -> Vec { + let host = lightwire::get_preferred_host(); + let default_device_name = match host.default_output_device() { + Some(device) => device.name().expect("Couldn't get default device name"), + None => "-".to_string(), + }; + + // Parse all of the devices + let mut parsed_devices = Vec::::new(); + for device in host.output_devices().expect("Couldn't get output devices") { + let name = device.name().expect("Couldn't get device name"); + let is_default = name == default_device_name; + + // Add the parsed device to the list + parsed_devices.push(AudioOuputDevice { + name: name, + system_default: is_default, + }); + } + + return parsed_devices; +} + +// Get the default input device +pub fn get_default_output_device() -> AudioOuputDevice { + let host = lightwire::get_preferred_host(); + let default_device = host + .default_output_device() + .expect("No default device found!"); + return AudioOuputDevice { + name: default_device + .name() + .expect("No name found for default device"), + system_default: true, + }; +} diff --git a/libspaceship/src/api/engine.rs b/libspaceship/src/api/engine.rs new file mode 100644 index 00000000..699a0be2 --- /dev/null +++ b/libspaceship/src/api/engine.rs @@ -0,0 +1,119 @@ +use crate::{binding, frb_generated::StreamSink}; + +pub struct LightwireEngine { + pub id: u32, +} + +// Create a new engine +pub async fn create_lightwire_engine() -> LightwireEngine { + LightwireEngine { + id: binding::create_engine().await, + } +} + +// Stream the packets of an engine to a sink +pub async fn start_packet_stream( + engine: LightwireEngine, + packet_sink: StreamSink<(Option>, Option, Option)>, +) { + binding::init_engine(engine.id, move |(packet, amplitude, speech)| { + packet_sink + .add((packet, amplitude, speech)) + .expect("Couldn't send packet"); + }) + .await; +} + +// Set the enabled status of the microphone on an engine +pub async fn set_voice_enabled(engine: LightwireEngine, enabled: bool) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + engine.set_voice_enabled(enabled).await; +} + +// Set the current input device for an engine +pub async fn set_input_device(engine: LightwireEngine, device: String) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + engine.set_input_device(device).await; +} + +// Set the enabled status of the audio on an engine +pub async fn set_audio_enabled(engine: LightwireEngine, enabled: bool) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + engine.set_audio_enabled(enabled).await; +} + +// Set the current output device for an engine +pub async fn set_output_device(engine: LightwireEngine, device: String) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + engine.set_output_device(device).await; +} + +// Enable or disable voice activity detection on an engine +pub async fn set_activity_detection(engine: LightwireEngine, enabled: bool) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + engine.set_activity_detection(enabled).await; +} + +// Enable or disable automatic voice activity detection for an engine +pub async fn set_automatic_detection(engine: LightwireEngine, enabled: bool) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + engine.set_automatic_detection(enabled).await; +} + +// Set the talking amplitude for an engine +pub async fn set_talking_amplitude(engine: LightwireEngine, amplitude: f32) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + engine.set_talking_amplitude(amplitude).await; +} + +// Set the bitrate for encoding +pub async fn set_encoding_bitrate(engine: LightwireEngine, auto: bool, max: bool, bitrate: i32) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + if auto { + engine.set_bitrate(audiopus::Bitrate::Auto).await; + } else if max { + engine.set_bitrate(audiopus::Bitrate::Max).await; + } else { + engine + .set_bitrate(audiopus::Bitrate::BitsPerSecond(bitrate)) + .await; + } +} + +// Let the engine play a new audio packet (id needs to be registered before using register_target) +pub async fn handle_packet(engine: LightwireEngine, id: String, packet: Vec) { + let engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + engine.handle_packet(id, packet).await; +} + +// Stop an engine +pub async fn stop_engine(engine: LightwireEngine) { + let lw_engine = binding::get_engine(engine.id) + .await + .expect("Engine hasn't been initialized yet"); + lw_engine.stop().await; + binding::delete_engine(engine.id).await; +} + +// Stop all engines currently there +pub async fn stop_all_engines() { + binding::stop_all_engines().await; +} diff --git a/libspaceship/src/api/general.rs b/libspaceship/src/api/general.rs new file mode 100644 index 00000000..fd870bb1 --- /dev/null +++ b/libspaceship/src/api/general.rs @@ -0,0 +1,6 @@ +use crate::{binding, frb_generated::StreamSink}; + +// Create a log stream that sends logs to Dart +pub async fn create_log_stream(sink: StreamSink) { + binding::set_log_sink(sink).await; +} diff --git a/libspaceship/src/api/mod.rs b/libspaceship/src/api/mod.rs new file mode 100644 index 00000000..fcede48e --- /dev/null +++ b/libspaceship/src/api/mod.rs @@ -0,0 +1,3 @@ +pub mod audio_devices; +pub mod engine; +pub mod general; diff --git a/libspaceship/src/binding.rs b/libspaceship/src/binding.rs new file mode 100644 index 00000000..50a03c6f --- /dev/null +++ b/libspaceship/src/binding.rs @@ -0,0 +1,127 @@ +use std::collections::HashMap; + +use lazy_static::lazy_static; +use tokio::sync::Mutex; + +use crate::{ + frb_generated::StreamSink, + lightwire::{self, Engine}, +}; + +lazy_static! { + // Bindings for the lightwire engine + static ref ENGINE_COUNT: Mutex = Mutex::new(0); + static ref ENGINE_MAP: Mutex>> = Mutex::new(HashMap::new()); + + // Bindings for the logging from Rust + static ref LOG_SINK: Mutex>> = Mutex::new(None); +} + +// Create a new engine in global state (needed for the binding to Dart) +pub async fn create_engine() -> u32 { + // Calculate the next index and increment the count + let index = { + let mut count = ENGINE_COUNT.lock().await; + *count += 1; + count.clone() + }; + + // Add an empty engine to the map + let mut map = ENGINE_MAP.lock().await; + map.insert(index, None); + + return index; +} + +// Initialize an engine with the callback sending back the packets +pub async fn init_engine(id: u32, mut send_fn: F) +where + F: FnMut((Option>, Option, Option)) + Send + 'static, +{ + // Get the global map of engines + let mut map = ENGINE_MAP.lock().await; + map.insert( + id, + Some( + lightwire::Engine::create(move |packet| { + send_fn((packet.encode(), packet.amplitude, packet.speech)); + }) + .await, + ), + ); +} + +// Get an engine from the map +pub async fn get_engine(id: u32) -> Option { + let map = ENGINE_MAP.lock().await; + let result = map.get(&id); + if result.is_none() { + return None; + } + + return result.unwrap().to_owned(); +} + +// Remove an engine from the map +pub async fn delete_engine(id: u32) { + ENGINE_MAP.lock().await.remove(&id); +} + +// Stop all engines +pub async fn stop_all_engines() { + let mut map = ENGINE_MAP.lock().await; + for (_, engine) in map.iter() { + if let Some(engine) = engine { + engine.stop().await; + } + } + map.clear(); +} + +// Set the sink of the log stream +pub async fn set_log_sink(sink: StreamSink) { + let mut stream = LOG_SINK.lock().await; + *stream = Some(sink); +} + +// Log information +pub fn info_impl(message: &str) { + let message = message.to_string(); + tokio::spawn(async move { + let sink = LOG_SINK.lock().await; + if let Some(sink) = sink.as_ref() { + sink.add(format!("info: {}", message)) + .expect("Couldn't send log message"); + } else { + println!("info: {}", message); + } + }); +} + +// Log an error +pub fn error_impl(message: &str) { + let message = message.to_string(); + tokio::spawn(async move { + let sink = LOG_SINK.lock().await; + if let Some(sink) = sink.as_ref() { + sink.add(format!("error: {}", message)) + .expect("Couldn't send log message"); + } else { + println!("error: {}", message); + } + }); +} + +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => { + $crate::binding::info_impl(&format!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! error { + ($($arg:tt)*) => { + $crate::binding::error_impl(&format!($($arg)*)) + }; +} diff --git a/libspaceship/src/frb_generated.rs b/libspaceship/src/frb_generated.rs new file mode 100644 index 00000000..190c2271 --- /dev/null +++ b/libspaceship/src/frb_generated.rs @@ -0,0 +1,1284 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +#![allow( + non_camel_case_types, + unused, + non_snake_case, + clippy::needless_return, + clippy::redundant_closure_call, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::unused_unit, + clippy::double_parens, + clippy::let_and_return, + clippy::too_many_arguments, + clippy::match_single_binding, + clippy::clone_on_copy, + clippy::let_unit_value, + clippy::deref_addrof, + clippy::explicit_auto_deref, + clippy::borrow_deref_ref, + clippy::needless_borrow +)] + +// Section: imports + +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; +use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; +use flutter_rust_bridge::{Handler, IntoIntoDart}; + +// Section: boilerplate + +flutter_rust_bridge::frb_generated_boilerplate!( + default_stream_sink_codec = SseCodec, + default_rust_opaque = RustOpaqueMoi, + default_rust_auto_opaque = RustAutoOpaqueMoi, +); +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.9.0"; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 2025983791; + +// Section: executor + +flutter_rust_bridge::frb_generated_default_handler!(); + +// Section: wire_funcs + +fn wire__crate__api__engine__create_lightwire_engine_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "create_lightwire_engine", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok( + crate::api::engine::create_lightwire_engine().await, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__general__create_log_stream_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "create_log_stream", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_sink = + >::sse_decode( + &mut deserializer, + ); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::general::create_log_stream(api_sink).await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__audio_devices__get_default_input_device_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_default_input_device", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::audio_devices::get_default_input_device())?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__audio_devices__get_default_output_device_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_default_output_device", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::audio_devices::get_default_output_device(), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__audio_devices__get_input_devices_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_input_devices", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::audio_devices::get_input_devices())?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__audio_devices__get_output_devices_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_output_devices", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::audio_devices::get_output_devices())?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__engine__handle_packet_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "handle_packet", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_id = ::sse_decode(&mut deserializer); + let api_packet = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::handle_packet(api_engine, api_id, api_packet).await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__set_activity_detection_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_activity_detection", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_enabled = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::set_activity_detection(api_engine, api_enabled) + .await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__set_audio_enabled_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_audio_enabled", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_enabled = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::set_audio_enabled(api_engine, api_enabled).await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__set_automatic_detection_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_automatic_detection", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_enabled = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::set_automatic_detection(api_engine, api_enabled) + .await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__set_encoding_bitrate_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_encoding_bitrate", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_auto = ::sse_decode(&mut deserializer); + let api_max = ::sse_decode(&mut deserializer); + let api_bitrate = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::set_encoding_bitrate( + api_engine, + api_auto, + api_max, + api_bitrate, + ) + .await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__set_input_device_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_input_device", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_device = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::set_input_device(api_engine, api_device).await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__set_output_device_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_output_device", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_device = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::set_output_device(api_engine, api_device).await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__set_talking_amplitude_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_talking_amplitude", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_amplitude = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::set_talking_amplitude(api_engine, api_amplitude) + .await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__set_voice_enabled_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_voice_enabled", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_enabled = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::set_voice_enabled(api_engine, api_enabled).await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__start_packet_stream_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "start_packet_stream", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + let api_packet_sink = >, Option, Option), + flutter_rust_bridge::for_generated::SseCodec, + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::start_packet_stream(api_engine, api_packet_sink) + .await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__stop_all_engines_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stop_all_engines", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::stop_all_engines().await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__engine__stop_engine_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stop_engine", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_engine = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::api::engine::stop_engine(api_engine).await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} + +// Section: dart2rust + +impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); + } +} + +impl SseDecode for StreamSink { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return StreamSink::deserialize(inner); + } +} + +impl SseDecode + for StreamSink< + (Option>, Option, Option), + flutter_rust_bridge::for_generated::SseCodec, + > +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return StreamSink::deserialize(inner); + } +} + +impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap(); + } +} + +impl SseDecode for crate::api::audio_devices::AudioInputDevice { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_name = ::sse_decode(deserializer); + let mut var_systemDefault = ::sse_decode(deserializer); + return crate::api::audio_devices::AudioInputDevice { + name: var_name, + system_default: var_systemDefault, + }; + } +} + +impl SseDecode for crate::api::audio_devices::AudioOuputDevice { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_name = ::sse_decode(deserializer); + let mut var_systemDefault = ::sse_decode(deserializer); + return crate::api::audio_devices::AudioOuputDevice { + name: var_name, + system_default: var_systemDefault, + }; + } +} + +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +impl SseDecode for f32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_f32::().unwrap() + } +} + +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + +impl SseDecode for crate::api::engine::LightwireEngine { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_id = ::sse_decode(deserializer); + return crate::api::engine::LightwireEngine { id: var_id }; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode( + deserializer, + )); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode( + deserializer, + )); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for (Option>, Option, Option) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = >>::sse_decode(deserializer); + let mut var_field1 = >::sse_decode(deserializer); + let mut var_field2 = >::sse_decode(deserializer); + return (var_field0, var_field1, var_field2); + } +} + +impl SseDecode for u32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u32::().unwrap() + } +} + +impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() + } +} + +impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} +} + +fn pde_ffi_dispatcher_primary_impl( + func_id: i32, + port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__api__engine__create_lightwire_engine_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 2 => wire__crate__api__general__create_log_stream_impl(port, ptr, rust_vec_len, data_len), + 3 => wire__crate__api__audio_devices__get_default_input_device_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 4 => wire__crate__api__audio_devices__get_default_output_device_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 5 => wire__crate__api__audio_devices__get_input_devices_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 6 => wire__crate__api__audio_devices__get_output_devices_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 7 => wire__crate__api__engine__handle_packet_impl(port, ptr, rust_vec_len, data_len), + 8 => { + wire__crate__api__engine__set_activity_detection_impl(port, ptr, rust_vec_len, data_len) + } + 9 => wire__crate__api__engine__set_audio_enabled_impl(port, ptr, rust_vec_len, data_len), + 10 => wire__crate__api__engine__set_automatic_detection_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 11 => { + wire__crate__api__engine__set_encoding_bitrate_impl(port, ptr, rust_vec_len, data_len) + } + 12 => wire__crate__api__engine__set_input_device_impl(port, ptr, rust_vec_len, data_len), + 13 => wire__crate__api__engine__set_output_device_impl(port, ptr, rust_vec_len, data_len), + 14 => { + wire__crate__api__engine__set_talking_amplitude_impl(port, ptr, rust_vec_len, data_len) + } + 15 => wire__crate__api__engine__set_voice_enabled_impl(port, ptr, rust_vec_len, data_len), + 16 => wire__crate__api__engine__start_packet_stream_impl(port, ptr, rust_vec_len, data_len), + 17 => wire__crate__api__engine__stop_all_engines_impl(port, ptr, rust_vec_len, data_len), + 18 => wire__crate__api__engine__stop_engine_impl(port, ptr, rust_vec_len, data_len), + _ => unreachable!(), + } +} + +fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + _ => unreachable!(), + } +} + +// Section: rust2dart + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::audio_devices::AudioInputDevice { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.name.into_into_dart().into_dart(), + self.system_default.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::audio_devices::AudioInputDevice +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::audio_devices::AudioInputDevice +{ + fn into_into_dart(self) -> crate::api::audio_devices::AudioInputDevice { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::audio_devices::AudioOuputDevice { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.name.into_into_dart().into_dart(), + self.system_default.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::audio_devices::AudioOuputDevice +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::audio_devices::AudioOuputDevice +{ + fn into_into_dart(self) -> crate::api::audio_devices::AudioOuputDevice { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::engine::LightwireEngine { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [self.id.into_into_dart().into_dart()].into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::engine::LightwireEngine +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::engine::LightwireEngine +{ + fn into_into_dart(self) -> crate::api::engine::LightwireEngine { + self + } +} + +impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(format!("{:?}", self), serializer); + } +} + +impl SseEncode for StreamSink { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + unimplemented!("") + } +} + +impl SseEncode + for StreamSink< + (Option>, Option, Option), + flutter_rust_bridge::for_generated::SseCodec, + > +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + unimplemented!("") + } +} + +impl SseEncode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.into_bytes(), serializer); + } +} + +impl SseEncode for crate::api::audio_devices::AudioInputDevice { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.name, serializer); + ::sse_encode(self.system_default, serializer); + } +} + +impl SseEncode for crate::api::audio_devices::AudioOuputDevice { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.name, serializer); + ::sse_encode(self.system_default, serializer); + } +} + +impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self as _).unwrap(); + } +} + +impl SseEncode for f32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_f32::(self).unwrap(); + } +} + +impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i32::(self).unwrap(); + } +} + +impl SseEncode for crate::api::engine::LightwireEngine { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.id, serializer); + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >::sse_encode(value, serializer); + } + } +} + +impl SseEncode for (Option>, Option, Option) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(self.0, serializer); + >::sse_encode(self.1, serializer); + >::sse_encode(self.2, serializer); + } +} + +impl SseEncode for u32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u32::(self).unwrap(); + } +} + +impl SseEncode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self).unwrap(); + } +} + +impl SseEncode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +#[cfg(not(target_family = "wasm"))] +mod io { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.9.0. + + // Section: imports + + use super::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_io!(); +} +#[cfg(not(target_family = "wasm"))] +pub use io::*; + +/// cbindgen:ignore +#[cfg(target_family = "wasm")] +mod web { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.9.0. + + // Section: imports + + use super::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::wasm_bindgen; + use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_web!(); +} +#[cfg(target_family = "wasm")] +pub use web::*; diff --git a/libspaceship/src/lib.rs b/libspaceship/src/lib.rs new file mode 100644 index 00000000..b361d2b3 --- /dev/null +++ b/libspaceship/src/lib.rs @@ -0,0 +1,4 @@ +pub mod api; +mod binding; +mod frb_generated; +mod lightwire; diff --git a/libspaceship/src/lightwire/encoder.rs b/libspaceship/src/lightwire/encoder.rs new file mode 100644 index 00000000..ed00f3e9 --- /dev/null +++ b/libspaceship/src/lightwire/encoder.rs @@ -0,0 +1,159 @@ +use std::{cmp, sync::Arc}; + +use audiopus::{coder::Encoder, Bitrate}; +use rand::Rng; +use tokio::sync::{mpsc::Receiver, Mutex}; + +use super::{AudioPacket, MicrophoneOptions}; + +pub struct EncodingEngine { + encoder: Option>, + bitrate: audiopus::Bitrate, + current_seq: u16, +} + +impl EncodingEngine { + // Start a new encoding engine + pub fn create( + mut sample_receiver: Receiver>, + options: Arc>, + mut send_fn: F, + ) -> Arc> + where + F: FnMut(AudioPacket) + Send + 'static, + { + // Create a new Opus encoder for this encoding engine + let encoder = Encoder::new( + audiopus::SampleRate::Hz48000, + audiopus::Channels::Mono, + audiopus::Application::Voip, + ) + .expect("Couldn't create opus encoder"); + + let engine = Arc::new(Mutex::new(Self { + encoder: Some(Mutex::new(encoder)), + bitrate: audiopus::Bitrate::Auto, + current_seq: rand::rng().random(), + })); + + // Spawn the encoding task + tokio::task::spawn_blocking({ + let engine = engine.clone(); + + // State for voice activity detection + let mut talking_streak = 0; + let mut noise_floor = 0.0; + + // Constants for the automatic voice activity detection + let alpha = 0.99; + let threshold_factor = 1.7; + + move || loop { + let samples: Option> = sample_receiver.blocking_recv(); + if samples.is_none() { + break; + } + let samples = samples.expect("Couldn't unwrap option even though some?"); + + // Get the options for voice activity detection + let options = options.blocking_lock(); + + // Run voice activity detection (if desired) + let mut speech = None; + let mut amplitude = None; + if options.activity_detection { + // Calculate the mean square root of the samples (or "energy", really useful for speech detection) + let mut avg = 0.0; + for sample in samples.iter() { + avg += sample * sample; + } + avg = avg / samples.len() as f32; + avg = avg.sqrt(); + + // Try to detect speech + let speech_detected: bool = if options.automatic_detection { + // TODO: Improve this maybe + // Use automatic detection by having a noise floor + noise_floor = alpha * noise_floor + (1.0 - alpha) * avg; + avg > noise_floor * threshold_factor + } else { + // Use the talking amplitude for speech detection + avg = pcm_to_db(avg); + amplitude = Some(avg); + avg > options.talking_amplitude + }; + + // Compute the current talking state + speech = Some(if speech_detected { + talking_streak = 25; + true + } else { + talking_streak = cmp::max(talking_streak - 1, 0); + talking_streak > 0 + }); + } + + // Encode using Opus (only when speech is detected) + let mut seq = 0; + let mut packet = None; + if speech.is_none() || speech.is_some_and(|s| s) { + let mut engine = engine.blocking_lock(); + if engine.encoder.is_none() { + break; + } + + // Increment the sequence number + if engine.current_seq == u16::MAX { + engine.current_seq = 0; + } else { + engine.current_seq += 1; + } + + // Get and set the encoder settings + let encoder = engine.encoder.as_ref().unwrap(); + let mut coder = encoder.blocking_lock(); + coder + .set_bitrate(engine.bitrate) + .expect("Couldn't set bitrate"); + + // Encode the packet + let mut output = [0u8; 2000]; + let output_size = coder + .encode_float(samples.as_slice(), &mut output) + .expect("Couldn't encode"); + let (encoded, _) = output.split_at(output_size); + + // Return the packet + packet = Some(encoded.to_vec()); + seq = engine.current_seq; + } + + // Send to the client + send_fn(AudioPacket { + id: None, + speech: speech, + amplitude: amplitude, + packet: packet, + seq: seq, + }); + } + }); + + return engine.clone(); + } + + // Set the bitrate of the encoder + pub fn set_bitrate(&mut self, bitrate: Bitrate) { + self.bitrate = bitrate + } + + // Stop the encoding engine + pub fn stop(&mut self) { + self.encoder = None; + } +} + +// Convert pcm data to decibel +fn pcm_to_db(pcm: f32) -> f32 { + 20.0 * pcm.abs().log10() +} diff --git a/libspaceship/src/lightwire/mod.rs b/libspaceship/src/lightwire/mod.rs new file mode 100644 index 00000000..863243dc --- /dev/null +++ b/libspaceship/src/lightwire/mod.rs @@ -0,0 +1,231 @@ +use std::sync::Arc; + +use audiopus::Bitrate; +use encoder::EncodingEngine; +use player::PlayingEngine; +use tokio::sync::{mpsc::UnboundedSender, Mutex}; +use voice::VoiceInput; + +mod encoder; +mod player; +mod voice; + +struct MicrophoneOptions { + activity_detection: bool, + automatic_detection: bool, + talking_amplitude: f32, +} + +#[derive(Clone)] +pub struct Engine { + microphone_options: Arc>, + voice_input: Arc>, + encoding_engine: Arc>, + playing_engine: Arc>, + packet_sender: UnboundedSender, +} + +impl Engine { + pub async fn create(send_fn: F) -> Self + where + F: FnMut(AudioPacket) + Send + 'static, + { + // Create the default microphone options + let options = Arc::new(Mutex::new(MicrophoneOptions { + activity_detection: true, + automatic_detection: false, + talking_amplitude: -50.0, + })); + + // Create the voice input + let (voice_input, receiver) = VoiceInput::create(); + + // Create the encoding engine + let encoding_engine = EncodingEngine::create(receiver, options.clone(), send_fn); + + // Start the playing engine + let (playing_engine, sender) = PlayingEngine::create().await; + + // Initialize the engine + return Self { + microphone_options: options, + voice_input: voice_input, + encoding_engine: encoding_engine, + playing_engine: playing_engine, + packet_sender: sender, + }; + } + + // Enable or disable the microphone + pub async fn set_voice_enabled(&self, enabled: bool) { + let mut input = self.voice_input.lock().await; + input.set_paused(!enabled); + } + + // Set the current input device + pub async fn set_input_device(&self, device: String) { + let mut input = self.voice_input.lock().await; + input.set_device(device); + } + + // Enable or disable playing sound + pub async fn set_audio_enabled(&self, enabled: bool) { + let mut engine = self.playing_engine.lock().await; + engine.set_enabled(enabled); + } + + // Set the current output device + pub async fn set_output_device(&self, device: String) { + let mut engine = self.playing_engine.lock().await; + engine.set_device(device); + } + + // Enable or disable activity detection + pub async fn set_activity_detection(&self, enabled: bool) { + let mut opts = self.microphone_options.lock().await; + opts.activity_detection = enabled; + } + + // Enable or disable automatic voice activity detection + pub async fn set_automatic_detection(&self, enabled: bool) { + let mut opts = self.microphone_options.lock().await; + opts.automatic_detection = enabled; + } + + // Set the talking amplitude for voice activity detection (in decibel) + pub async fn set_talking_amplitude(&self, amplitude: f32) { + let mut opts = self.microphone_options.lock().await; + opts.talking_amplitude = amplitude; + } + + // Set the bitrate of the encoder + pub async fn set_bitrate(&self, bitrate: Bitrate) { + let mut opts = self.encoding_engine.lock().await; + opts.set_bitrate(bitrate); + } + + // Handle a packet + pub async fn handle_packet(&self, id: String, packet: Vec) { + // Make sure the the engine is actually enabled + let mut engine = self.playing_engine.lock().await; + if !engine.is_enabled() { + return; + } + + // Add the target in case it doesn't exist + if !engine.does_target_exist(&id) { + engine.add_target(self.playing_engine.clone(), id.clone()); + } + + self.packet_sender + .send(AudioPacket::decode(Some(id), packet)) + .ok(); + } + + // Stop the engine + pub async fn stop(&self) { + self.encoding_engine.lock().await.stop(); + self.voice_input.lock().await.stop(); + self.playing_engine.lock().await.stop(); + } +} + +#[derive(Clone)] +pub struct AudioPacket { + pub id: Option, + pub seq: u16, + pub amplitude: Option, + pub speech: Option, + pub packet: Option>, +} + +impl jittr::Packet for AudioPacket { + fn sequence_number(&self) -> u16 { + self.seq + } +} + +impl AudioPacket { + // Encode the audio packet to bytes + // + // Format: | seq | voice_data | + pub fn encode(&self) -> Option> { + // Make sure there actually is a packet + if self.packet.is_none() { + return None; + } + + // Encode the packet with the sequence number added + let packet = self.packet.as_ref().unwrap(); + let mut packet_vec = Vec::with_capacity(2 + 4 + packet.len()); + packet_vec.extend_from_slice(&self.seq.to_le_bytes()); + packet_vec.extend(packet.iter()); + return Some(packet_vec); + } + + // Decode the audio packet + // + // Format: | seq | voice_data | + pub fn decode(id: Option, bytes: Vec) -> Self { + let (seq_bytes, packet) = bytes.split_at(2); + return Self { + id: id, + speech: None, + amplitude: None, + seq: u16::from_le_bytes([seq_bytes[0], seq_bytes[1]]), + packet: Some(packet.to_vec()), + }; + } +} + +// Get the host lightwire is going to use (mainly for making sure we can easily change it in the future in case needed) +pub fn get_preferred_host() -> cpal::Host { + return cpal::default_host(); +} + +/* +Demo of voice input and the decoding engine (just here for maybe future idk) + +tokio::task::spawn_blocking(move || { + let mut decoder = + opus::Decoder::new(48000, opus::Channels::Mono).expect("Couldn't create decoder"); + + let (_stream, stream_handle) = + OutputStream::try_default().expect("Failed to get default output stream"); + let sink = Sink::try_new(&stream_handle).expect("Failed to create sink"); + + // Decode all the packets + loop { + // Listen for new packets + let encoded_sample = encoded_receiver.blocking_recv(); + if encoded_sample.is_none() { + break; + } + + // Decode the packet + let mut output = [0f32; 2000]; + let amount = decoder + .decode_float( + encoded_sample.unwrap().packet.as_slice(), + &mut output, + false, + ) + .expect("Couldn't decode"); + println!("decoded {}", amount); + let (sample, _) = output.split_at(amount); + + let source = SamplesBuffer::new(1, sample_rate, sample); + sink.append(source); + } + + sink.sleep_until_end(); +}); + +thread::sleep(Duration::from_secs(3)); +{ + let mut voice_input_ref = voice_input.lock().unwrap(); + voice_input_ref.stop(); +} +thread::sleep(Duration::from_secs(6)); + +*/ diff --git a/libspaceship/src/lightwire/player.rs b/libspaceship/src/lightwire/player.rs new file mode 100644 index 00000000..7dff464d --- /dev/null +++ b/libspaceship/src/lightwire/player.rs @@ -0,0 +1,266 @@ +use std::{collections::HashMap, sync::Arc, thread, time::Duration}; + +use audiopus::coder::Decoder; +use cpal::{traits::HostTrait, Device}; +use jittr::JitterBuffer; +use rodio::{buffer::SamplesBuffer, DeviceTrait, OutputStream, OutputStreamHandle, Sink}; +use tokio::{ + sync::{ + mpsc::{self, UnboundedSender}, + Mutex, + }, + time, +}; + +use crate::{error, info}; + +use super::{get_preferred_host, AudioPacket}; + +pub struct PlayingEngine { + client_map: HashMap>>, + device: String, + output_handle: Option, + enabled: bool, + stop: bool, +} + +struct Client { + sink: Sink, + buffer: JitterBuffer, + decoder: Option, +} + +// Default sample rate for all packets +static DEFAULT_SAMPLE_RATE: u32 = 48000; +static DEFAULT_FRAME_SIZE: usize = (DEFAULT_SAMPLE_RATE / 50) as usize; +static DEFAULT_SAMPLE_RATE_OPUS: audiopus::SampleRate = audiopus::SampleRate::Hz48000; + +impl PlayingEngine { + pub async fn create() -> (Arc>, UnboundedSender) { + let engine = Arc::new(Mutex::new(Self { + client_map: HashMap::new(), + device: "-".to_string(), + output_handle: None, + enabled: true, + stop: false, + })); + + // Create a new channel for the packets coming in + let (sender, mut receiver) = mpsc::unbounded_channel(); + + tokio::task::spawn_blocking({ + let engine = engine.clone(); + move || { + // Stream needs to be created on the main thread + let (mut _stream, mut stream_handle): (OutputStream, OutputStreamHandle); + + // Start a loop to keep the stream in scope until the engine exists + let mut last_device = "----".to_string(); + loop { + { + let mut engine = engine.blocking_lock(); + if engine.stop { + info!("stopping playing engine."); + return; + } + + // Restart the output stream when the device changes + if engine.device != last_device { + let host = get_preferred_host(); + + // Try to get the device + let mut device: Option = None; + for dev in host.output_devices().expect("Couldn't list output devices") + { + if dev.name().expect("Couldn't get output name") == engine.device { + device = Some(dev); + break; + } + } + + // Create the new output stream + if let Some(dev) = device { + (_stream, stream_handle) = OutputStream::try_from_device(&dev) + .expect("Failed to get output stream from found device"); + } else { + (_stream, stream_handle) = OutputStream::try_default() + .expect("Failed to get default output stream"); + } + engine.output_handle = Some(stream_handle); + + // Change the sinks of all the clients to use the the new output stream + for client in engine.client_map.values() { + let mut client = client.blocking_lock(); + client.sink = Sink::try_new(engine.output_handle.as_ref().unwrap()) + .expect("Couldn't create new Sink") + } + + last_device = engine.device.clone(); + } + } + thread::sleep(Duration::from_millis(500)); + } + } + }); + + tokio::spawn({ + let engine = engine.clone(); + async move { + loop { + // Listen for new audio packets + let data = time::timeout(Duration::from_millis(500), receiver.recv()).await; + if data.is_err() { + continue; + } + let engine = engine.lock().await; + let data = data.unwrap(); + if data.is_none() || engine.stop { + info!("closed playing engine."); + return; + } + let data: AudioPacket = data.expect("No data found"); + + // Make sure the client with the specified id actually exists + let client_id = data + .id + .as_ref() + .expect("No client id in packet for decoding, can't decode this"); + if !engine.client_map.contains_key(client_id) { + error!("client {} hasn't been added yet", client_id); + continue; + } + + // Add the packet to the jitter buffer of the client + let client = engine + .client_map + .get(client_id) + .expect("Not found even though key exists, wtf"); + let mut client = client.lock().await; + client.buffer.push(data); + } + } + }); + + return (engine, sender); + } + + // Check if a target exists in the playing engine + pub fn does_target_exist(&self, id: &String) -> bool { + self.client_map.contains_key(id) + } + + // Add a new client to the playing engine + pub fn add_target(&mut self, arc: Arc>, id: String) { + // Create a sink for the thing + let handle = self.output_handle.as_ref().unwrap(); + let sink = Sink::try_new(handle).expect("Couldn't create sink"); + + // Add the target to the playing engine + let client = Arc::new(Mutex::new(Client { + sink: sink, + buffer: JitterBuffer::new(), + decoder: None, + })); + self.client_map.insert(id.clone(), client.clone()); + + // Spawn a task for playing the packets at a consistent interval + tokio::spawn(async move { + let mut seals = 0; + let mut interval = time::interval(Duration::from_millis(20)); + loop { + interval.tick().await; + let mut engine = arc.lock().await; // needs to be locked here to prevent deadlocks with device switching + let mut client = client.lock().await; + + // Make sure the engine is actually enabled + if !engine.enabled { + engine.remove_target(&id); + info!("unused listener for client {}", id); + client.buffer.clear(); + return; + } + + // Shutdown the listener completely at some point to prevent unneeded resource usage + seals += 1; + if seals > 1000 { + engine.remove_target(&id); + info!("unused listener for client {}", id); + client.buffer.clear(); + return; + } + + // Actually play the packet (or add seal) + if let Some(packet) = client.buffer.pop() { + seals = 0; + + // Create a decoder in case there isn't one + if client.decoder.is_none() { + client.decoder = Some( + Decoder::new(DEFAULT_SAMPLE_RATE_OPUS, audiopus::Channels::Mono) + .expect("Couldn't create decoder"), + ); + } + + // Decode the packet + let decoder = client.decoder.as_mut().expect("Decoder not found, wtf"); + let mut decoded = [0f32; DEFAULT_FRAME_SIZE]; + let frame_size = decoder + .decode_float(Some(&packet.packet.unwrap()), &mut decoded[..], false) + .expect("Couldn't decode packet"); + let (decoded, _) = decoded.split_at(frame_size); + + println!("playing seq={}", packet.seq); + + // Play the packet using the sink + client + .sink + .append(SamplesBuffer::new(1, DEFAULT_SAMPLE_RATE, decoded)); + } else if let Some(decoder) = &mut client.decoder { + // Don't generate seals anymore at some point + if seals > 10 { + continue; + } + + // Generate loss concealment using the decoder + let mut decoded = [0f32; DEFAULT_FRAME_SIZE]; + let none_option: Option<&Vec> = None; + let frame_size = decoder + .decode_float(none_option, &mut decoded[..], false) + .expect("Couldn't generate loss concealment"); + let (decoded, _) = decoded.split_at(frame_size); + + // Play the loss concealment using the sink + client + .sink + .append(SamplesBuffer::new(1, DEFAULT_SAMPLE_RATE, decoded)); + } + } + }); + } + + // Remove a target from the engine + pub fn remove_target(&mut self, id: &String) { + self.client_map.remove(id); + } + + // Completely stop the engine + pub fn stop(&mut self) { + self.stop = true; + self.client_map.clear(); + } + + // Enable or disable playing sound + pub fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + // Get the enabled state of the engine + pub fn is_enabled(&self) -> bool { + self.enabled + } + + // Set the output device of the engine + pub fn set_device(&mut self, device: String) { + self.device = device; + } +} diff --git a/libspaceship/src/lightwire/voice.rs b/libspaceship/src/lightwire/voice.rs new file mode 100644 index 00000000..12b357f1 --- /dev/null +++ b/libspaceship/src/lightwire/voice.rs @@ -0,0 +1,197 @@ +use std::{sync::Arc, thread, time::Duration}; + +use cpal::traits::{HostTrait, StreamTrait}; +use rodio::DeviceTrait; +use rubato::{FftFixedOut, Resampler}; +use tokio::sync::{ + mpsc::{self, Receiver, Sender}, + Mutex, +}; + +use crate::{error, info}; + +use super::get_preferred_host; + +pub struct VoiceInput { + device: String, + sample_rate: u32, + frame_size: u32, + stop: bool, + paused: bool, +} + +impl VoiceInput { + pub fn create() -> (Arc>, Receiver>) { + let voice_input = Arc::new(Mutex::new(Self { + device: "def".to_string(), + sample_rate: 48000, + frame_size: 48000 / 50, + stop: false, + paused: true, + })); + + // Create a new channel for sending the packets + let (sender, receiver) = mpsc::channel(4); + + // Create a new task for handling all of the sending + VoiceInput::create_task(voice_input.clone(), sender); + + return (voice_input, receiver); + } + + // Create the blocking task for the actual recording of the microphone + fn create_task(voice_input: Arc>, sender: Sender>) { + tokio::task::spawn_blocking({ + let sender = sender; + let vc_input = voice_input.clone(); // Clone for use in the task + move || { + // Create the stream here to make sure it stays in scope + let host = get_preferred_host(); + + // Get the currently selected device + let mut device = host + .default_input_device() + .expect("No default input device"); + let selected_device = { + let input = vc_input.blocking_lock(); + input.device.clone() + }; + for dev in host.input_devices().expect("Couldn't get input devices") { + if dev.name().expect("Couldn't get device name") == selected_device { + device = dev; + break; + } + } + + // Create stream config for the device based on the channels + // Determine appropriate buffer size based on device capabilities + let device_config = device.default_input_config().expect("No default config"); + let channels = device_config.channels(); + let frame_size = { + let input = vc_input.blocking_lock(); + input.frame_size + }; + let desired_buffer_size = match device_config.buffer_size() { + cpal::SupportedBufferSize::Range { min, max } => { + // Try to use frame_size, but stay within allowed range + let frame_size = frame_size * channels as u32; + cpal::BufferSize::Fixed(frame_size.clamp(*min, *max)) + } + cpal::SupportedBufferSize::Unknown => { + // Use default buffer size when unknown + cpal::BufferSize::Default + } + }; + let stream_config = cpal::StreamConfig { + channels: device_config.channels(), + sample_rate: device_config.sample_rate(), + buffer_size: desired_buffer_size, + }; + + // Create a resampler to resample to 48kHz (default sample rate) + let sample_rate = { + let input = vc_input.blocking_lock(); + input.sample_rate + }; + let mut resampler = FftFixedOut::::new( + usize::try_from(device_config.sample_rate().0).unwrap(), + usize::try_from(sample_rate).unwrap(), + usize::try_from(frame_size).unwrap(), + 1, + 1, + ) + .expect("Couldn't create resampler"); + + // Error function for printing errors that happen during voice handling + let err_fn = move |err| error!("error in cpal: {}", err); + + // Create the callback for the voice data received from cpal + let callback = { + let input: Arc> = vc_input.clone(); + let mut overflow_buffer = Vec::::new(); + let sender = sender.to_owned(); + + move |data: &[f32], _: &cpal::InputCallbackInfo| { + // Check if paused + { + let input = input.blocking_lock(); + if input.paused { + return; + } + } + + // Add the data to the buffer + if channels == 1 { + overflow_buffer.extend_from_slice(data); + } else { + // Convert to mono from stereo + let mono: Vec = + data.chunks(2).map(|c| (c[0] + c[1]) * 0.5).collect(); + overflow_buffer.extend_from_slice(&mono); + } + + // Get the frame size needed for resampling + let mut needed_frame_size = resampler.input_frames_next(); + + // Dispatch complete frames + while overflow_buffer.len() >= needed_frame_size { + let packet: Vec = overflow_buffer + .drain(0..needed_frame_size as usize) + .collect(); + + // Resample the packet + let result = resampler + .process(&[packet], None) + .expect("Couldn't resample"); + + // Send the packet + sender.blocking_send(result[0].to_owned()).ok(); + + // Set the next frame size + needed_frame_size = resampler.input_frames_next(); + } + } + }; + + // Start the audio listening stream + let stream = device + .build_input_stream(&stream_config, callback, err_fn, None) + .expect("Couldn't build stream"); + + stream.play().expect("Couldn't start stream"); + + loop { + // Check if there was a new device or if the thing was stopped + let new_device = { + let input = vc_input.blocking_lock(); + if input.stop { + break; + } + input.device != selected_device + }; + + // Restart in that case + if new_device { + info!("new device"); + VoiceInput::create_task(vc_input.clone(), sender.clone()); + break; + } + + thread::sleep(Duration::from_millis(100)); + } + } + }); + } + + pub fn set_paused(&mut self, paused: bool) { + self.paused = paused; + } + + pub fn stop(&mut self) { + self.stop = true; + } + + pub fn set_device(&mut self, device: String) { + self.device = device + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 81c11371..998377a6 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,12 +8,12 @@ #include #include +#include #include #include #include #include #include -#include #include #include @@ -24,6 +24,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); + flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); g_autoptr(FlPluginRegistrar) open_file_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); @@ -39,9 +42,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); - g_autoptr(FlPluginRegistrar) tray_manager_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); - tray_manager_plugin_register_with_registrar(tray_manager_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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7c951aa9..b3963f41 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,17 +5,18 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_secure_storage_linux + flutter_webrtc open_file_linux pasteboard screen_retriever_linux sodium_libs sqlite3_flutter_libs - tray_manager url_launcher_linux window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + libspaceship ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/liphium_bridge/lib/liphium_bridge.dart b/liphium_bridge/lib/liphium_bridge.dart index 636dfc8a..845a589d 100644 --- a/liphium_bridge/lib/liphium_bridge.dart +++ b/liphium_bridge/lib/liphium_bridge.dart @@ -3,5 +3,8 @@ library liphium_bridge; export "src/base.dart" show fileUtil; -export "src/interface.dart" if (dart.library.io) "src/native.dart" if (dart.library.js) "src/web.dart" show XImage, XDirectory, isDirectorySupported; +export "src/interface.dart" + if (dart.library.io) "src/native.dart" + if (dart.library.js) "src/web.dart" + show XImage, XDirectory, isDirectorySupported; export "src/web.dart" show XFileImage; diff --git a/liphium_bridge/lib/src/web.dart b/liphium_bridge/lib/src/web.dart index c02acfe2..5752254f 100644 --- a/liphium_bridge/lib/src/web.dart +++ b/liphium_bridge/lib/src/web.dart @@ -83,7 +83,8 @@ class XFileImage extends ImageProvider { final double scale; @override - Future obtainKey(final ImageConfiguration configuration) => SynchronousFuture(this); + Future obtainKey(final ImageConfiguration configuration) => + SynchronousFuture(this); @override ImageStreamCompleter loadImage( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8d009810..852f13e9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import audio_session import file_selector_macos import flutter_secure_storage_macos +import flutter_webrtc import just_audio import open_file_mac import pasteboard @@ -15,7 +16,6 @@ import path_provider_foundation import screen_retriever_macos import sodium_libs import sqlite3_flutter_libs -import tray_manager import url_launcher_macos import window_manager @@ -23,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) @@ -30,7 +31,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SodiumLibsPlugin.register(with: registry.registrar(forPlugin: "SodiumLibsPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) - TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile b/macos/Podfile index c795730d..29c8eb32 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -28,7 +28,6 @@ flutter_macos_podfile_setup target 'Runner' do use_frameworks! - use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do diff --git a/macos/Podfile.lock b/macos/Podfile.lock index c964b7a7..80e1410e 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,10 +3,16 @@ PODS: - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS - - flutter_secure_storage_macos (6.1.1): + - flutter_secure_storage_macos (6.1.3): - FlutterMacOS + - flutter_webrtc (0.12.6): + - FlutterMacOS + - WebRTC-SDK (= 125.6422.06) - FlutterMacOS (1.0.0) - just_audio (0.0.1): + - Flutter + - FlutterMacOS + - libspaceship (0.0.1): - FlutterMacOS - open_file_mac (0.0.1): - FlutterMacOS @@ -17,24 +23,24 @@ PODS: - FlutterMacOS - screen_retriever_macos (0.0.1): - FlutterMacOS - - "sodium_libs (3.4.1+2)": + - "sodium_libs (3.4.3+2)": - Flutter - FlutterMacOS - - sqlite3 (3.47.2): - - sqlite3/common (= 3.47.2) - - sqlite3/common (3.47.2) - - sqlite3/dbstatvtab (3.47.2): + - sqlite3 (3.49.1): + - sqlite3/common (= 3.49.1) + - sqlite3/common (3.49.1) + - sqlite3/dbstatvtab (3.49.1): - sqlite3/common - - sqlite3/fts5 (3.47.2): + - sqlite3/fts5 (3.49.1): - sqlite3/common - - sqlite3/perf-threadsafe (3.47.2): + - sqlite3/perf-threadsafe (3.49.1): - sqlite3/common - - sqlite3/rtree (3.47.2): + - sqlite3/rtree (3.49.1): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (~> 3.47.2) + - sqlite3 (~> 3.49.1) - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe @@ -43,6 +49,7 @@ PODS: - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS + - WebRTC-SDK (125.6422.06) - window_manager (0.2.0): - FlutterMacOS @@ -50,8 +57,10 @@ DEPENDENCIES: - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) + - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`) + - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`) + - libspaceship (from `Flutter/ephemeral/.symlinks/plugins/libspaceship/macos`) - open_file_mac (from `Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) @@ -65,6 +74,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - sqlite3 + - WebRTC-SDK EXTERNAL SOURCES: audio_session: @@ -73,10 +83,14 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos flutter_secure_storage_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos + flutter_webrtc: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos FlutterMacOS: :path: Flutter/ephemeral just_audio: - :path: Flutter/ephemeral/.symlinks/plugins/just_audio/macos + :path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin + libspaceship: + :path: Flutter/ephemeral/.symlinks/plugins/libspaceship/macos open_file_mac: :path: Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos pasteboard: @@ -97,22 +111,25 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 + audio_session: 728ae3823d914f809c485d390274861a24b0904e file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 + flutter_secure_storage_macos: c2754d3483d20bb207bb9e5a14f1b8e771abcdb9 + flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 + just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 + libspaceship: 9a342f1bb5291587e62f20c84f4469e5c3dc511a open_file_mac: 0e554648e2a87ce59e9438e3e5ca3e552e90d89a pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 - sodium_libs: 4a81757edfdea3152b0d38e0a9a450c8f9bf7070 - sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71 - sqlite3_flutter_libs: 58ae36c0dd086395d066b4fe4de9cdca83e717b3 + sodium_libs: 7f895e4528eaa251d8e93a38237bc990cc3c73ce + sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index f921fe4a..aebbac0c 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,14 +21,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 03C72F785CA2AC00C965D3C2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00FB58E3465AC56328D3498C /* Pods_Runner.framework */; }; 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 */; }; - E96E60BE652AF09EEBDC678D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 60ED9BFA95AA126999220261 /* Pods_RunnerTests.framework */; }; + 753785678124A70A779F13B9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 518E13D127F67BA055E3CC77 /* Pods_Runner.framework */; }; + CC93BDD988C250A4127BECC7 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 045143EA540BBF064066157F /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,9 +62,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 00FB58E3465AC56328D3498C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 28F29AF2EFF3D12A4F86A875 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 32CE5AE37FE8D8628345FC4C /* 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 = ""; }; + 045143EA540BBF064066157F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 20B7BA6E1026A9B6B10A454B /* 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 = ""; }; + 32A4923180C802605A70969C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -81,13 +81,13 @@ 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 = ""; }; - 60ED9BFA95AA126999220261 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F5FFE286CDC0C8049A1E2CC /* 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 = ""; }; + 518E13D127F67BA055E3CC77 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6F2087016D1C279A64CB3683 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 8B8B275A19FC7CEBDF396DC0 /* 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 = ""; }; - 947BE44BAD67A1CD0AF00C93 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 7D80084AE92896674F90848B /* 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 = ""; }; + 9251D52C91D4F76968D19FE3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - B52E8054D72425147F13731E /* 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 = ""; }; - CA190E3529AB505F3A85EBD1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,7 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E96E60BE652AF09EEBDC678D /* Pods_RunnerTests.framework in Frameworks */, + CC93BDD988C250A4127BECC7 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,26 +103,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 03C72F785CA2AC00C965D3C2 /* Pods_Runner.framework in Frameworks */, + 753785678124A70A779F13B9 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 036FA8D726FA705A0E65D51E /* Pods */ = { - isa = PBXGroup; - children = ( - 8B8B275A19FC7CEBDF396DC0 /* Pods-Runner.debug.xcconfig */, - 32CE5AE37FE8D8628345FC4C /* Pods-Runner.release.xcconfig */, - B52E8054D72425147F13731E /* Pods-Runner.profile.xcconfig */, - CA190E3529AB505F3A85EBD1 /* Pods-RunnerTests.debug.xcconfig */, - 947BE44BAD67A1CD0AF00C93 /* Pods-RunnerTests.release.xcconfig */, - 28F29AF2EFF3D12A4F86A875 /* Pods-RunnerTests.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -150,7 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - 036FA8D726FA705A0E65D51E /* Pods */, + A37746E677C9A9E3C64A5712 /* Pods */, ); sourceTree = ""; }; @@ -198,11 +185,24 @@ path = Runner; sourceTree = ""; }; + A37746E677C9A9E3C64A5712 /* Pods */ = { + isa = PBXGroup; + children = ( + 4F5FFE286CDC0C8049A1E2CC /* Pods-Runner.debug.xcconfig */, + 20B7BA6E1026A9B6B10A454B /* Pods-Runner.release.xcconfig */, + 7D80084AE92896674F90848B /* Pods-Runner.profile.xcconfig */, + 6F2087016D1C279A64CB3683 /* Pods-RunnerTests.debug.xcconfig */, + 9251D52C91D4F76968D19FE3 /* Pods-RunnerTests.release.xcconfig */, + 32A4923180C802605A70969C /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - 00FB58E3465AC56328D3498C /* Pods_Runner.framework */, - 60ED9BFA95AA126999220261 /* Pods_RunnerTests.framework */, + 518E13D127F67BA055E3CC77 /* Pods_Runner.framework */, + 045143EA540BBF064066157F /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -214,7 +214,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 6E4E491CAD2FC1FBD06650A5 /* [CP] Check Pods Manifest.lock */, + 79C6156BD9FD2FD0C1B6D6AE /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -233,13 +233,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 6836EC8E2FDAB9914F00760C /* [CP] Check Pods Manifest.lock */, + 8021B2A2C172E63956D9AC13 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 2E50CF68C5B1ADDA19341C1C /* [CP] Embed Pods Frameworks */, + 375FA3EA8885225EFFB4CD2C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -322,23 +322,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 2E50CF68C5B1ADDA19341C1C /* [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; - }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -377,7 +360,24 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 6836EC8E2FDAB9914F00760C /* [CP] Check Pods Manifest.lock */ = { + 375FA3EA8885225EFFB4CD2C /* [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; + }; + 79C6156BD9FD2FD0C1B6D6AE /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -392,14 +392,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 6E4E491CAD2FC1FBD06650A5 /* [CP] Check Pods Manifest.lock */ = { + 8021B2A2C172E63956D9AC13 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -414,7 +414,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -472,7 +472,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CA190E3529AB505F3A85EBD1 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 6F2087016D1C279A64CB3683 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -487,7 +487,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 947BE44BAD67A1CD0AF00C93 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 9251D52C91D4F76968D19FE3 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -502,7 +502,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 28F29AF2EFF3D12A4F86A875 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 32A4923180C802605A70969C /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4e81be77..4a74faca 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index db44369c..b3c17614 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -4,7 +4,7 @@ import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return false + return true } override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 9838e2f8..79b456d0 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = chat_interface PRODUCT_BUNDLE_IDENTIFIER = com.liphium.chatInterface // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2024 com.liphium. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2025 com.liphium. All rights reserved. diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index cd29841b..82d308e8 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -14,5 +14,9 @@ keychain-access-groups + com.apple.security.device.camera + + com.apple.security.device.microphone + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 999432fc..82d308e8 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,11 +4,19 @@ com.apple.security.app-sandbox + com.apple.security.cs.allow-jit + + com.apple.security.network.server + com.apple.security.network.client com.apple.security.files.user-selected.read-write keychain-access-groups + com.apple.security.device.camera + + com.apple.security.device.microphone + diff --git a/pubspec.lock b/pubspec.lock index 96aec588..bc8a3b61 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,31 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "82.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "13c1e6c6fd460522ea840abec3f677cc226f5fec7872c04ad7b425517ccf54f7" url: "https://pub.dev" source: hosted - version: "6.11.0" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" - url: "https://pub.dev" - source: hosted - version: "0.11.3" + version: "7.4.4" archive: dependency: "direct main" description: @@ -42,42 +29,42 @@ packages: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "0511d6be23b007e95105ae023db599aea731df604608978dada7f9faf2637623" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.6.4" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" audio_session: dependency: transitive description: name: audio_session - sha256: b2a26ba8b7efa1790d6460e82971fde3e398cfbe2295df9dea22f3499d2c12a7 + sha256: f100a780050adf5c552d8b59d7af32a60229fda79c623e64004caf9368ccc748 url: "https://pub.dev" source: hosted - version: "0.1.23" + version: "0.2.1" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: @@ -86,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" + source: hosted + version: "2.1.0" build_config: dependency: transitive description: @@ -98,26 +93,26 @@ packages: dependency: transitive description: name: build_daemon - sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.4.15" build_runner_core: dependency: transitive description: @@ -138,18 +133,18 @@ packages: dependency: transitive description: name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 url: "https://pub.dev" source: hosted - version: "8.9.3" + version: "8.9.5" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: transitive description: @@ -166,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: @@ -178,10 +181,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -191,13 +194,13 @@ packages: source: hosted version: "4.10.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" console: dependency: transitive description: @@ -218,10 +221,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "802bd084fb82e55df091ec8ad1553a7331b61c08251eef19a508b6f3f3a9858d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.13.1" cross_file: dependency: transitive description: @@ -250,10 +253,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "3.0.1" + dart_webrtc: + dependency: transitive + description: + name: dart_webrtc + sha256: "5b76fd85ac95d6f5dee3e7d7de8d4b51bfbec1dc73804647c6aebb52d6297116" + url: "https://pub.dev" + source: hosted + version: "1.5.3+hotfix.2" db_viewer: dependency: transitive description: @@ -262,30 +273,47 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + dbus_secrets: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "5ce433d7336a0afce1cc5c41cd99dbf3783255f3" + url: "https://github.com/Liphium/dbus_secrets.git" + source: git + version: "0.0.1" dio: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.0+1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.1" drift: dependency: "direct main" description: name: drift - sha256: c2d073d35ad441730812f4ea05b5dd031fb81c5f9786a4f5fb77ecd6307b6f74 + sha256: b584ddeb2b74436735dd2cf746d2d021e19a9a6770f409212fd5cbc2814ada85 url: "https://pub.dev" source: hosted - version: "2.22.1" + version: "2.26.1" drift_db_viewer: dependency: "direct main" description: @@ -298,10 +326,10 @@ packages: dependency: "direct dev" description: name: drift_dev - sha256: f4ab5d6976b1e31551ceb82ff597a505bda7818ff4f7be08a1da9d55eb6e730c + sha256: "54dc207c6e4662741f60e5752678df183957ab907754ffab0372a7082f6d2816" url: "https://pub.dev" source: hosted - version: "2.22.1" + version: "2.26.1" encrypt: dependency: "direct main" description: @@ -322,34 +350,34 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: "direct main" description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" ffigen: dependency: "direct dev" description: name: ffigen - sha256: e0bdaa4ff30106aab68e7fa19311df4ced2035dc07be30f2e112855e8dcd3259 + sha256: "72d732c33557fc0ca9b46379d3deff2dadbdc539696dc0b270189e2989be20ef" url: "https://pub.dev" source: hosted - version: "16.0.0" + version: "18.1.0" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" file_selector: dependency: "direct main" description: @@ -362,10 +390,10 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "98ac58e878b05ea2fdb204e7f4fc4978d90406c9881874f901428e01d3b18fbc" + sha256: "6bba3d590ee9462758879741abc132a19133600dd31832f55627442f1ebd7b54" url: "https://pub.dev" source: hosted - version: "0.5.1+12" + version: "0.5.1+14" file_selector_ios: dependency: transitive description: @@ -410,10 +438,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.3+4" fixnum: dependency: transitive description: @@ -435,11 +463,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" - flutter_driver: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -448,30 +471,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" + url: "https://pub.dev" + source: hosted + version: "2.9.0" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" url: "https://pub.dev" source: hosted - version: "9.2.2" + version: "9.2.4" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.3" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -514,14 +545,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_webrtc: + dependency: "direct main" + description: + name: flutter_webrtc + sha256: e84ca404ef4b0d07a0fcd676b15814fabd5bc08e8d4b8dd8b634f76beab1bb50 + url: "https://pub.dev" + source: hosted + version: "0.14.0" freezed: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -538,19 +577,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - fuchsia_remote_debug_protocol: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" get: dependency: "direct main" description: name: get - sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 url: "https://pub.dev" source: hosted - version: "4.6.6" + version: "4.7.2" get_it: dependency: transitive description: @@ -563,10 +597,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -579,18 +613,18 @@ packages: dependency: transitive description: name: html - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5" + version: "0.15.6" http: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -603,10 +637,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" image: dependency: transitive description: @@ -615,11 +649,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.0" - integration_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" io: dependency: transitive description: @@ -648,26 +677,26 @@ packages: dependency: "direct main" description: name: just_audio - sha256: a49e7120b95600bd357f37a2bb04cd1e88252f7cdea8f3368803779b925b1049 + sha256: "90421d34e9ee9a407c3722e9db6a45c57b90620b51fcf3a172bae4c3c3278ed5" url: "https://pub.dev" source: hosted - version: "0.9.42" + version: "0.10.2" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" + sha256: "4cd94536af0219fa306205a58e78d67e02b0555283c1c094ee41e402a14a5c4a" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.0" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "9a98035b8b24b40749507687520ec5ab404e291d2b0937823ff45d92cb18d448" + sha256: "8c7e779892e180cbc9ffb5a3c52f6e90e1cbbf4a63694cc450972a7edbd2bb6d" url: "https://pub.dev" source: hosted - version: "0.4.13" + version: "0.4.15" just_audio_windows: dependency: "direct main" description: @@ -680,18 +709,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -700,6 +729,13 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + libspaceship: + dependency: "direct main" + description: + path: rust_builder + relative: true + source: path + version: "0.0.1" lints: dependency: transitive description: @@ -723,22 +759,23 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive + lorien_chat_list: + dependency: "direct main" description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" + path: "." + ref: master + resolved-ref: "262c65789adfd9c89792174bb8458e331a34a35b" + url: "https://github.com/Unbreathable/chat_list.git" + source: git + version: "0.0.2" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -747,22 +784,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" - menu_base: - dependency: transitive - description: - name: menu_base - sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" - url: "https://pub.dev" - source: hosted - version: "0.1.1" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -775,10 +804,10 @@ packages: dependency: "direct dev" description: name: msix - sha256: c50d6bd1aafe0d071a3c1e5a5ccb056404502935cb0a549e3178c4aae16caf33 + sha256: edde648a8133bf301883c869d19d127049683037c65ff64173ba526ac7a8af2f url: "https://pub.dev" source: hosted - version: "3.16.8" + version: "3.16.9" nested: dependency: transitive description: @@ -863,26 +892,26 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" pasteboard: dependency: "direct main" description: name: pasteboard - sha256: "7bf733f3a00c7188ec1f2c6f0612854248b302cf91ef3611a2b7bb141c0f9d55" + sha256: "9ff73ada33f79a59ff91f6c01881fd4ed0a0031cfc4ae2d86c0384471525fca1" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.0" path: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: @@ -895,10 +924,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -935,26 +964,26 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" url: "https://pub.dev" source: hosted - version: "11.3.1" + version: "11.4.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc url: "https://pub.dev" source: hosted - version: "12.0.13" + version: "12.1.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 url: "https://pub.dev" source: hosted - version: "9.4.5" + version: "9.4.7" permission_handler_html: dependency: transitive description: @@ -967,10 +996,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.3.0" permission_handler_windows: dependency: transitive description: @@ -983,18 +1012,18 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1019,38 +1048,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: + preact_signals: dependency: transitive description: - name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + name: preact_signals + sha256: a5b445796a02244b85fa43d79a7030fbfbbb08f7c48277ee93c636140a8fb2e0 url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "1.8.3" provider: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" quiver: dependency: transitive description: @@ -1159,18 +1188,34 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + signals: + dependency: "direct main" + description: + name: signals + sha256: "85225d03e71720b6fff62a8296ba7328a3e363d1ffbb9f3a85efaa05d9e85e67" + url: "https://pub.dev" + source: hosted + version: "6.0.2" + signals_core: + dependency: transitive + description: + name: signals_core + sha256: "5668ff1fb953fe48c88b03d50d43c5fe128cd7c831b932a4157f5f6be868b80f" url: "https://pub.dev" source: hosted - version: "2.0.1" - shortid: + version: "6.0.2" + signals_flutter: dependency: transitive description: - name: shortid - sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + name: signals_flutter + sha256: "66b36ed0b5961cc7d8d0606813bc3fd66758dc1728cbfcffef0d2a97c7a706a2" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "6.0.2" sky_engine: dependency: transitive description: flutter @@ -1187,20 +1232,19 @@ packages: sodium_libs: dependency: "direct main" description: - path: "packages/sodium_libs" - ref: "49ddd98a700785e51b78b22c191bf645de1edfc8" - resolved-ref: "49ddd98a700785e51b78b22c191bf645de1edfc8" - url: "https://github.com/Skycoder42/libsodium_dart_bindings.git" - source: git - version: "3.4.2" + name: sodium_libs + sha256: "5d01dcf7f11fbf9dc9670fc7b98300090155a08e451921dc0845c674a225cade" + url: "https://pub.dev" + source: hosted + version: "3.4.4+3" source_gen: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_map_stack_trace: dependency: transitive description: @@ -1221,10 +1265,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -1237,42 +1281,42 @@ packages: dependency: transitive description: name: sqlite3 - sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5 + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.5" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.28" + version: "0.5.32" sqlparser: dependency: transitive description: name: sqlparser - sha256: "4cad4b2c5f63dc9ea1a8dcffb58cf762322bea5dd8836870164a65e913bdae41" + sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee" url: "https://pub.dev" source: hosted - version: "0.40.0" + version: "0.41.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -1285,58 +1329,50 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - sync_http: - dependency: transitive - description: - name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.3.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: "direct dev" description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.8" timing: dependency: transitive description: @@ -1345,14 +1381,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - tray_manager: - dependency: "direct main" - description: - name: tray_manager - sha256: f231031c5c0eb4ad514e18ddaab27a912ddbe50335c594bc28fb0f9972ab6a84 - url: "https://pub.dev" - source: hosted - version: "0.3.1" typed_data: dependency: transitive description: @@ -1381,18 +1409,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.14" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1421,18 +1449,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" uuid: dependency: transitive description: @@ -1453,10 +1481,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" watcher: dependency: transitive description: @@ -1469,10 +1497,10 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: "direct main" description: @@ -1485,18 +1513,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" - webdriver: - dependency: transitive - description: - name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" - url: "https://pub.dev" - source: hosted - version: "3.0.4" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: @@ -1505,14 +1525,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: "86fe3afc81a08481dfb25cf14a5a94e27062ecef25544783f352c914e0bbc1ca" + url: "https://pub.dev" + source: hosted + version: "1.2.2+hotfix.2" win32: dependency: transitive description: name: win32 - sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.12.0" window_manager: dependency: "direct main" description: @@ -1554,5 +1582,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.2 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index f8110e55..6cdfdbb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: "none" version: 1.0.0-ALPHA environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.7.0 <4.0.0" dependencies: flutter: @@ -20,11 +20,7 @@ dependencies: http: ">=1.2.1 <2.0.0" pointycastle: ">=3.8.0 <4.0.0" url_launcher: ">=6.2.6 <7.0.0" - sodium_libs: - git: - url: https://github.com/Skycoder42/libsodium_dart_bindings.git - ref: 49ddd98a700785e51b78b22c191bf645de1edfc8 - path: packages/sodium_libs + sodium_libs: ">=3.4.3 <4.0.0" ffi: ">=2.1.2 <3.0.0" freezed_annotation: ">=2.4.1 <3.0.0" file_selector: ">=1.0.3 <2.0.0" @@ -42,12 +38,25 @@ dependencies: just_audio_windows: ">=0.2.1 <1.0.0" sqlite3_flutter_libs: ">=0.5.24 <1.0.0" fading_edge_scrollview: ">=4.1.1 <5.0.0" - tray_manager: ">=0.2.4 <1.0.0" liphium_bridge: path: ./liphium_bridge web_socket: ">=0.1.6 <1.0.0" flutter_secure_storage: ">=9.2.2 <10.0.0" open_file: ">=3.5.10 <4.0.0" + flutter_webrtc: ">=0.12.5+hotfix.2 <1.0.0" + signals: ">=6.0.2 <7.0.0" + libspaceship: + path: rust_builder + flutter_rust_bridge: 2.9.0 + lorien_chat_list: + git: + url: https://github.com/Unbreathable/chat_list.git + ref: master + collection: ^1.19.1 + dbus_secrets: + git: + url: https://github.com/Liphium/dbus_secrets.git + ref: main dev_dependencies: flutter_test: @@ -59,8 +68,6 @@ dev_dependencies: ffigen: any freezed: any msix: any - integration_test: - sdk: flutter flutter: uses-material-design: true @@ -69,15 +76,6 @@ flutter: - family: Roboto Mono fonts: - asset: assets/RobotoMono.ttf - - family: Roboto - fonts: - - asset: assets/Roboto-Regular.ttf - - family: Open Sans - fonts: - - asset: assets/OpenSans.ttf - - family: Emoji - fonts: - - asset: assets/NotoColorEmoji-Regular.ttf assets: - assets/ diff --git a/rust_builder/.gitignore b/rust_builder/.gitignore new file mode 100644 index 00000000..ac5aa989 --- /dev/null +++ b/rust_builder/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/rust_builder/README.md b/rust_builder/README.md new file mode 100644 index 00000000..922615f9 --- /dev/null +++ b/rust_builder/README.md @@ -0,0 +1 @@ +Please ignore this folder, which is just glue to build Rust with Flutter. \ No newline at end of file diff --git a/rust_builder/android/.gitignore b/rust_builder/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/rust_builder/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/rust_builder/android/build.gradle b/rust_builder/android/build.gradle new file mode 100644 index 00000000..45e5b4a9 --- /dev/null +++ b/rust_builder/android/build.gradle @@ -0,0 +1,56 @@ +// The Android Gradle Plugin builds the native code with the Android NDK. + +group 'com.flutter_rust_bridge.libspaceship' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + // The Android Gradle Plugin knows how to build native code with the NDK. + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.flutter_rust_bridge.libspaceship' + } + + // Bumping the plugin compileSdkVersion requires all clients of this plugin + // to bump the version in their app. + compileSdkVersion 33 + + // Use the NDK version + // declared in /android/app/build.gradle file of the Flutter project. + // Replace it with a version number if this plugin requires a specfic NDK version. + // (e.g. ndkVersion "23.1.7779620") + ndkVersion android.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 19 + } +} + +apply from: "../cargokit/gradle/plugin.gradle" +cargokit { + manifestDir = "../../libspaceship" + libname = "libspaceship" +} diff --git a/rust_builder/android/settings.gradle b/rust_builder/android/settings.gradle new file mode 100644 index 00000000..9b247f86 --- /dev/null +++ b/rust_builder/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'libspaceship' diff --git a/rust_builder/android/src/main/AndroidManifest.xml b/rust_builder/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ec3c65b2 --- /dev/null +++ b/rust_builder/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/rust_builder/cargokit/.gitignore b/rust_builder/cargokit/.gitignore new file mode 100644 index 00000000..cf7bb868 --- /dev/null +++ b/rust_builder/cargokit/.gitignore @@ -0,0 +1,4 @@ +target +.dart_tool +*.iml +!pubspec.lock diff --git a/rust_builder/cargokit/LICENSE b/rust_builder/cargokit/LICENSE new file mode 100644 index 00000000..d33a5fea --- /dev/null +++ b/rust_builder/cargokit/LICENSE @@ -0,0 +1,42 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Copyright 2022 Matej Knopp + +================================================================================ + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +================================================================================ + +APACHE LICENSE, VERSION 2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/rust_builder/cargokit/README b/rust_builder/cargokit/README new file mode 100644 index 00000000..398474db --- /dev/null +++ b/rust_builder/cargokit/README @@ -0,0 +1,11 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Experimental repository to provide glue for seamlessly integrating cargo build +with flutter plugins and packages. + +See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ +for a tutorial on how to use Cargokit. + +Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. + diff --git a/rust_builder/cargokit/build_pod.sh b/rust_builder/cargokit/build_pod.sh new file mode 100644 index 00000000..ed0e0d98 --- /dev/null +++ b/rust_builder/cargokit/build_pod.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -e + +BASEDIR=$(dirname "$0") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +BASEDIR=$(cd "$BASEDIR" ; pwd -P) + +# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project +NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` + +export PATH=${NEW_PATH%?} # remove trailing : + +env + +# Platform name (macosx, iphoneos, iphonesimulator) +export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME + +# Arctive architectures (arm64, armv7, x86_64), space separated. +export CARGOKIT_DARWIN_ARCHS=$ARCHS + +# Current build configuration (Debug, Release) +export CARGOKIT_CONFIGURATION=$CONFIGURATION + +# Path to directory containing Cargo.toml. +export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 + +# Temporary directory for build artifacts. +export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR + +# Output directory for final artifacts. +export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME + +# Directory to store built tool artifacts. +export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool + +# Directory inside root project. Not necessarily the top level directory of root project. +export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT + +FLUTTER_EXPORT_BUILD_ENVIRONMENT=( + "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS + "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS +) + +for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" +do + if [[ -f "$path" ]]; then + source "$path" + fi +done + +sh "$BASEDIR/run_build_tool.sh" build-pod "$@" + +# Make a symlink from built framework to phony file, which will be used as input to +# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate +# attribute on custom build phase) +ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" +ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/rust_builder/cargokit/build_tool/README.md b/rust_builder/cargokit/build_tool/README.md new file mode 100644 index 00000000..a878c279 --- /dev/null +++ b/rust_builder/cargokit/build_tool/README.md @@ -0,0 +1,5 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/rust_builder/cargokit/build_tool/analysis_options.yaml b/rust_builder/cargokit/build_tool/analysis_options.yaml new file mode 100644 index 00000000..0e16a8b0 --- /dev/null +++ b/rust_builder/cargokit/build_tool/analysis_options.yaml @@ -0,0 +1,34 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +linter: + rules: + - prefer_relative_imports + - directives_ordering + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/rust_builder/cargokit/build_tool/bin/build_tool.dart b/rust_builder/cargokit/build_tool/bin/build_tool.dart new file mode 100644 index 00000000..268eb524 --- /dev/null +++ b/rust_builder/cargokit/build_tool/bin/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:build_tool/build_tool.dart' as build_tool; + +void main(List arguments) { + build_tool.runMain(arguments); +} diff --git a/rust_builder/cargokit/build_tool/lib/build_tool.dart b/rust_builder/cargokit/build_tool/lib/build_tool.dart new file mode 100644 index 00000000..7c1bb750 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'src/build_tool.dart' as build_tool; + +Future runMain(List args) async { + return build_tool.runMain(args); +} diff --git a/rust_builder/cargokit/build_tool/lib/src/android_environment.dart b/rust_builder/cargokit/build_tool/lib/src/android_environment.dart new file mode 100644 index 00000000..15fc9eed --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/android_environment.dart @@ -0,0 +1,195 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; +import 'package:version/version.dart'; + +import 'target.dart'; +import 'util.dart'; + +class AndroidEnvironment { + AndroidEnvironment({ + required this.sdkPath, + required this.ndkVersion, + required this.minSdkVersion, + required this.targetTempDir, + required this.target, + }); + + static void clangLinkerWrapper(List args) { + final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; + if (clang == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); + } + final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; + if (target == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); + } + + runCommand(clang, [ + target, + ...args, + ]); + } + + /// Full path to Android SDK. + final String sdkPath; + + /// Full version of Android NDK. + final String ndkVersion; + + /// Minimum supported SDK version. + final int minSdkVersion; + + /// Target directory for build artifacts. + final String targetTempDir; + + /// Target being built. + final Target target; + + bool ndkIsInstalled() { + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); + return ndkPackageXml.existsSync(); + } + + void installNdk({ + required String javaHome, + }) { + final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; + final sdkManager = path.join( + sdkPath, + 'cmdline-tools', + 'latest', + 'bin', + 'sdkmanager$sdkManagerExtension', + ); + + log.info('Installing NDK $ndkVersion'); + runCommand(sdkManager, [ + '--install', + 'ndk;$ndkVersion', + ], environment: { + 'JAVA_HOME': javaHome, + }); + } + + Future> buildEnvironment() async { + final hostArch = Platform.isMacOS + ? "darwin-x86_64" + : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); + + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final toolchainPath = path.join( + ndkPath, + 'toolchains', + 'llvm', + 'prebuilt', + hostArch, + 'bin', + ); + + final minSdkVersion = + math.max(target.androidMinSdkVersion!, this.minSdkVersion); + + final exe = Platform.isWindows ? '.exe' : ''; + + final arKey = 'AR_${target.rust}'; + final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] + .map((e) => path.join(toolchainPath, e)) + .firstWhereOrNull((element) => File(element).existsSync()); + if (arValue == null) { + throw Exception('Failed to find ar for $target in $toolchainPath'); + } + + final targetArg = '--target=${target.rust}$minSdkVersion'; + + final ccKey = 'CC_${target.rust}'; + final ccValue = path.join(toolchainPath, 'clang$exe'); + final cfFlagsKey = 'CFLAGS_${target.rust}'; + final cFlagsValue = targetArg; + + final cxxKey = 'CXX_${target.rust}'; + final cxxValue = path.join(toolchainPath, 'clang++$exe'); + final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; + final cxxFlagsValue = targetArg; + + final linkerKey = + 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); + + final ranlibKey = 'RANLIB_${target.rust}'; + final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); + + final ndkVersionParsed = Version.parse(ndkVersion); + final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; + final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); + + final runRustTool = + Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; + + final packagePath = (await Isolate.resolvePackageUri( + Uri.parse('package:build_tool/buildtool.dart')))! + .toFilePath(); + final selfPath = path.canonicalize(path.join( + packagePath, + '..', + '..', + '..', + runRustTool, + )); + + // Make sure that run_build_tool is working properly even initially launched directly + // through dart run. + final toolTempDir = + Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; + + return { + arKey: arValue, + ccKey: ccValue, + cfFlagsKey: cFlagsValue, + cxxKey: cxxValue, + cxxFlagsKey: cxxFlagsValue, + ranlibKey: ranlibValue, + rustFlagsKey: rustFlagsValue, + linkerKey: selfPath, + // Recognized by main() so we know when we're acting as a wrapper + '_CARGOKIT_NDK_LINK_TARGET': targetArg, + '_CARGOKIT_NDK_LINK_CLANG': ccValue, + 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, + }; + } + + // Workaround for libgcc missing in NDK23, inspired by cargo-ndk + String _libGccWorkaround(String buildDir, Version ndkVersion) { + final workaroundDir = path.join( + buildDir, + 'cargokit', + 'libgcc_workaround', + '${ndkVersion.major}', + ); + Directory(workaroundDir).createSync(recursive: true); + if (ndkVersion.major >= 23) { + File(path.join(workaroundDir, 'libgcc.a')) + .writeAsStringSync('INPUT(-lunwind)'); + } else { + // Other way around, untested, forward libgcc.a from libunwind once Rust + // gets updated for NDK23+. + File(path.join(workaroundDir, 'libunwind.a')) + .writeAsStringSync('INPUT(-lgcc)'); + } + + var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; + if (rustFlags.isNotEmpty) { + rustFlags = '$rustFlags\x1f'; + } + rustFlags = '$rustFlags-L\x1f$workaroundDir'; + return rustFlags; + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart b/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart new file mode 100644 index 00000000..e608cece --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart @@ -0,0 +1,266 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'builder.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'rustup.dart'; +import 'target.dart'; + +class Artifact { + /// File system location of the artifact. + final String path; + + /// Actual file name that the artifact should have in destination folder. + final String finalFileName; + + AritifactType get type { + if (finalFileName.endsWith('.dll') || + finalFileName.endsWith('.dll.lib') || + finalFileName.endsWith('.pdb') || + finalFileName.endsWith('.so') || + finalFileName.endsWith('.dylib')) { + return AritifactType.dylib; + } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { + return AritifactType.staticlib; + } else { + throw Exception('Unknown artifact type for $finalFileName'); + } + } + + Artifact({ + required this.path, + required this.finalFileName, + }); +} + +final _log = Logger('artifacts_provider'); + +class ArtifactProvider { + ArtifactProvider({ + required this.environment, + required this.userOptions, + }); + + final BuildEnvironment environment; + final CargokitUserOptions userOptions; + + Future>> getArtifacts(List targets) async { + final result = await _getPrecompiledArtifacts(targets); + + final pendingTargets = List.of(targets); + pendingTargets.removeWhere((element) => result.containsKey(element)); + + if (pendingTargets.isEmpty) { + return result; + } + + final rustup = Rustup(); + for (final target in targets) { + final builder = RustBuilder(target: target, environment: environment); + builder.prepare(rustup); + _log.info('Building ${environment.crateInfo.packageName} for $target'); + final targetDir = await builder.build(); + // For local build accept both static and dynamic libraries. + final artifactNames = { + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.dylib, + remote: false, + ), + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.staticlib, + remote: false, + ) + }; + final artifacts = artifactNames + .map((artifactName) => Artifact( + path: path.join(targetDir, artifactName), + finalFileName: artifactName, + )) + .where((element) => File(element.path).existsSync()) + .toList(); + result[target] = artifacts; + } + return result; + } + + Future>> _getPrecompiledArtifacts( + List targets) async { + if (userOptions.usePrecompiledBinaries == false) { + _log.info('Precompiled binaries are disabled'); + return {}; + } + if (environment.crateOptions.precompiledBinaries == null) { + _log.fine('Precompiled binaries not enabled for this crate'); + return {}; + } + + final start = Stopwatch()..start(); + final crateHash = CrateHash.compute(environment.manifestDir, + tempStorage: environment.targetTempDir); + _log.fine( + 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); + + final downloadedArtifactsDir = + path.join(environment.targetTempDir, 'precompiled', crateHash); + Directory(downloadedArtifactsDir).createSync(recursive: true); + + final res = >{}; + + for (final target in targets) { + final requiredArtifacts = getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + remote: true, + ); + final artifactsForTarget = []; + + for (final artifact in requiredArtifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final downloadedPath = path.join(downloadedArtifactsDir, fileName); + if (!File(downloadedPath).existsSync()) { + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + await _tryDownloadArtifacts( + crateHash: crateHash, + fileName: fileName, + signatureFileName: signatureFileName, + finalPath: downloadedPath, + ); + } + if (File(downloadedPath).existsSync()) { + artifactsForTarget.add(Artifact( + path: downloadedPath, + finalFileName: artifact, + )); + } else { + break; + } + } + + // Only provide complete set of artifacts. + if (artifactsForTarget.length == requiredArtifacts.length) { + _log.fine('Found precompiled artifacts for $target'); + res[target] = artifactsForTarget; + } + } + + return res; + } + + static Future _get(Uri url, {Map? headers}) async { + int attempt = 0; + const maxAttempts = 10; + while (true) { + try { + return await get(url, headers: headers); + } on SocketException catch (e) { + // Try to detect reset by peer error and retry. + if (attempt++ < maxAttempts && + (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { + _log.severe( + 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); + await Future.delayed(Duration(seconds: 1)); + continue; + } else { + rethrow; + } + } + } + } + + Future _tryDownloadArtifacts({ + required String crateHash, + required String fileName, + required String signatureFileName, + required String finalPath, + }) async { + final precompiledBinaries = environment.crateOptions.precompiledBinaries!; + final prefix = precompiledBinaries.uriPrefix; + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); + _log.fine('Downloading signature from $signatureUrl'); + final signature = await _get(signatureUrl); + if (signature.statusCode == 404) { + _log.warning( + 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); + return; + } + if (signature.statusCode != 200) { + _log.severe( + 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); + return; + } + _log.fine('Downloading binary from $url'); + final res = await _get(url); + if (res.statusCode != 200) { + _log.severe('Failed to download binary $url: status ${res.statusCode}'); + return; + } + if (verify( + precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { + File(finalPath).writeAsBytesSync(res.bodyBytes); + } else { + _log.shout('Signature verification failed! Ignoring binary.'); + } + } +} + +enum AritifactType { + staticlib, + dylib, +} + +AritifactType artifactTypeForTarget(Target target) { + if (target.darwinPlatform != null) { + return AritifactType.staticlib; + } else { + return AritifactType.dylib; + } +} + +List getArtifactNames({ + required Target target, + required String libraryName, + required bool remote, + AritifactType? aritifactType, +}) { + aritifactType ??= artifactTypeForTarget(target); + if (target.darwinArch != null) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.dylib']; + } + } else if (target.rust.contains('-windows-')) { + if (aritifactType == AritifactType.staticlib) { + return ['$libraryName.lib']; + } else { + return [ + '$libraryName.dll', + '$libraryName.dll.lib', + if (!remote) '$libraryName.pdb' + ]; + } + } else if (target.rust.contains('-linux-')) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.so']; + } + } else { + throw Exception("Unsupported target: ${target.rust}"); + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart b/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart new file mode 100644 index 00000000..6f3b2a4e --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart @@ -0,0 +1,40 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +class BuildCMake { + final CargokitUserOptions userOptions; + + BuildCMake({required this.userOptions}); + + Future build() async { + final targetPlatform = Environment.targetPlatform; + final target = Target.forFlutterName(Environment.targetPlatform); + if (target == null) { + throw Exception("Unknown target platform: $targetPlatform"); + } + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts([target]); + + final libs = artifacts[target]!; + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path) + .copySync(path.join(Environment.outputDir, lib.finalFileName)); + } + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart b/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart new file mode 100644 index 00000000..7e61fcbb --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart @@ -0,0 +1,49 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +final log = Logger('build_gradle'); + +class BuildGradle { + BuildGradle({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.targetPlatforms.map((arch) { + final target = Target.forFlutterName(arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: true); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + for (final target in targets) { + final libs = artifacts[target]!; + final outputDir = path.join(Environment.outputDir, target.android!); + Directory(outputDir).createSync(recursive: true); + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); + } + } + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/build_pod.dart b/rust_builder/cargokit/build_tool/lib/src/build_pod.dart new file mode 100644 index 00000000..8a9c0db5 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/build_pod.dart @@ -0,0 +1,89 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; +import 'util.dart'; + +class BuildPod { + BuildPod({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.darwinArchs.map((arch) { + final target = Target.forDarwin( + platformName: Environment.darwinPlatformName, darwinAarch: arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + void performLipo(String targetFile, Iterable sourceFiles) { + runCommand("lipo", [ + '-create', + ...sourceFiles, + '-output', + targetFile, + ]); + } + + final outputDir = Environment.outputDir; + + Directory(outputDir).createSync(recursive: true); + + final staticLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.staticlib) + .toList(); + final dynamicLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.dylib) + .toList(); + + final libName = environment.crateInfo.packageName; + + // If there is static lib, use it and link it with pod + if (staticLibs.isNotEmpty) { + final finalTargetFile = path.join(outputDir, "lib$libName.a"); + performLipo(finalTargetFile, staticLibs.map((e) => e.path)); + } else { + // Otherwise try to replace bundle dylib with our dylib + final bundlePaths = [ + '$libName.framework/Versions/A/$libName', + '$libName.framework/$libName', + ]; + + for (final bundlePath in bundlePaths) { + final targetFile = path.join(outputDir, bundlePath); + if (File(targetFile).existsSync()) { + performLipo(targetFile, dynamicLibs.map((e) => e.path)); + + // Replace absolute id with @rpath one so that it works properly + // when moved to Frameworks. + runCommand("install_name_tool", [ + '-id', + '@rpath/$bundlePath', + targetFile, + ]); + return; + } + } + throw Exception('Unable to find bundle for dynamic library'); + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/build_tool.dart b/rust_builder/cargokit/build_tool/lib/src/build_tool.dart new file mode 100644 index 00000000..c8f36981 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/build_tool.dart @@ -0,0 +1,271 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; + +import 'android_environment.dart'; +import 'build_cmake.dart'; +import 'build_gradle.dart'; +import 'build_pod.dart'; +import 'logging.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; +import 'util.dart'; +import 'verify_binaries.dart'; + +final log = Logger('build_tool'); + +abstract class BuildCommand extends Command { + Future runBuildCommand(CargokitUserOptions options); + + @override + Future run() async { + final options = CargokitUserOptions.load(); + + if (options.verboseLogging || + Platform.environment['CARGOKIT_VERBOSE'] == '1') { + enableVerboseLogging(); + } + + await runBuildCommand(options); + } +} + +class BuildPodCommand extends BuildCommand { + @override + final name = 'build-pod'; + + @override + final description = 'Build cocoa pod library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildPod(userOptions: options); + await build.build(); + } +} + +class BuildGradleCommand extends BuildCommand { + @override + final name = 'build-gradle'; + + @override + final description = 'Build android library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildGradle(userOptions: options); + await build.build(); + } +} + +class BuildCMakeCommand extends BuildCommand { + @override + final name = 'build-cmake'; + + @override + final description = 'Build CMake library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildCMake(userOptions: options); + await build.build(); + } +} + +class GenKeyCommand extends Command { + @override + final name = 'gen-key'; + + @override + final description = 'Generate key pair for signing precompiled binaries'; + + @override + void run() { + final kp = generateKey(); + final private = HEX.encode(kp.privateKey.bytes); + final public = HEX.encode(kp.publicKey.bytes); + print("Private Key: $private"); + print("Public Key: $public"); + } +} + +class PrecompileBinariesCommand extends Command { + PrecompileBinariesCommand() { + argParser + ..addOption( + 'repository', + mandatory: true, + help: 'Github repository slug in format owner/name', + ) + ..addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ) + ..addMultiOption('target', + help: 'Rust target triple of artifact to build.\n' + 'Can be specified multiple times or omitted in which case\n' + 'all targets for current platform will be built.') + ..addOption( + 'android-sdk-location', + help: 'Location of Android SDK (if available)', + ) + ..addOption( + 'android-ndk-version', + help: 'Android NDK version (if available)', + ) + ..addOption( + 'android-min-sdk-version', + help: 'Android minimum rquired version (if available)', + ) + ..addOption( + 'temp-dir', + help: 'Directory to store temporary build artifacts', + ) + ..addFlag( + "verbose", + abbr: "v", + defaultsTo: false, + help: "Enable verbose logging", + ); + } + + @override + final name = 'precompile-binaries'; + + @override + final description = 'Prebuild and upload binaries\n' + 'Private key must be passed through PRIVATE_KEY environment variable. ' + 'Use gen_key through generate priave key.\n' + 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; + + @override + Future run() async { + final verbose = argResults!['verbose'] as bool; + if (verbose) { + enableVerboseLogging(); + } + + final privateKeyString = Platform.environment['PRIVATE_KEY']; + if (privateKeyString == null) { + throw ArgumentError('Missing PRIVATE_KEY environment variable'); + } + final githubToken = Platform.environment['GITHUB_TOKEN']; + if (githubToken == null) { + throw ArgumentError('Missing GITHUB_TOKEN environment variable'); + } + final privateKey = HEX.decode(privateKeyString); + if (privateKey.length != 64) { + throw ArgumentError('Private key must be 64 bytes long'); + } + final manifestDir = argResults!['manifest-dir'] as String; + if (!Directory(manifestDir).existsSync()) { + throw ArgumentError('Manifest directory does not exist: $manifestDir'); + } + String? androidMinSdkVersionString = + argResults!['android-min-sdk-version'] as String?; + int? androidMinSdkVersion; + if (androidMinSdkVersionString != null) { + androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); + if (androidMinSdkVersion == null) { + throw ArgumentError( + 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); + } + } + final targetStrigns = argResults!['target'] as List; + final targets = targetStrigns.map((target) { + final res = Target.forRustTriple(target); + if (res == null) { + throw ArgumentError('Invalid target: $target'); + } + return res; + }).toList(growable: false); + final precompileBinaries = PrecompileBinaries( + privateKey: PrivateKey(privateKey), + githubToken: githubToken, + manifestDir: manifestDir, + repositorySlug: RepositorySlug.full(argResults!['repository'] as String), + targets: targets, + androidSdkLocation: argResults!['android-sdk-location'] as String?, + androidNdkVersion: argResults!['android-ndk-version'] as String?, + androidMinSdkVersion: androidMinSdkVersion, + tempDir: argResults!['temp-dir'] as String?, + ); + + await precompileBinaries.run(); + } +} + +class VerifyBinariesCommand extends Command { + VerifyBinariesCommand() { + argParser.addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ); + } + + @override + final name = "verify-binaries"; + + @override + final description = 'Verifies published binaries\n' + 'Checks whether there is a binary published for each targets\n' + 'and checks the signature.'; + + @override + Future run() async { + final manifestDir = argResults!['manifest-dir'] as String; + final verifyBinaries = VerifyBinaries( + manifestDir: manifestDir, + ); + await verifyBinaries.run(); + } +} + +Future runMain(List args) async { + try { + // Init logging before options are loaded + initLogging(); + + if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { + return AndroidEnvironment.clangLinkerWrapper(args); + } + + final runner = CommandRunner('build_tool', 'Cargokit built_tool') + ..addCommand(BuildPodCommand()) + ..addCommand(BuildGradleCommand()) + ..addCommand(BuildCMakeCommand()) + ..addCommand(GenKeyCommand()) + ..addCommand(PrecompileBinariesCommand()) + ..addCommand(VerifyBinariesCommand()); + + await runner.run(args); + } on ArgumentError catch (e) { + stderr.writeln(e.toString()); + exit(1); + } catch (e, s) { + log.severe(kDoubleSeparator); + log.severe('Cargokit BuildTool failed with error:'); + log.severe(kSeparator); + log.severe(e); + // This tells user to install Rust, there's no need to pollute the log with + // stack trace. + if (e is! RustupNotFoundException) { + log.severe(kSeparator); + log.severe(s); + log.severe(kSeparator); + log.severe('BuildTool arguments: $args'); + } + log.severe(kDoubleSeparator); + exit(1); + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/builder.dart b/rust_builder/cargokit/build_tool/lib/src/builder.dart new file mode 100644 index 00000000..84c46e4f --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/builder.dart @@ -0,0 +1,198 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'android_environment.dart'; +import 'cargo.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; +import 'util.dart'; + +final _log = Logger('builder'); + +enum BuildConfiguration { + debug, + release, + profile, +} + +extension on BuildConfiguration { + bool get isDebug => this == BuildConfiguration.debug; + String get rustName => switch (this) { + BuildConfiguration.debug => 'debug', + BuildConfiguration.release => 'release', + BuildConfiguration.profile => 'release', + }; +} + +class BuildException implements Exception { + final String message; + + BuildException(this.message); + + @override + String toString() { + return 'BuildException: $message'; + } +} + +class BuildEnvironment { + final BuildConfiguration configuration; + final CargokitCrateOptions crateOptions; + final String targetTempDir; + final String manifestDir; + final CrateInfo crateInfo; + + final bool isAndroid; + final String? androidSdkPath; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? javaHome; + + BuildEnvironment({ + required this.configuration, + required this.crateOptions, + required this.targetTempDir, + required this.manifestDir, + required this.crateInfo, + required this.isAndroid, + this.androidSdkPath, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.javaHome, + }); + + static BuildConfiguration parseBuildConfiguration(String value) { + // XCode configuration adds the flavor to configuration name. + final firstSegment = value.split('-').first; + final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( + (e) => e.name == firstSegment, + ); + if (buildConfiguration == null) { + _log.warning('Unknown build configuraiton $value, will assume release'); + return BuildConfiguration.release; + } + return buildConfiguration; + } + + static BuildEnvironment fromEnvironment({ + required bool isAndroid, + }) { + final buildConfiguration = + parseBuildConfiguration(Environment.configuration); + final manifestDir = Environment.manifestDir; + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + final crateInfo = CrateInfo.load(manifestDir); + return BuildEnvironment( + configuration: buildConfiguration, + crateOptions: crateOptions, + targetTempDir: Environment.targetTempDir, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: isAndroid, + androidSdkPath: isAndroid ? Environment.sdkPath : null, + androidNdkVersion: isAndroid ? Environment.ndkVersion : null, + androidMinSdkVersion: + isAndroid ? int.parse(Environment.minSdkVersion) : null, + javaHome: isAndroid ? Environment.javaHome : null, + ); + } +} + +class RustBuilder { + final Target target; + final BuildEnvironment environment; + + RustBuilder({ + required this.target, + required this.environment, + }); + + void prepare( + Rustup rustup, + ) { + final toolchain = _toolchain; + if (rustup.installedTargets(toolchain) == null) { + rustup.installToolchain(toolchain); + } + if (toolchain == 'nightly') { + rustup.installRustSrcForNightly(); + } + if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { + rustup.installTarget(target.rust, toolchain: toolchain); + } + } + + CargoBuildOptions? get _buildOptions => + environment.crateOptions.cargo[environment.configuration]; + + String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; + + /// Returns the path of directory containing build artifacts. + Future build() async { + final extraArgs = _buildOptions?.flags ?? []; + final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); + runCommand( + 'rustup', + [ + 'run', + _toolchain, + 'cargo', + 'build', + ...extraArgs, + '--manifest-path', + manifestPath, + '-p', + environment.crateInfo.packageName, + if (!environment.configuration.isDebug) '--release', + '--target', + target.rust, + '--target-dir', + environment.targetTempDir, + ], + environment: await _buildEnvironment(), + ); + return path.join( + environment.targetTempDir, + target.rust, + environment.configuration.rustName, + ); + } + + Future> _buildEnvironment() async { + if (target.android == null) { + return {}; + } else { + final sdkPath = environment.androidSdkPath; + final ndkVersion = environment.androidNdkVersion; + final minSdkVersion = environment.androidMinSdkVersion; + if (sdkPath == null) { + throw BuildException('androidSdkPath is not set'); + } + if (ndkVersion == null) { + throw BuildException('androidNdkVersion is not set'); + } + if (minSdkVersion == null) { + throw BuildException('androidMinSdkVersion is not set'); + } + final env = AndroidEnvironment( + sdkPath: sdkPath, + ndkVersion: ndkVersion, + minSdkVersion: minSdkVersion, + targetTempDir: environment.targetTempDir, + target: target, + ); + if (!env.ndkIsInstalled() && environment.javaHome != null) { + env.installNdk(javaHome: environment.javaHome!); + } + return env.buildEnvironment(); + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/cargo.dart b/rust_builder/cargokit/build_tool/lib/src/cargo.dart new file mode 100644 index 00000000..0d8958ff --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/cargo.dart @@ -0,0 +1,48 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:toml/toml.dart'; + +class ManifestException { + ManifestException(this.message, {required this.fileName}); + + final String? fileName; + final String message; + + @override + String toString() { + if (fileName != null) { + return 'Failed to parse package manifest at $fileName: $message'; + } else { + return 'Failed to parse package manifest: $message'; + } + } +} + +class CrateInfo { + CrateInfo({required this.packageName}); + + final String packageName; + + static CrateInfo parseManifest(String manifest, {final String? fileName}) { + final toml = TomlDocument.parse(manifest); + final package = toml.toMap()['package']; + if (package == null) { + throw ManifestException('Missing package section', fileName: fileName); + } + final name = package['name']; + if (name == null) { + throw ManifestException('Missing package name', fileName: fileName); + } + return CrateInfo(packageName: name); + } + + static CrateInfo load(String manifestDir) { + final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); + final manifest = manifestFile.readAsStringSync(); + return parseManifest(manifest, fileName: manifestFile.path); + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart b/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart new file mode 100644 index 00000000..0c4d88d1 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart @@ -0,0 +1,124 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as path; + +class CrateHash { + /// Computes a hash uniquely identifying crate content. This takes into account + /// content all all .rs files inside the src directory, as well as Cargo.toml, + /// Cargo.lock, build.rs and cargokit.yaml. + /// + /// If [tempStorage] is provided, computed hash is stored in a file in that directory + /// and reused on subsequent calls if the crate content hasn't changed. + static String compute(String manifestDir, {String? tempStorage}) { + return CrateHash._( + manifestDir: manifestDir, + tempStorage: tempStorage, + )._compute(); + } + + CrateHash._({ + required this.manifestDir, + required this.tempStorage, + }); + + String _compute() { + final files = getFiles(); + final tempStorage = this.tempStorage; + if (tempStorage != null) { + final quickHash = _computeQuickHash(files); + final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); + quickHashFolder.createSync(recursive: true); + final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); + if (quickHashFile.existsSync()) { + return quickHashFile.readAsStringSync(); + } + final hash = _computeHash(files); + quickHashFile.writeAsStringSync(hash); + return hash; + } else { + return _computeHash(files); + } + } + + /// Computes a quick hash based on files stat (without reading contents). This + /// is used to cache the real hash, which is slower to compute since it involves + /// reading every single file. + String _computeQuickHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + final data = ByteData(8); + for (final file in files) { + input.add(utf8.encode(file.path)); + final stat = file.statSync(); + data.setUint64(0, stat.size); + input.add(data.buffer.asUint8List()); + data.setUint64(0, stat.modified.millisecondsSinceEpoch); + input.add(data.buffer.asUint8List()); + } + + input.close(); + return base64Url.encode(output.events.single.bytes); + } + + String _computeHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + void addTextFile(File file) { + // text Files are hashed by lines in case we're dealing with github checkout + // that auto-converts line endings. + final splitter = LineSplitter(); + if (file.existsSync()) { + final data = file.readAsStringSync(); + final lines = splitter.convert(data); + for (final line in lines) { + input.add(utf8.encode(line)); + } + } + } + + for (final file in files) { + addTextFile(file); + } + + input.close(); + final res = output.events.single; + + // Truncate to 128bits. + final hash = res.bytes.sublist(0, 16); + return hex.encode(hash); + } + + List getFiles() { + final src = Directory(path.join(manifestDir, 'src')); + final files = src + .listSync(recursive: true, followLinks: false) + .whereType() + .toList(); + files.sortBy((element) => element.path); + void addFile(String relative) { + final file = File(path.join(manifestDir, relative)); + if (file.existsSync()) { + files.add(file); + } + } + + addFile('Cargo.toml'); + addFile('Cargo.lock'); + addFile('build.rs'); + addFile('cargokit.yaml'); + return files; + } + + final String manifestDir; + final String? tempStorage; +} diff --git a/rust_builder/cargokit/build_tool/lib/src/environment.dart b/rust_builder/cargokit/build_tool/lib/src/environment.dart new file mode 100644 index 00000000..996483a1 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/environment.dart @@ -0,0 +1,68 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +extension on String { + String resolveSymlink() => File(this).resolveSymbolicLinksSync(); +} + +class Environment { + /// Current build configuration (debug or release). + static String get configuration => + _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); + + static bool get isDebug => configuration == 'debug'; + static bool get isRelease => configuration == 'release'; + + /// Temporary directory where Rust build artifacts are placed. + static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); + + /// Final output directory where the build artifacts are placed. + static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); + + /// Path to the crate manifest (containing Cargo.toml). + static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); + + /// Directory inside root project. Not necessarily root folder. Symlinks are + /// not resolved on purpose. + static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); + + // Pod + + /// Platform name (macosx, iphoneos, iphonesimulator). + static String get darwinPlatformName => + _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); + + /// List of architectures to build for (arm64, armv7, x86_64). + static List get darwinArchs => + _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); + + // Gradle + static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); + static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); + static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); + static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); + static List get targetPlatforms => + _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); + + // CMAKE + static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); + + static String _getEnv(String key) { + final res = Platform.environment[key]; + if (res == null) { + throw Exception("Missing environment variable $key"); + } + return res; + } + + static String _getEnvPath(String key) { + final res = _getEnv(key); + if (Directory(res).existsSync()) { + return res.resolveSymlink(); + } else { + return res; + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/logging.dart b/rust_builder/cargokit/build_tool/lib/src/logging.dart new file mode 100644 index 00000000..5edd4fd1 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/logging.dart @@ -0,0 +1,52 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; + +const String kSeparator = "--"; +const String kDoubleSeparator = "=="; + +bool _lastMessageWasSeparator = false; + +void _log(LogRecord rec) { + final prefix = '${rec.level.name}: '; + final out = rec.level == Level.SEVERE ? stderr : stdout; + if (rec.message == kSeparator) { + if (!_lastMessageWasSeparator) { + out.write(prefix); + out.writeln('-' * 80); + _lastMessageWasSeparator = true; + } + return; + } else if (rec.message == kDoubleSeparator) { + out.write(prefix); + out.writeln('=' * 80); + _lastMessageWasSeparator = true; + return; + } + out.write(prefix); + out.writeln(rec.message); + _lastMessageWasSeparator = false; +} + +void initLogging() { + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((LogRecord rec) { + final lines = rec.message.split('\n'); + for (final line in lines) { + if (line.isNotEmpty || lines.length == 1 || line != lines.last) { + _log(LogRecord( + rec.level, + line, + rec.loggerName, + )); + } + } + }); +} + +void enableVerboseLogging() { + Logger.root.level = Level.ALL; +} diff --git a/rust_builder/cargokit/build_tool/lib/src/options.dart b/rust_builder/cargokit/build_tool/lib/src/options.dart new file mode 100644 index 00000000..22aef1d3 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/options.dart @@ -0,0 +1,309 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import 'builder.dart'; +import 'environment.dart'; +import 'rustup.dart'; + +final _log = Logger('options'); + +/// A class for exceptions that have source span information attached. +class SourceSpanException implements Exception { + // This is a getter so that subclasses can override it. + /// A message describing the exception. + String get message => _message; + final String _message; + + // This is a getter so that subclasses can override it. + /// The span associated with this exception. + /// + /// This may be `null` if the source location can't be determined. + SourceSpan? get span => _span; + final SourceSpan? _span; + + SourceSpanException(this._message, this._span); + + /// Returns a string representation of `this`. + /// + /// [color] may either be a [String], a [bool], or `null`. If it's a string, + /// it indicates an ANSI terminal color escape that should be used to + /// highlight the span's text. If it's `true`, it indicates that the text + /// should be highlighted using the default color. If it's `false` or `null`, + /// it indicates that the text shouldn't be highlighted. + @override + String toString({Object? color}) { + if (span == null) return message; + return 'Error on ${span!.message(message, color: color)}'; + } +} + +enum Toolchain { + stable, + beta, + nightly, +} + +class CargoBuildOptions { + final Toolchain toolchain; + final List flags; + + CargoBuildOptions({ + required this.toolchain, + required this.flags, + }); + + static Toolchain _toolchainFromNode(YamlNode node) { + if (node case YamlScalar(value: String name)) { + final toolchain = + Toolchain.values.firstWhereOrNull((element) => element.name == name); + if (toolchain != null) { + return toolchain; + } + } + throw SourceSpanException( + 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', + node.span); + } + + static CargoBuildOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + Toolchain toolchain = Toolchain.stable; + List flags = []; + for (final MapEntry(:key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: 'toolchain')) { + toolchain = _toolchainFromNode(value); + } else if (key case YamlScalar(value: 'extra_flags')) { + if (value case YamlList(nodes: List list)) { + if (list.every((element) { + if (element case YamlScalar(value: String _)) { + return true; + } + return false; + })) { + flags = list.map((e) => e.value as String).toList(); + continue; + } + } + throw SourceSpanException( + 'Extra flags must be a list of strings', value.span); + } else { + throw SourceSpanException( + 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', + key.span); + } + } + return CargoBuildOptions(toolchain: toolchain, flags: flags); + } +} + +extension on YamlMap { + /// Map that extracts keys so that we can do map case check on them. + Map get valueMap => + nodes.map((key, value) => MapEntry(key.value, value)); +} + +class PrecompiledBinaries { + final String uriPrefix; + final PublicKey publicKey; + + PrecompiledBinaries({ + required this.uriPrefix, + required this.publicKey, + }); + + static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { + final bytes = HEX.decode(key); + if (bytes.length != 32) { + throw SourceSpanException( + 'Invalid public key. Must be 32 bytes long.', span); + } + return PublicKey(bytes); + } + + static PrecompiledBinaries parse(YamlNode node) { + if (node case YamlMap(valueMap: Map map)) { + if (map + case { + 'url_prefix': YamlNode urlPrefixNode, + 'public_key': YamlNode publicKeyNode, + }) { + final urlPrefix = switch (urlPrefixNode) { + YamlScalar(value: String urlPrefix) => urlPrefix, + _ => throw SourceSpanException( + 'Invalid URL prefix value.', urlPrefixNode.span), + }; + final publicKey = switch (publicKeyNode) { + YamlScalar(value: String publicKey) => + _publicKeyFromHex(publicKey, publicKeyNode.span), + _ => throw SourceSpanException( + 'Invalid public key value.', publicKeyNode.span), + }; + return PrecompiledBinaries( + uriPrefix: urlPrefix, + publicKey: publicKey, + ); + } + } + throw SourceSpanException( + 'Invalid precompiled binaries value. ' + 'Expected Map with "url_prefix" and "public_key".', + node.span); + } +} + +/// Cargokit options specified for Rust crate. +class CargokitCrateOptions { + CargokitCrateOptions({ + this.cargo = const {}, + this.precompiledBinaries, + }); + + final Map cargo; + final PrecompiledBinaries? precompiledBinaries; + + static CargokitCrateOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + final options = {}; + PrecompiledBinaries? precompiledBinaries; + + for (final entry in node.nodes.entries) { + if (entry + case MapEntry( + key: YamlScalar(value: 'cargo'), + value: YamlNode node, + )) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: String name)) { + final configuration = BuildConfiguration.values + .firstWhereOrNull((element) => element.name == name); + if (configuration != null) { + options[configuration] = CargoBuildOptions.parse(value); + continue; + } + } + throw SourceSpanException( + 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', + key.span); + } + } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { + precompiledBinaries = PrecompiledBinaries.parse(entry.value); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', + entry.key.span); + } + } + return CargokitCrateOptions( + cargo: options, + precompiledBinaries: precompiledBinaries, + ); + } + + static CargokitCrateOptions load({ + required String manifestDir, + }) { + final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); + final file = File.fromUri(uri); + if (file.existsSync()) { + final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); + return parse(contents); + } else { + return CargokitCrateOptions(); + } + } +} + +class CargokitUserOptions { + // When Rustup is installed always build locally unless user opts into + // using precompiled binaries. + static bool defaultUsePrecompiledBinaries() { + return Rustup.executablePath() == null; + } + + CargokitUserOptions({ + required this.usePrecompiledBinaries, + required this.verboseLogging, + }); + + CargokitUserOptions._() + : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), + verboseLogging = false; + + static CargokitUserOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); + bool verboseLogging = false; + + for (final entry in node.nodes.entries) { + if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { + if (entry.value case YamlScalar(value: bool value)) { + usePrecompiledBinaries = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "use_precompiled_binaries". Must be a boolean.', + entry.value.span); + } else if (entry.key case YamlScalar(value: 'verbose_logging')) { + if (entry.value case YamlScalar(value: bool value)) { + verboseLogging = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "verbose_logging". Must be a boolean.', + entry.value.span); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', + entry.key.span); + } + } + return CargokitUserOptions( + usePrecompiledBinaries: usePrecompiledBinaries, + verboseLogging: verboseLogging, + ); + } + + static CargokitUserOptions load() { + String fileName = "cargokit_options.yaml"; + var userProjectDir = Directory(Environment.rootProjectDir); + + while (userProjectDir.parent.path != userProjectDir.path) { + final configFile = File(path.join(userProjectDir.path, fileName)); + if (configFile.existsSync()) { + final contents = loadYamlNode( + configFile.readAsStringSync(), + sourceUrl: configFile.uri, + ); + final res = parse(contents); + if (res.verboseLogging) { + _log.info('Found user options file at ${configFile.path}'); + } + return res; + } + userProjectDir = userProjectDir.parent; + } + return CargokitUserOptions._(); + } + + final bool usePrecompiledBinaries; + final bool verboseLogging; +} diff --git a/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart b/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart new file mode 100644 index 00000000..c27f4195 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart @@ -0,0 +1,202 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; + +final _log = Logger('precompile_binaries'); + +class PrecompileBinaries { + PrecompileBinaries({ + required this.privateKey, + required this.githubToken, + required this.repositorySlug, + required this.manifestDir, + required this.targets, + this.androidSdkLocation, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.tempDir, + }); + + final PrivateKey privateKey; + final String githubToken; + final RepositorySlug repositorySlug; + final String manifestDir; + final List targets; + final String? androidSdkLocation; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? tempDir; + + static String fileName(Target target, String name) { + return '${target.rust}_$name'; + } + + static String signatureFileName(Target target, String name) { + return '${target.rust}_$name.sig'; + } + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final targets = List.of(this.targets); + if (targets.isEmpty) { + targets.addAll([ + ...Target.buildableTargets(), + if (androidSdkLocation != null) ...Target.androidTargets(), + ]); + } + + _log.info('Precompiling binaries for $targets'); + + final hash = CrateHash.compute(manifestDir); + _log.info('Computed crate hash: $hash'); + + final String tagName = 'precompiled_$hash'; + + final github = GitHub(auth: Authentication.withToken(githubToken)); + final repo = github.repositories; + final release = await _getOrCreateRelease( + repo: repo, + tagName: tagName, + packageName: crateInfo.packageName, + hash: hash, + ); + + final tempDir = this.tempDir != null + ? Directory(this.tempDir!) + : Directory.systemTemp.createTempSync('precompiled_'); + + tempDir.createSync(recursive: true); + + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + + final buildEnvironment = BuildEnvironment( + configuration: BuildConfiguration.release, + crateOptions: crateOptions, + targetTempDir: tempDir.path, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: androidSdkLocation != null, + androidSdkPath: androidSdkLocation, + androidNdkVersion: androidNdkVersion, + androidMinSdkVersion: androidMinSdkVersion, + ); + + final rustup = Rustup(); + + for (final target in targets) { + final artifactNames = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + if (artifactNames.every((name) { + final fileName = PrecompileBinaries.fileName(target, name); + return (release.assets ?? []).any((e) => e.name == fileName); + })) { + _log.info("All artifacts for $target already exist - skipping"); + continue; + } + + _log.info('Building for $target'); + + final builder = + RustBuilder(target: target, environment: buildEnvironment); + builder.prepare(rustup); + final res = await builder.build(); + + final assets = []; + for (final name in artifactNames) { + final file = File(path.join(res, name)); + if (!file.existsSync()) { + throw Exception('Missing artifact: ${file.path}'); + } + + final data = file.readAsBytesSync(); + final create = CreateReleaseAsset( + name: PrecompileBinaries.fileName(target, name), + contentType: "application/octet-stream", + assetData: data, + ); + final signature = sign(privateKey, data); + final signatureCreate = CreateReleaseAsset( + name: signatureFileName(target, name), + contentType: "application/octet-stream", + assetData: signature, + ); + bool verified = verify(public(privateKey), data, signature); + if (!verified) { + throw Exception('Signature verification failed'); + } + assets.add(create); + assets.add(signatureCreate); + } + _log.info('Uploading assets: ${assets.map((e) => e.name)}'); + for (final asset in assets) { + // This seems to be failing on CI so do it one by one + int retryCount = 0; + while (true) { + try { + await repo.uploadReleaseAssets(release, [asset]); + break; + } on Exception catch (e) { + if (retryCount == 10) { + rethrow; + } + ++retryCount; + _log.shout( + 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); + await Future.delayed(Duration(seconds: 2)); + } + } + } + } + + _log.info('Cleaning up'); + tempDir.deleteSync(recursive: true); + } + + Future _getOrCreateRelease({ + required RepositoriesService repo, + required String tagName, + required String packageName, + required String hash, + }) async { + Release release; + try { + _log.info('Fetching release $tagName'); + release = await repo.getReleaseByTagName(repositorySlug, tagName); + } on ReleaseNotFound { + _log.info('Release not found - creating release $tagName'); + release = await repo.createRelease( + repositorySlug, + CreateRelease.from( + tagName: tagName, + name: 'Precompiled binaries ${hash.substring(0, 8)}', + targetCommitish: null, + isDraft: false, + isPrerelease: false, + body: 'Precompiled binaries for crate $packageName, ' + 'crate hash $hash.', + )); + } + return release; + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/rustup.dart b/rust_builder/cargokit/build_tool/lib/src/rustup.dart new file mode 100644 index 00000000..0ac8d086 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/rustup.dart @@ -0,0 +1,136 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; + +import 'util.dart'; + +class _Toolchain { + _Toolchain( + this.name, + this.targets, + ); + + final String name; + final List targets; +} + +class Rustup { + List? installedTargets(String toolchain) { + final targets = _installedTargets(toolchain); + return targets != null ? List.unmodifiable(targets) : null; + } + + void installToolchain(String toolchain) { + log.info("Installing Rust toolchain: $toolchain"); + runCommand("rustup", ['toolchain', 'install', toolchain]); + _installedToolchains + .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); + } + + void installTarget( + String target, { + required String toolchain, + }) { + log.info("Installing Rust target: $target"); + runCommand("rustup", [ + 'target', + 'add', + '--toolchain', + toolchain, + target, + ]); + _installedTargets(toolchain)?.add(target); + } + + final List<_Toolchain> _installedToolchains; + + Rustup() : _installedToolchains = _getInstalledToolchains(); + + List? _installedTargets(String toolchain) => _installedToolchains + .firstWhereOrNull( + (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) + ?.targets; + + static List<_Toolchain> _getInstalledToolchains() { + String extractToolchainName(String line) { + // ignore (default) after toolchain name + final parts = line.split(' '); + return parts[0]; + } + + final res = runCommand("rustup", ['toolchain', 'list']); + + // To list all non-custom toolchains, we need to filter out lines that + // don't start with "stable", "beta", or "nightly". + Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) + .map(extractToolchainName) + .toList(growable: true); + + return lines + .map( + (name) => _Toolchain( + name, + _getInstalledTargets(name), + ), + ) + .toList(growable: true); + } + + static List _getInstalledTargets(String toolchain) { + final res = runCommand("rustup", [ + 'target', + 'list', + '--toolchain', + toolchain, + '--installed', + ]); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty) + .toList(growable: true); + return lines; + } + + bool _didInstallRustSrcForNightly = false; + + void installRustSrcForNightly() { + if (_didInstallRustSrcForNightly) { + return; + } + // Useful for -Z build-std + runCommand( + "rustup", + ['component', 'add', 'rust-src', '--toolchain', 'nightly'], + ); + _didInstallRustSrcForNightly = true; + } + + static String? executablePath() { + final envPath = Platform.environment['PATH']; + final envPathSeparator = Platform.isWindows ? ';' : ':'; + final home = Platform.isWindows + ? Platform.environment['USERPROFILE'] + : Platform.environment['HOME']; + final paths = [ + if (home != null) path.join(home, '.cargo', 'bin'), + if (envPath != null) ...envPath.split(envPathSeparator), + ]; + for (final p in paths) { + final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; + final rustupPath = path.join(p, rustup); + if (File(rustupPath).existsSync()) { + return rustupPath; + } + } + return null; + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/target.dart b/rust_builder/cargokit/build_tool/lib/src/target.dart new file mode 100644 index 00000000..6fbc58b6 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/target.dart @@ -0,0 +1,140 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; + +import 'util.dart'; + +class Target { + Target({ + required this.rust, + this.flutter, + this.android, + this.androidMinSdkVersion, + this.darwinPlatform, + this.darwinArch, + }); + + static final all = [ + Target( + rust: 'armv7-linux-androideabi', + flutter: 'android-arm', + android: 'armeabi-v7a', + androidMinSdkVersion: 16, + ), + Target( + rust: 'aarch64-linux-android', + flutter: 'android-arm64', + android: 'arm64-v8a', + androidMinSdkVersion: 21, + ), + Target( + rust: 'i686-linux-android', + flutter: 'android-x86', + android: 'x86', + androidMinSdkVersion: 16, + ), + Target( + rust: 'x86_64-linux-android', + flutter: 'android-x64', + android: 'x86_64', + androidMinSdkVersion: 21, + ), + Target( + rust: 'x86_64-pc-windows-msvc', + flutter: 'windows-x64', + ), + Target( + rust: 'x86_64-unknown-linux-gnu', + flutter: 'linux-x64', + ), + Target( + rust: 'aarch64-unknown-linux-gnu', + flutter: 'linux-arm64', + ), + Target( + rust: 'x86_64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'x86_64', + ), + Target( + rust: 'aarch64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios', + darwinPlatform: 'iphoneos', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios-sim', + darwinPlatform: 'iphonesimulator', + darwinArch: 'arm64', + ), + Target( + rust: 'x86_64-apple-ios', + darwinPlatform: 'iphonesimulator', + darwinArch: 'x86_64', + ), + ]; + + static Target? forFlutterName(String flutterName) { + return all.firstWhereOrNull((element) => element.flutter == flutterName); + } + + static Target? forDarwin({ + required String platformName, + required String darwinAarch, + }) { + return all.firstWhereOrNull((element) => // + element.darwinPlatform == platformName && + element.darwinArch == darwinAarch); + } + + static Target? forRustTriple(String triple) { + return all.firstWhereOrNull((element) => element.rust == triple); + } + + static List androidTargets() { + return all + .where((element) => element.android != null) + .toList(growable: false); + } + + /// Returns buildable targets on current host platform ignoring Android targets. + static List buildableTargets() { + if (Platform.isLinux) { + // Right now we don't support cross-compiling on Linux. So we just return + // the host target. + final arch = runCommand('arch', []).stdout as String; + if (arch.trim() == 'aarch64') { + return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; + } else { + return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; + } + } + return all.where((target) { + if (Platform.isWindows) { + return target.rust.contains('-windows-'); + } else if (Platform.isMacOS) { + return target.darwinPlatform != null; + } + return false; + }).toList(growable: false); + } + + @override + String toString() { + return rust; + } + + final String? flutter; + final String rust; + final String? android; + final int? androidMinSdkVersion; + final String? darwinPlatform; + final String? darwinArch; +} diff --git a/rust_builder/cargokit/build_tool/lib/src/util.dart b/rust_builder/cargokit/build_tool/lib/src/util.dart new file mode 100644 index 00000000..8bb6a872 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/util.dart @@ -0,0 +1,172 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'logging.dart'; +import 'rustup.dart'; + +final log = Logger("process"); + +class CommandFailedException implements Exception { + final String executable; + final List arguments; + final ProcessResult result; + + CommandFailedException({ + required this.executable, + required this.arguments, + required this.result, + }); + + @override + String toString() { + final stdout = result.stdout.toString().trim(); + final stderr = result.stderr.toString().trim(); + return [ + "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", + "Returned Exit Code: ${result.exitCode}", + kSeparator, + "STDOUT:", + if (stdout.isNotEmpty) stdout, + kSeparator, + "STDERR:", + if (stderr.isNotEmpty) stderr, + ].join('\n'); + } +} + +class TestRunCommandArgs { + final String executable; + final List arguments; + final String? workingDirectory; + final Map? environment; + final bool includeParentEnvironment; + final bool runInShell; + final Encoding? stdoutEncoding; + final Encoding? stderrEncoding; + + TestRunCommandArgs({ + required this.executable, + required this.arguments, + this.workingDirectory, + this.environment, + this.includeParentEnvironment = true, + this.runInShell = false, + this.stdoutEncoding, + this.stderrEncoding, + }); +} + +class TestRunCommandResult { + TestRunCommandResult({ + this.pid = 1, + this.exitCode = 0, + this.stdout = '', + this.stderr = '', + }); + + final int pid; + final int exitCode; + final String stdout; + final String stderr; +} + +TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; + +ProcessResult runCommand( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, +}) { + if (testRunCommandOverride != null) { + final result = testRunCommandOverride!(TestRunCommandArgs( + executable: executable, + arguments: arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + )); + return ProcessResult( + result.pid, + result.exitCode, + result.stdout, + result.stderr, + ); + } + log.finer('Running command $executable ${arguments.join(' ')}'); + final res = Process.runSync( + _resolveExecutable(executable), + arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stderrEncoding: stderrEncoding, + stdoutEncoding: stdoutEncoding, + ); + if (res.exitCode != 0) { + throw CommandFailedException( + executable: executable, + arguments: arguments, + result: res, + ); + } else { + return res; + } +} + +class RustupNotFoundException implements Exception { + @override + String toString() { + return [ + ' ', + 'rustup not found in PATH.', + ' ', + 'Maybe you need to install Rust? It only takes a minute:', + ' ', + if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', + if (hasHomebrewRustInPath()) ...[ + '\$ brew unlink rust # Unlink homebrew Rust from PATH', + ], + if (!Platform.isWindows) + "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", + ' ', + ].join('\n'); + } + + static bool hasHomebrewRustInPath() { + if (!Platform.isMacOS) { + return false; + } + final envPath = Platform.environment['PATH'] ?? ''; + final paths = envPath.split(':'); + return paths.any((p) { + return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); + }); + } +} + +String _resolveExecutable(String executable) { + if (executable == 'rustup') { + final resolved = Rustup.executablePath(); + if (resolved != null) { + return resolved; + } + throw RustupNotFoundException(); + } else { + return executable; + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart b/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart new file mode 100644 index 00000000..2366b57b --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart @@ -0,0 +1,84 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; + +import 'artifacts_provider.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; + +class VerifyBinaries { + VerifyBinaries({ + required this.manifestDir, + }); + + final String manifestDir; + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final config = CargokitCrateOptions.load(manifestDir: manifestDir); + final precompiledBinaries = config.precompiledBinaries; + if (precompiledBinaries == null) { + stdout.writeln('Crate does not support precompiled binaries.'); + } else { + final crateHash = CrateHash.compute(manifestDir); + stdout.writeln('Crate hash: $crateHash'); + + for (final target in Target.all) { + final message = 'Checking ${target.rust}...'; + stdout.write(message.padRight(40)); + stdout.flush(); + + final artifacts = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + final prefix = precompiledBinaries.uriPrefix; + + bool ok = true; + + for (final artifact in artifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = + Uri.parse('$prefix$crateHash/$signatureFileName'); + + final signature = await get(signatureUrl); + if (signature.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + final asset = await get(url); + if (asset.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + + if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, + signature.bodyBytes)) { + stdout.writeln('INVALID SIGNATURE'); + ok = false; + } + } + + if (ok) { + stdout.writeln('OK'); + } + } + } + } +} diff --git a/rust_builder/cargokit/build_tool/pubspec.lock b/rust_builder/cargokit/build_tool/pubspec.lock new file mode 100644 index 00000000..343bdd36 --- /dev/null +++ b/rust_builder/cargokit/build_tool/pubspec.lock @@ -0,0 +1,453 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: "direct main" + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: "direct main" + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + ed25519_edwards: + dependency: "direct main" + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + github: + dependency: "direct main" + description: + name: github + sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" + url: "https://pub.dev" + source: hosted + version: "9.17.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + hex: + dependency: "direct main" + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: "direct main" + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" + url: "https://pub.dev" + source: hosted + version: "1.24.6" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + toml: + dependency: "direct main" + description: + name: toml + sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + version: + dependency: "direct main" + description: + name: version + sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + url: "https://pub.dev" + source: hosted + version: "11.9.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.0.0 <4.0.0" diff --git a/rust_builder/cargokit/build_tool/pubspec.yaml b/rust_builder/cargokit/build_tool/pubspec.yaml new file mode 100644 index 00000000..18c61e33 --- /dev/null +++ b/rust_builder/cargokit/build_tool/pubspec.yaml @@ -0,0 +1,33 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +name: build_tool +description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. +publish_to: none +version: 1.0.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + +# Add regular dependencies here. +dependencies: + # these are pinned on purpose because the bundle_tool_runner doesn't have + # pubspec.lock. See run_build_tool.sh + logging: 1.2.0 + path: 1.8.0 + version: 3.0.0 + collection: 1.18.0 + ed25519_edwards: 0.3.1 + hex: 0.2.0 + yaml: 3.1.2 + source_span: 1.10.0 + github: 9.17.0 + args: 2.4.2 + crypto: 3.0.3 + convert: 3.1.1 + http: 1.1.0 + toml: 0.14.0 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 diff --git a/rust_builder/cargokit/cmake/cargokit.cmake b/rust_builder/cargokit/cmake/cargokit.cmake new file mode 100644 index 00000000..ddd05df9 --- /dev/null +++ b/rust_builder/cargokit/cmake/cargokit.cmake @@ -0,0 +1,99 @@ +SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) + +if(WIN32) + # REALPATH does not properly resolve symlinks on windows :-/ + execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) +endif() + +# Arguments +# - target: CMAKE target to which rust library is linked +# - manifest_dir: relative path from current folder to directory containing cargo manifest +# - lib_name: cargo package name +# - any_symbol_name: name of any exported symbol from the library. +# used on windows to force linking with library. +function(apply_cargokit target manifest_dir lib_name any_symbol_name) + + set(CARGOKIT_LIB_NAME "${lib_name}") + set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") + if (CMAKE_CONFIGURATION_TYPES) + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") + else() + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") + endif() + set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") + + if (FLUTTER_TARGET_PLATFORM) + set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") + else() + set(CARGOKIT_TARGET_PLATFORM "windows-x64") + endif() + + set(CARGOKIT_ENV + "CARGOKIT_CMAKE=${CMAKE_COMMAND}" + "CARGOKIT_CONFIGURATION=$" + "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" + "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" + "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" + "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" + "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" + "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" + ) + + if (WIN32) + set(SCRIPT_EXTENSION ".cmd") + set(IMPORT_LIB_EXTENSION ".lib") + else() + set(SCRIPT_EXTENSION ".sh") + set(IMPORT_LIB_EXTENSION "") + execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") + endif() + + # Using generators in custom command is only supported in CMake 3.20+ + if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") + foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endforeach() + else() + add_custom_command( + OUTPUT + ${OUTPUT_LIB} + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endif() + + + set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) + + if (TARGET ${target}) + # If we have actual cmake target provided create target and make existing + # target depend on it + add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) + add_dependencies("${target}" "${target}_cargokit") + target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") + if(WIN32) + target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") + endif() + else() + # Otherwise (FFI) just use ALL to force building always + add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) + endif() + + # Allow adding the output library to plugin bundled libraries + set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) + +endfunction() diff --git a/rust_builder/cargokit/cmake/resolve_symlinks.ps1 b/rust_builder/cargokit/cmake/resolve_symlinks.ps1 new file mode 100644 index 00000000..3d10d283 --- /dev/null +++ b/rust_builder/cargokit/cmake/resolve_symlinks.ps1 @@ -0,0 +1,27 @@ +function Resolve-Symlinks { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string] $Path + ) + + [string] $separator = '/' + [string[]] $parts = $Path.Split($separator) + + [string] $realPath = '' + foreach ($part in $parts) { + if ($realPath -and !$realPath.EndsWith($separator)) { + $realPath += $separator + } + $realPath += $part + $item = Get-Item $realPath + if ($item.Target) { + $realPath = $item.Target.Replace('\', '/') + } + } + $realPath +} + +$path=Resolve-Symlinks -Path $args[0] +Write-Host $path diff --git a/rust_builder/cargokit/gradle/plugin.gradle b/rust_builder/cargokit/gradle/plugin.gradle new file mode 100644 index 00000000..1aead891 --- /dev/null +++ b/rust_builder/cargokit/gradle/plugin.gradle @@ -0,0 +1,179 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import java.nio.file.Paths +import org.apache.tools.ant.taskdefs.condition.Os + +CargoKitPlugin.file = buildscript.sourceFile + +apply plugin: CargoKitPlugin + +class CargoKitExtension { + String manifestDir; // Relative path to folder containing Cargo.toml + String libname; // Library name within Cargo.toml. Must be a cdylib +} + +abstract class CargoKitBuildTask extends DefaultTask { + + @Input + String buildMode + + @Input + String buildDir + + @Input + String outputDir + + @Input + String ndkVersion + + @Input + String sdkDirectory + + @Input + int compileSdkVersion; + + @Input + int minSdkVersion; + + @Input + String pluginFile + + @Input + List targetPlatforms + + @TaskAction + def build() { + if (project.cargokit.manifestDir == null) { + throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); + } + + if (project.cargokit.libname == null) { + throw new GradleException("Property 'libname' must be set on cargokit extension"); + } + + def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" + def path = Paths.get(new File(pluginFile).parent, "..", executableName); + + def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) + + def rootProjectDir = project.rootProject.projectDir + + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + project.exec { + commandLine 'chmod', '+x', path + } + } + + project.exec { + executable path + args "build-gradle" + environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir + environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" + environment "CARGOKIT_MANIFEST_DIR", manifestDir + environment "CARGOKIT_CONFIGURATION", buildMode + environment "CARGOKIT_TARGET_TEMP_DIR", buildDir + environment "CARGOKIT_OUTPUT_DIR", outputDir + environment "CARGOKIT_NDK_VERSION", ndkVersion + environment "CARGOKIT_SDK_DIR", sdkDirectory + environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion + environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion + environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") + environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] + } + } +} + +class CargoKitPlugin implements Plugin { + + static String file; + + private Plugin findFlutterPlugin(Project rootProject) { + _findFlutterPlugin(rootProject.childProjects) + } + + private Plugin _findFlutterPlugin(Map projects) { + for (project in projects) { + for (plugin in project.value.getPlugins()) { + if (plugin.class.name == "FlutterPlugin") { + return plugin; + } + } + def plugin = _findFlutterPlugin(project.value.childProjects); + if (plugin != null) { + return plugin; + } + } + return null; + } + + @Override + void apply(Project project) { + def plugin = findFlutterPlugin(project.rootProject); + + project.extensions.create("cargokit", CargoKitExtension) + + if (plugin == null) { + print("Flutter plugin not found, CargoKit plugin will not be applied.") + return; + } + + def cargoBuildDir = "${project.buildDir}/build" + + // Determine if the project is an application or library + def isApplication = plugin.project.plugins.hasPlugin('com.android.application') + def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants + + variants.all { variant -> + + final buildType = variant.buildType.name + + def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; + def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; + jniLibs.srcDir(new File(cargoOutputDir)) + + def platforms = plugin.getTargetPlatforms().collect() + + // Same thing addFlutterDependencies does in flutter.gradle + if (buildType == "debug") { + platforms.add("android-x86") + platforms.add("android-x64") + } + + // The task name depends on plugin properties, which are not available + // at this point + project.getGradle().afterProject { + def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; + + if (project.tasks.findByName(taskName)) { + return + } + + if (plugin.project.android.ndkVersion == null) { + throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") + } + + def task = project.tasks.create(taskName, CargoKitBuildTask.class) { + buildMode = variant.buildType.name + buildDir = cargoBuildDir + outputDir = cargoOutputDir + ndkVersion = plugin.project.android.ndkVersion + sdkDirectory = plugin.project.android.sdkDirectory + minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int + compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int + targetPlatforms = platforms + pluginFile = CargoKitPlugin.file + } + def onTask = { newTask -> + if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { + newTask.dependsOn task + // Fix gradle 7.4.2 not picking up JNI library changes + newTask.outputs.upToDateWhen { false } + } + } + project.tasks.each onTask + project.tasks.whenTaskAdded onTask + } + } + } +} diff --git a/rust_builder/cargokit/run_build_tool.cmd b/rust_builder/cargokit/run_build_tool.cmd new file mode 100644 index 00000000..c45d0aa8 --- /dev/null +++ b/rust_builder/cargokit/run_build_tool.cmd @@ -0,0 +1,91 @@ +@echo off +setlocal + +setlocal ENABLEDELAYEDEXPANSION + +SET BASEDIR=%~dp0 + +if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( + mkdir "%CARGOKIT_TOOL_TEMP_DIR%" +) +cd /D "%CARGOKIT_TOOL_TEMP_DIR%" + +SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool +SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart + +set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% + +( + echo name: build_tool_runner + echo version: 1.0.0 + echo publish_to: none + echo. + echo environment: + echo sdk: '^>=3.0.0 ^<4.0.0' + echo. + echo dependencies: + echo build_tool: + echo path: %BUILD_TOOL_PKG_DIR_POSIX% +) >pubspec.yaml + +if not exist bin ( + mkdir bin +) + +( + echo import 'package:build_tool/build_tool.dart' as build_tool; + echo void main^(List^ args^) ^{ + echo build_tool.runMain^(args^); + echo ^} +) >bin\build_tool_runner.dart + +SET PRECOMPILED=bin\build_tool_runner.dill + +REM To detect changes in package we compare output of DIR /s (recursive) +set PREV_PACKAGE_INFO=.dart_tool\package_info.prev +set CUR_PACKAGE_INFO=.dart_tool\package_info.cur + +DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" + +REM Last line in dir output is free space on harddrive. That is bound to +REM change between invocation so we need to remove it +( + Set "Line=" + For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( + SetLocal EnableDelayedExpansion + If Defined Line Echo !Line! + EndLocal + Set "Line=%%A") +) >"%CUR_PACKAGE_INFO%" +DEL "%CUR_PACKAGE_INFO%_orig" + +REM Compare current directory listing with previous +FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 + +If %ERRORLEVEL% neq 0 ( + REM Changed - copy current to previous and remove precompiled kernel + if exist "%PREV_PACKAGE_INFO%" ( + DEL "%PREV_PACKAGE_INFO%" + ) + MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" + if exist "%PRECOMPILED%" ( + DEL "%PRECOMPILED%" + ) +) + +REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% +REM which means we need to do pub get and precompile +if not exist "%PRECOMPILED%" ( + echo Running pub get in "%cd%" + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart +) + +"%DART%" "%PRECOMPILED%" %* + +REM 253 means invalid snapshot version. +If %ERRORLEVEL% equ 253 ( + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart + "%DART%" "%PRECOMPILED%" %* +) diff --git a/rust_builder/cargokit/run_build_tool.sh b/rust_builder/cargokit/run_build_tool.sh new file mode 100755 index 00000000..6e594a23 --- /dev/null +++ b/rust_builder/cargokit/run_build_tool.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -e + +BASEDIR=$(dirname "$0") + +mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" + +cd "$CARGOKIT_TOOL_TEMP_DIR" + +# Write a very simple bin package in temp folder that depends on build_tool package +# from Cargokit. This is done to ensure that we don't pollute Cargokit folder +# with .dart_tool contents. + +BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" + +if [[ -z $FLUTTER_ROOT ]]; then # not defined + DART=dart +else + DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" +fi + +cat << EOF > "pubspec.yaml" +name: build_tool_runner +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + build_tool: + path: "$BUILD_TOOL_PKG_DIR" +EOF + +mkdir -p "bin" + +cat << EOF > "bin/build_tool_runner.dart" +import 'package:build_tool/build_tool.dart' as build_tool; +void main(List args) { + build_tool.runMain(args); +} +EOF + +# Create alias for `shasum` if it does not exist and `sha1sum` exists +if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then + shopt -s expand_aliases + alias shasum="sha1sum" +fi + +# Dart run will not cache any package that has a path dependency, which +# is the case for our build_tool_runner. So instead we precompile the package +# ourselves. +# To invalidate the cached kernel we use the hash of ls -LR of the build_tool +# package directory. This should be good enough, as the build_tool package +# itself is not meant to have any path dependencies. + +if [[ "$OSTYPE" == "darwin"* ]]; then + PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) +else + PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) +fi + +PACKAGE_HASH_FILE=".package_hash" + +if [ -f "$PACKAGE_HASH_FILE" ]; then + EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") + if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then + rm "$PACKAGE_HASH_FILE" + fi +fi + +# Run pub get if needed. +if [ ! -f "$PACKAGE_HASH_FILE" ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" +fi + +set +e + +"$DART" bin/build_tool_runner.dill "$@" + +exit_code=$? + +# 253 means invalid snapshot version. +if [ $exit_code == 253 ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + "$DART" bin/build_tool_runner.dill "$@" + exit_code=$? +fi + +exit $exit_code diff --git a/rust_builder/ios/Classes/dummy_file.c b/rust_builder/ios/Classes/dummy_file.c new file mode 100644 index 00000000..e06dab99 --- /dev/null +++ b/rust_builder/ios/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/rust_builder/ios/libspaceship.podspec b/rust_builder/ios/libspaceship.podspec new file mode 100644 index 00000000..45dba00e --- /dev/null +++ b/rust_builder/ios/libspaceship.podspec @@ -0,0 +1,45 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint libspaceship.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'libspaceship' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../libspaceship libspaceship', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/liblibspaceship.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/liblibspaceship.a', + } +end \ No newline at end of file diff --git a/rust_builder/linux/CMakeLists.txt b/rust_builder/linux/CMakeLists.txt new file mode 100644 index 00000000..ea7cb4e1 --- /dev/null +++ b/rust_builder/linux/CMakeLists.txt @@ -0,0 +1,19 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "libspaceship") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../../libspaceship libspaceship "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(libspaceship_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/rust_builder/macos/Classes/dummy_file.c b/rust_builder/macos/Classes/dummy_file.c new file mode 100644 index 00000000..e06dab99 --- /dev/null +++ b/rust_builder/macos/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/rust_builder/macos/libspaceship.podspec b/rust_builder/macos/libspaceship.podspec new file mode 100644 index 00000000..27efbcbe --- /dev/null +++ b/rust_builder/macos/libspaceship.podspec @@ -0,0 +1,44 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint libspaceship.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'libspaceship' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../libspaceship libspaceship', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/liblibspaceship.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/liblibspaceship.a -framework AudioToolbox -framework AudioUnit -framework IOKit -framework CoreAudio -framework OpenAL', + } +end \ No newline at end of file diff --git a/rust_builder/pubspec.yaml b/rust_builder/pubspec.yaml new file mode 100644 index 00000000..8e3d0d76 --- /dev/null +++ b/rust_builder/pubspec.yaml @@ -0,0 +1,34 @@ +name: libspaceship +description: "Utility to build Rust code" +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + ffi: ^2.0.2 + ffigen: ^11.0.0 + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true + linux: + ffiPlugin: true + macos: + ffiPlugin: true + windows: + ffiPlugin: true diff --git a/rust_builder/windows/.gitignore b/rust_builder/windows/.gitignore new file mode 100644 index 00000000..b3eb2be1 --- /dev/null +++ b/rust_builder/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/rust_builder/windows/CMakeLists.txt b/rust_builder/windows/CMakeLists.txt new file mode 100644 index 00000000..c73fa120 --- /dev/null +++ b/rust_builder/windows/CMakeLists.txt @@ -0,0 +1,20 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "libspaceship") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../../../../../../libspaceship libspaceship "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(libspaceship_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/scripts/creator/macos/default.entitlements b/scripts/creator/macos/default.entitlements new file mode 100644 index 00000000..82d308e8 --- /dev/null +++ b/scripts/creator/macos/default.entitlements @@ -0,0 +1,22 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + + keychain-access-groups + + com.apple.security.device.camera + + com.apple.security.device.microphone + + + diff --git a/scripts/creator/macos/macos_recreate.sh b/scripts/creator/macos/macos_recreate.sh new file mode 100755 index 00000000..2305c1b9 --- /dev/null +++ b/scripts/creator/macos/macos_recreate.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Check if the entitlements file exists in the current directory +if [ ! -f ./default.entitlements ]; then + echo "Error: default.entitlements not found in the current directory." + exit 1 +fi + +# Check that we are in the correct directory by verifying pubspec.yaml exists in ../../../ +if [ ! -f ../../../pubspec.yaml ]; then + echo "Error: pubspec.yaml not found in expected location. Make sure you're running this script from the correct directory." + exit 1 +fi + +# Create a new macOS platform +echo "Creating new macOS platform..." +(flutter create ../../../. --org com.liphium --platforms macos -e) + +# Replace the entitlements files +echo "Replacing entitlement files..." +cp ./default.entitlements ../../../macos/Runner/Release.entitlements +cp ./default.entitlements ../../../macos/Runner/DebugProfile.entitlements + +echo "macOS platform has been successfully recreated with custom entitlements." \ No newline at end of file diff --git a/test/drift/main/generated/schema.dart b/test/drift/main/generated/schema.dart index 73249d7c..c42542af 100644 --- a/test/drift/main/generated/schema.dart +++ b/test/drift/main/generated/schema.dart @@ -1,10 +1,13 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; import 'schema_v1.dart' as v1; import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; +import 'schema_v4.dart' as v4; +import 'schema_v5.dart' as v5; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -14,10 +17,16 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v1.DatabaseAtV1(db); case 2: return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); + case 4: + return v4.DatabaseAtV4(db); + case 5: + return v5.DatabaseAtV5(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2]; + static const versions = const [1, 2, 3, 4, 5]; } diff --git a/test/drift/main/generated/schema_v1.dart b/test/drift/main/generated/schema_v1.dart index 8b10a49e..b4092420 100644 --- a/test/drift/main/generated/schema_v1.dart +++ b/test/drift/main/generated/schema_v1.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class Conversation extends Table @@ -10,35 +10,80 @@ class Conversation extends Table final String? _alias; Conversation(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn token = GeneratedColumn( - 'token', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn key = GeneratedColumn( - 'key', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn lastVersion = GeneratedColumn( - 'last_version', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'last_version', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); late final GeneratedColumn readAt = GeneratedColumn( - 'read_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); - @override - List get $columns => - [id, vaultId, type, data, token, key, lastVersion, updatedAt, readAt]; + 'read_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + readAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -50,24 +95,51 @@ class Conversation extends Table ConversationData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ConversationData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}type'])!, - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, - token: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}token'])!, - key: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}key'])!, - lastVersion: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}last_version'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, - readAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}read_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + token: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}token'], + )!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + lastVersion: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}last_version'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + readAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}read_at'], + )!, ); } @@ -88,16 +160,17 @@ class ConversationData extends DataClass final BigInt lastVersion; final BigInt updatedAt; final BigInt readAt; - const ConversationData( - {required this.id, - required this.vaultId, - required this.type, - required this.data, - required this.token, - required this.key, - required this.lastVersion, - required this.updatedAt, - required this.readAt}); + const ConversationData({ + required this.id, + required this.vaultId, + required this.type, + required this.data, + required this.token, + required this.key, + required this.lastVersion, + required this.updatedAt, + required this.readAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -127,8 +200,10 @@ class ConversationData extends DataClass ); } - factory ConversationData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ConversationData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return ConversationData( id: serializer.fromJson(json['id']), @@ -158,27 +233,27 @@ class ConversationData extends DataClass }; } - ConversationData copyWith( - {String? id, - String? vaultId, - int? type, - String? data, - String? token, - String? key, - BigInt? lastVersion, - BigInt? updatedAt, - BigInt? readAt}) => - ConversationData( - id: id ?? this.id, - vaultId: vaultId ?? this.vaultId, - type: type ?? this.type, - data: data ?? this.data, - token: token ?? this.token, - key: key ?? this.key, - lastVersion: lastVersion ?? this.lastVersion, - updatedAt: updatedAt ?? this.updatedAt, - readAt: readAt ?? this.readAt, - ); + ConversationData copyWith({ + String? id, + String? vaultId, + int? type, + String? data, + String? token, + String? key, + BigInt? lastVersion, + BigInt? updatedAt, + BigInt? readAt, + }) => ConversationData( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + readAt: readAt ?? this.readAt, + ); ConversationData copyWithCompanion(ConversationCompanion data) { return ConversationData( id: data.id.present ? data.id.value : this.id, @@ -212,7 +287,16 @@ class ConversationData extends DataClass @override int get hashCode => Object.hash( - id, vaultId, type, data, token, key, lastVersion, updatedAt, readAt); + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + readAt, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -262,15 +346,15 @@ class ConversationCompanion extends UpdateCompanion { required BigInt updatedAt, required BigInt readAt, this.rowid = const Value.absent(), - }) : id = Value(id), - vaultId = Value(vaultId), - type = Value(type), - data = Value(data), - token = Value(token), - key = Value(key), - lastVersion = Value(lastVersion), - updatedAt = Value(updatedAt), - readAt = Value(readAt); + }) : id = Value(id), + vaultId = Value(vaultId), + type = Value(type), + data = Value(data), + token = Value(token), + key = Value(key), + lastVersion = Value(lastVersion), + updatedAt = Value(updatedAt), + readAt = Value(readAt); static Insertable custom({ Expression? id, Expression? vaultId, @@ -297,17 +381,18 @@ class ConversationCompanion extends UpdateCompanion { }); } - ConversationCompanion copyWith( - {Value? id, - Value? vaultId, - Value? type, - Value? data, - Value? token, - Value? key, - Value? lastVersion, - Value? updatedAt, - Value? readAt, - Value? rowid}) { + ConversationCompanion copyWith({ + Value? id, + Value? vaultId, + Value? type, + Value? data, + Value? token, + Value? key, + Value? lastVersion, + Value? updatedAt, + Value? readAt, + Value? rowid, + }) { return ConversationCompanion( id: id ?? this.id, vaultId: vaultId ?? this.vaultId, @@ -382,17 +467,33 @@ class Member extends Table with TableInfo { final String? _alias; Member(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn conversationId = GeneratedColumn( - 'conversation_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + 'conversation_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn roleId = GeneratedColumn( - 'role_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'role_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); @override List get $columns => [id, conversationId, accountId, roleId]; @override @@ -406,14 +507,25 @@ class Member extends Table with TableInfo { MemberData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MemberData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - conversationId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}conversation_id']), - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}account_id'])!, - roleId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}role_id'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + conversationId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation_id'], + ), + accountId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + roleId: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role_id'], + )!, ); } @@ -428,11 +540,12 @@ class MemberData extends DataClass implements Insertable { final String? conversationId; final String accountId; final int roleId; - const MemberData( - {required this.id, - this.conversationId, - required this.accountId, - required this.roleId}); + const MemberData({ + required this.id, + this.conversationId, + required this.accountId, + required this.roleId, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -448,16 +561,19 @@ class MemberData extends DataClass implements Insertable { MemberCompanion toCompanion(bool nullToAbsent) { return MemberCompanion( id: Value(id), - conversationId: conversationId == null && nullToAbsent - ? const Value.absent() - : Value(conversationId), + conversationId: + conversationId == null && nullToAbsent + ? const Value.absent() + : Value(conversationId), accountId: Value(accountId), roleId: Value(roleId), ); } - factory MemberData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory MemberData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return MemberData( id: serializer.fromJson(json['id']), @@ -477,24 +593,25 @@ class MemberData extends DataClass implements Insertable { }; } - MemberData copyWith( - {String? id, - Value conversationId = const Value.absent(), - String? accountId, - int? roleId}) => - MemberData( - id: id ?? this.id, - conversationId: - conversationId.present ? conversationId.value : this.conversationId, - accountId: accountId ?? this.accountId, - roleId: roleId ?? this.roleId, - ); + MemberData copyWith({ + String? id, + Value conversationId = const Value.absent(), + String? accountId, + int? roleId, + }) => MemberData( + id: id ?? this.id, + conversationId: + conversationId.present ? conversationId.value : this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + ); MemberData copyWithCompanion(MemberCompanion data) { return MemberData( id: data.id.present ? data.id.value : this.id, - conversationId: data.conversationId.present - ? data.conversationId.value - : this.conversationId, + conversationId: + data.conversationId.present + ? data.conversationId.value + : this.conversationId, accountId: data.accountId.present ? data.accountId.value : this.accountId, roleId: data.roleId.present ? data.roleId.value : this.roleId, ); @@ -542,9 +659,9 @@ class MemberCompanion extends UpdateCompanion { required String accountId, required int roleId, this.rowid = const Value.absent(), - }) : id = Value(id), - accountId = Value(accountId), - roleId = Value(roleId); + }) : id = Value(id), + accountId = Value(accountId), + roleId = Value(roleId); static Insertable custom({ Expression? id, Expression? conversationId, @@ -561,12 +678,13 @@ class MemberCompanion extends UpdateCompanion { }); } - MemberCompanion copyWith( - {Value? id, - Value? conversationId, - Value? accountId, - Value? roleId, - Value? rowid}) { + MemberCompanion copyWith({ + Value? id, + Value? conversationId, + Value? accountId, + Value? roleId, + Value? rowid, + }) { return MemberCompanion( id: id ?? this.id, conversationId: conversationId ?? this.conversationId, @@ -616,11 +734,19 @@ class Setting extends Table with TableInfo { final String? _alias; Setting(this.attachedDatabase, [this._alias]); late final GeneratedColumn key = GeneratedColumn( - 'key', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn value = GeneratedColumn( - 'value', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [key, value]; @override @@ -634,10 +760,16 @@ class Setting extends Table with TableInfo { SettingData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return SettingData( - key: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}key'])!, - value: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}value'])!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, ); } @@ -660,14 +792,13 @@ class SettingData extends DataClass implements Insertable { } SettingCompanion toCompanion(bool nullToAbsent) { - return SettingCompanion( - key: Value(key), - value: Value(value), - ); + return SettingCompanion(key: Value(key), value: Value(value)); } - factory SettingData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return SettingData( key: serializer.fromJson(json['key']), @@ -683,10 +814,8 @@ class SettingData extends DataClass implements Insertable { }; } - SettingData copyWith({String? key, String? value}) => SettingData( - key: key ?? this.key, - value: value ?? this.value, - ); + SettingData copyWith({String? key, String? value}) => + SettingData(key: key ?? this.key, value: value ?? this.value); SettingData copyWithCompanion(SettingCompanion data) { return SettingData( key: data.key.present ? data.key.value : this.key, @@ -726,8 +855,8 @@ class SettingCompanion extends UpdateCompanion { required String key, required String value, this.rowid = const Value.absent(), - }) : key = Value(key), - value = Value(value); + }) : key = Value(key), + value = Value(value); static Insertable custom({ Expression? key, Expression? value, @@ -740,8 +869,11 @@ class SettingCompanion extends UpdateCompanion { }); } - SettingCompanion copyWith( - {Value? key, Value? value, Value? rowid}) { + SettingCompanion copyWith({ + Value? key, + Value? value, + Value? rowid, + }) { return SettingCompanion( key: key ?? this.key, value: value ?? this.value, @@ -781,26 +913,56 @@ class Friend extends Table with TableInfo { final String? _alias; Friend(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); - @override - List get $columns => - [id, name, displayName, vaultId, keys, updatedAt]; + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + displayName, + vaultId, + keys, + updatedAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -812,18 +974,36 @@ class Friend extends Table with TableInfo { FriendData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return FriendData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, ); } @@ -840,13 +1020,14 @@ class FriendData extends DataClass implements Insertable { final String vaultId; final String keys; final BigInt updatedAt; - const FriendData( - {required this.id, - required this.name, - required this.displayName, - required this.vaultId, - required this.keys, - required this.updatedAt}); + const FriendData({ + required this.id, + required this.name, + required this.displayName, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -870,8 +1051,10 @@ class FriendData extends DataClass implements Insertable { ); } - factory FriendData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory FriendData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return FriendData( id: serializer.fromJson(json['id']), @@ -895,21 +1078,21 @@ class FriendData extends DataClass implements Insertable { }; } - FriendData copyWith( - {String? id, - String? name, - String? displayName, - String? vaultId, - String? keys, - BigInt? updatedAt}) => - FriendData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - vaultId: vaultId ?? this.vaultId, - keys: keys ?? this.keys, - updatedAt: updatedAt ?? this.updatedAt, - ); + FriendData copyWith({ + String? id, + String? name, + String? displayName, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => FriendData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); FriendData copyWithCompanion(FriendCompanion data) { return FriendData( id: data.id.present ? data.id.value : this.id, @@ -975,12 +1158,12 @@ class FriendCompanion extends UpdateCompanion { required String keys, required BigInt updatedAt, this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - vaultId = Value(vaultId), - keys = Value(keys), - updatedAt = Value(updatedAt); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, Expression? name, @@ -1001,14 +1184,15 @@ class FriendCompanion extends UpdateCompanion { }); } - FriendCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? vaultId, - Value? keys, - Value? updatedAt, - Value? rowid}) { + FriendCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { return FriendCompanion( id: id ?? this.id, name: name ?? this.name, @@ -1068,32 +1252,67 @@ class Request extends Table with TableInfo { final String? _alias; Request(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn self = GeneratedColumn( - 'self', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("self" IN (0, 1))')); + 'self', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("self" IN (0, 1))', + ), + ); late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); - @override - List get $columns => - [id, name, displayName, self, vaultId, keys, updatedAt]; + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + displayName, + self, + vaultId, + keys, + updatedAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -1105,20 +1324,41 @@ class Request extends Table with TableInfo { RequestData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return RequestData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - self: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}self'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + self: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}self'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, ); } @@ -1136,14 +1376,15 @@ class RequestData extends DataClass implements Insertable { final String vaultId; final String keys; final BigInt updatedAt; - const RequestData( - {required this.id, - required this.name, - required this.displayName, - required this.self, - required this.vaultId, - required this.keys, - required this.updatedAt}); + const RequestData({ + required this.id, + required this.name, + required this.displayName, + required this.self, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1169,8 +1410,10 @@ class RequestData extends DataClass implements Insertable { ); } - factory RequestData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory RequestData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return RequestData( id: serializer.fromJson(json['id']), @@ -1196,23 +1439,23 @@ class RequestData extends DataClass implements Insertable { }; } - RequestData copyWith( - {String? id, - String? name, - String? displayName, - bool? self, - String? vaultId, - String? keys, - BigInt? updatedAt}) => - RequestData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - self: self ?? this.self, - vaultId: vaultId ?? this.vaultId, - keys: keys ?? this.keys, - updatedAt: updatedAt ?? this.updatedAt, - ); + RequestData copyWith({ + String? id, + String? name, + String? displayName, + bool? self, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => RequestData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); RequestData copyWithCompanion(RequestCompanion data) { return RequestData( id: data.id.present ? data.id.value : this.id, @@ -1284,13 +1527,13 @@ class RequestCompanion extends UpdateCompanion { required String keys, required BigInt updatedAt, this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - self = Value(self), - vaultId = Value(vaultId), - keys = Value(keys), - updatedAt = Value(updatedAt); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + self = Value(self), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, Expression? name, @@ -1313,15 +1556,16 @@ class RequestCompanion extends UpdateCompanion { }); } - RequestCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? self, - Value? vaultId, - Value? keys, - Value? updatedAt, - Value? rowid}) { + RequestCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? self, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { return RequestCompanion( id: id ?? this.id, name: name ?? this.name, @@ -1387,17 +1631,33 @@ class UnknownProfile extends Table final String? _alias; UnknownProfile(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [id, name, displayName, keys]; @override @@ -1411,14 +1671,26 @@ class UnknownProfile extends Table UnknownProfileData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return UnknownProfileData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, ); } @@ -1434,11 +1706,12 @@ class UnknownProfileData extends DataClass final String name; final String displayName; final String keys; - const UnknownProfileData( - {required this.id, - required this.name, - required this.displayName, - required this.keys}); + const UnknownProfileData({ + required this.id, + required this.name, + required this.displayName, + required this.keys, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1458,8 +1731,10 @@ class UnknownProfileData extends DataClass ); } - factory UnknownProfileData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory UnknownProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return UnknownProfileData( id: serializer.fromJson(json['id']), @@ -1479,14 +1754,17 @@ class UnknownProfileData extends DataClass }; } - UnknownProfileData copyWith( - {String? id, String? name, String? displayName, String? keys}) => - UnknownProfileData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - keys: keys ?? this.keys, - ); + UnknownProfileData copyWith({ + String? id, + String? name, + String? displayName, + String? keys, + }) => UnknownProfileData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + ); UnknownProfileData copyWithCompanion(UnknownProfileCompanion data) { return UnknownProfileData( id: data.id.present ? data.id.value : this.id, @@ -1539,10 +1817,10 @@ class UnknownProfileCompanion extends UpdateCompanion { required String displayName, required String keys, this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - keys = Value(keys); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + keys = Value(keys); static Insertable custom({ Expression? id, Expression? name, @@ -1559,12 +1837,13 @@ class UnknownProfileCompanion extends UpdateCompanion { }); } - UnknownProfileCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? keys, - Value? rowid}) { + UnknownProfileCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? keys, + Value? rowid, + }) { return UnknownProfileCompanion( id: id ?? this.id, name: name ?? this.name, @@ -1614,14 +1893,26 @@ class Profile extends Table with TableInfo { final String? _alias; Profile(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn pictureContainer = GeneratedColumn( - 'picture_container', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'picture_container', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [id, pictureContainer, data]; @override @@ -1635,12 +1926,21 @@ class Profile extends Table with TableInfo { ProfileData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ProfileData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - pictureContainer: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}picture_container'])!, - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + pictureContainer: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}picture_container'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, ); } @@ -1654,8 +1954,11 @@ class ProfileData extends DataClass implements Insertable { final String id; final String pictureContainer; final String data; - const ProfileData( - {required this.id, required this.pictureContainer, required this.data}); + const ProfileData({ + required this.id, + required this.pictureContainer, + required this.data, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1673,8 +1976,10 @@ class ProfileData extends DataClass implements Insertable { ); } - factory ProfileData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return ProfileData( id: serializer.fromJson(json['id']), @@ -1701,9 +2006,10 @@ class ProfileData extends DataClass implements Insertable { ProfileData copyWithCompanion(ProfileCompanion data) { return ProfileData( id: data.id.present ? data.id.value : this.id, - pictureContainer: data.pictureContainer.present - ? data.pictureContainer.value - : this.pictureContainer, + pictureContainer: + data.pictureContainer.present + ? data.pictureContainer.value + : this.pictureContainer, data: data.data.present ? data.data.value : this.data, ); } @@ -1745,9 +2051,9 @@ class ProfileCompanion extends UpdateCompanion { required String pictureContainer, required String data, this.rowid = const Value.absent(), - }) : id = Value(id), - pictureContainer = Value(pictureContainer), - data = Value(data); + }) : id = Value(id), + pictureContainer = Value(pictureContainer), + data = Value(data); static Insertable custom({ Expression? id, Expression? pictureContainer, @@ -1762,11 +2068,12 @@ class ProfileCompanion extends UpdateCompanion { }); } - ProfileCompanion copyWith( - {Value? id, - Value? pictureContainer, - Value? data, - Value? rowid}) { + ProfileCompanion copyWith({ + Value? id, + Value? pictureContainer, + Value? data, + Value? rowid, + }) { return ProfileCompanion( id: id ?? this.id, pictureContainer: pictureContainer ?? this.pictureContainer, @@ -1811,8 +2118,12 @@ class TrustedLink extends Table with TableInfo { final String? _alias; TrustedLink(this.attachedDatabase, [this._alias]); late final GeneratedColumn domain = GeneratedColumn( - 'domain', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'domain', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [domain]; @override @@ -1826,8 +2137,11 @@ class TrustedLink extends Table with TableInfo { TrustedLinkData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return TrustedLinkData( - domain: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}domain'])!, + domain: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}domain'], + )!, ); } @@ -1848,29 +2162,24 @@ class TrustedLinkData extends DataClass implements Insertable { } TrustedLinkCompanion toCompanion(bool nullToAbsent) { - return TrustedLinkCompanion( - domain: Value(domain), - ); + return TrustedLinkCompanion(domain: Value(domain)); } - factory TrustedLinkData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory TrustedLinkData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return TrustedLinkData( - domain: serializer.fromJson(json['domain']), - ); + return TrustedLinkData(domain: serializer.fromJson(json['domain'])); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'domain': serializer.toJson(domain), - }; + return {'domain': serializer.toJson(domain)}; } - TrustedLinkData copyWith({String? domain}) => TrustedLinkData( - domain: domain ?? this.domain, - ); + TrustedLinkData copyWith({String? domain}) => + TrustedLinkData(domain: domain ?? this.domain); TrustedLinkData copyWithCompanion(TrustedLinkCompanion data) { return TrustedLinkData( domain: data.domain.present ? data.domain.value : this.domain, @@ -1950,26 +2259,56 @@ class LibraryEntry extends Table final String? _alias; LibraryEntry(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn width = GeneratedColumn( - 'width', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); late final GeneratedColumn height = GeneratedColumn( - 'height', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => - [id, type, createdAt, data, width, height]; + 'height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + type, + createdAt, + data, + width, + height, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -1981,18 +2320,36 @@ class LibraryEntry extends Table LibraryEntryData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return LibraryEntryData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}type'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}created_at'])!, - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, - width: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}width'])!, - height: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}height'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + width: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + )!, + height: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + )!, ); } @@ -2010,13 +2367,14 @@ class LibraryEntryData extends DataClass final String data; final int width; final int height; - const LibraryEntryData( - {required this.id, - required this.type, - required this.createdAt, - required this.data, - required this.width, - required this.height}); + const LibraryEntryData({ + required this.id, + required this.type, + required this.createdAt, + required this.data, + required this.width, + required this.height, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2040,8 +2398,10 @@ class LibraryEntryData extends DataClass ); } - factory LibraryEntryData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory LibraryEntryData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return LibraryEntryData( id: serializer.fromJson(json['id']), @@ -2065,21 +2425,21 @@ class LibraryEntryData extends DataClass }; } - LibraryEntryData copyWith( - {String? id, - int? type, - BigInt? createdAt, - String? data, - int? width, - int? height}) => - LibraryEntryData( - id: id ?? this.id, - type: type ?? this.type, - createdAt: createdAt ?? this.createdAt, - data: data ?? this.data, - width: width ?? this.width, - height: height ?? this.height, - ); + LibraryEntryData copyWith({ + String? id, + int? type, + BigInt? createdAt, + String? data, + int? width, + int? height, + }) => LibraryEntryData( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + ); LibraryEntryData copyWithCompanion(LibraryEntryCompanion data) { return LibraryEntryData( id: data.id.present ? data.id.value : this.id, @@ -2143,12 +2503,12 @@ class LibraryEntryCompanion extends UpdateCompanion { required int width, required int height, this.rowid = const Value.absent(), - }) : id = Value(id), - type = Value(type), - createdAt = Value(createdAt), - data = Value(data), - width = Value(width), - height = Value(height); + }) : id = Value(id), + type = Value(type), + createdAt = Value(createdAt), + data = Value(data), + width = Value(width), + height = Value(height); static Insertable custom({ Expression? id, Expression? type, @@ -2169,14 +2529,15 @@ class LibraryEntryCompanion extends UpdateCompanion { }); } - LibraryEntryCompanion copyWith( - {Value? id, - Value? type, - Value? createdAt, - Value? data, - Value? width, - Value? height, - Value? rowid}) { + LibraryEntryCompanion copyWith({ + Value? id, + Value? type, + Value? createdAt, + Value? data, + Value? width, + Value? height, + Value? rowid, + }) { return LibraryEntryCompanion( id: id ?? this.id, type: type ?? this.type, @@ -2246,16 +2607,16 @@ class DatabaseAtV1 extends GeneratedDatabase { allSchemaEntities.whereType>(); @override List get allSchemaEntities => [ - conversation, - member, - setting, - friend, - request, - unknownProfile, - profile, - trustedLink, - libraryEntry - ]; + conversation, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + ]; @override int get schemaVersion => 1; } diff --git a/test/drift/main/generated/schema_v2.dart b/test/drift/main/generated/schema_v2.dart index a84a010c..96a97534 100644 --- a/test/drift/main/generated/schema_v2.dart +++ b/test/drift/main/generated/schema_v2.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class Conversation extends Table @@ -10,35 +10,80 @@ class Conversation extends Table final String? _alias; Conversation(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn token = GeneratedColumn( - 'token', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn key = GeneratedColumn( - 'key', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn lastVersion = GeneratedColumn( - 'last_version', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'last_version', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); late final GeneratedColumn readAt = GeneratedColumn( - 'read_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'read_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); @override - List get $columns => - [id, vaultId, type, data, token, key, lastVersion, updatedAt, readAt]; + List get $columns => [ + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + readAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -50,24 +95,51 @@ class Conversation extends Table ConversationData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ConversationData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}type'])!, - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, - token: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}token'])!, - key: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}key'])!, - lastVersion: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}last_version'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, - readAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}read_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + token: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}token'], + )!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + lastVersion: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}last_version'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + readAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}read_at'], + )!, ); } @@ -88,16 +160,17 @@ class ConversationData extends DataClass final BigInt lastVersion; final BigInt updatedAt; final BigInt readAt; - const ConversationData( - {required this.id, - required this.vaultId, - required this.type, - required this.data, - required this.token, - required this.key, - required this.lastVersion, - required this.updatedAt, - required this.readAt}); + const ConversationData({ + required this.id, + required this.vaultId, + required this.type, + required this.data, + required this.token, + required this.key, + required this.lastVersion, + required this.updatedAt, + required this.readAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -127,8 +200,10 @@ class ConversationData extends DataClass ); } - factory ConversationData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ConversationData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return ConversationData( id: serializer.fromJson(json['id']), @@ -158,27 +233,27 @@ class ConversationData extends DataClass }; } - ConversationData copyWith( - {String? id, - String? vaultId, - int? type, - String? data, - String? token, - String? key, - BigInt? lastVersion, - BigInt? updatedAt, - BigInt? readAt}) => - ConversationData( - id: id ?? this.id, - vaultId: vaultId ?? this.vaultId, - type: type ?? this.type, - data: data ?? this.data, - token: token ?? this.token, - key: key ?? this.key, - lastVersion: lastVersion ?? this.lastVersion, - updatedAt: updatedAt ?? this.updatedAt, - readAt: readAt ?? this.readAt, - ); + ConversationData copyWith({ + String? id, + String? vaultId, + int? type, + String? data, + String? token, + String? key, + BigInt? lastVersion, + BigInt? updatedAt, + BigInt? readAt, + }) => ConversationData( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + readAt: readAt ?? this.readAt, + ); ConversationData copyWithCompanion(ConversationCompanion data) { return ConversationData( id: data.id.present ? data.id.value : this.id, @@ -212,7 +287,16 @@ class ConversationData extends DataClass @override int get hashCode => Object.hash( - id, vaultId, type, data, token, key, lastVersion, updatedAt, readAt); + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + readAt, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -262,15 +346,15 @@ class ConversationCompanion extends UpdateCompanion { required BigInt updatedAt, required BigInt readAt, this.rowid = const Value.absent(), - }) : id = Value(id), - vaultId = Value(vaultId), - type = Value(type), - data = Value(data), - token = Value(token), - key = Value(key), - lastVersion = Value(lastVersion), - updatedAt = Value(updatedAt), - readAt = Value(readAt); + }) : id = Value(id), + vaultId = Value(vaultId), + type = Value(type), + data = Value(data), + token = Value(token), + key = Value(key), + lastVersion = Value(lastVersion), + updatedAt = Value(updatedAt), + readAt = Value(readAt); static Insertable custom({ Expression? id, Expression? vaultId, @@ -297,17 +381,18 @@ class ConversationCompanion extends UpdateCompanion { }); } - ConversationCompanion copyWith( - {Value? id, - Value? vaultId, - Value? type, - Value? data, - Value? token, - Value? key, - Value? lastVersion, - Value? updatedAt, - Value? readAt, - Value? rowid}) { + ConversationCompanion copyWith({ + Value? id, + Value? vaultId, + Value? type, + Value? data, + Value? token, + Value? key, + Value? lastVersion, + Value? updatedAt, + Value? readAt, + Value? rowid, + }) { return ConversationCompanion( id: id ?? this.id, vaultId: vaultId ?? this.vaultId, @@ -382,46 +467,78 @@ class Message extends Table with TableInfo { final String? _alias; Message(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn content = GeneratedColumn( - 'content', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'content', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn senderToken = GeneratedColumn( - 'sender_token', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'sender_token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn senderAddress = GeneratedColumn( - 'sender_address', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'sender_address', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); late final GeneratedColumn conversation = GeneratedColumn( - 'conversation', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'conversation', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn edited = GeneratedColumn( - 'edited', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("edited" IN (0, 1))')); + 'edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("edited" IN (0, 1))', + ), + ); late final GeneratedColumn verified = GeneratedColumn( - 'verified', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))')); + 'verified', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("verified" IN (0, 1))', + ), + ); @override List get $columns => [ - id, - content, - senderToken, - senderAddress, - createdAt, - conversation, - edited, - verified - ]; + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -433,22 +550,46 @@ class Message extends Table with TableInfo { MessageData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MessageData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - content: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}content'])!, - senderToken: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}sender_token'])!, - senderAddress: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}sender_address'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}created_at'])!, - conversation: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}conversation'])!, - edited: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}edited'])!, - verified: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + content: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + )!, + senderToken: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_token'], + )!, + senderAddress: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_address'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + conversation: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation'], + )!, + edited: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}edited'], + )!, + verified: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}verified'], + )!, ); } @@ -467,15 +608,16 @@ class MessageData extends DataClass implements Insertable { final String conversation; final bool edited; final bool verified; - const MessageData( - {required this.id, - required this.content, - required this.senderToken, - required this.senderAddress, - required this.createdAt, - required this.conversation, - required this.edited, - required this.verified}); + const MessageData({ + required this.id, + required this.content, + required this.senderToken, + required this.senderAddress, + required this.createdAt, + required this.conversation, + required this.edited, + required this.verified, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -503,8 +645,10 @@ class MessageData extends DataClass implements Insertable { ); } - factory MessageData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory MessageData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return MessageData( id: serializer.fromJson(json['id']), @@ -532,38 +676,40 @@ class MessageData extends DataClass implements Insertable { }; } - MessageData copyWith( - {String? id, - String? content, - String? senderToken, - String? senderAddress, - BigInt? createdAt, - String? conversation, - bool? edited, - bool? verified}) => - MessageData( - id: id ?? this.id, - content: content ?? this.content, - senderToken: senderToken ?? this.senderToken, - senderAddress: senderAddress ?? this.senderAddress, - createdAt: createdAt ?? this.createdAt, - conversation: conversation ?? this.conversation, - edited: edited ?? this.edited, - verified: verified ?? this.verified, - ); + MessageData copyWith({ + String? id, + String? content, + String? senderToken, + String? senderAddress, + BigInt? createdAt, + String? conversation, + bool? edited, + bool? verified, + }) => MessageData( + id: id ?? this.id, + content: content ?? this.content, + senderToken: senderToken ?? this.senderToken, + senderAddress: senderAddress ?? this.senderAddress, + createdAt: createdAt ?? this.createdAt, + conversation: conversation ?? this.conversation, + edited: edited ?? this.edited, + verified: verified ?? this.verified, + ); MessageData copyWithCompanion(MessageCompanion data) { return MessageData( id: data.id.present ? data.id.value : this.id, content: data.content.present ? data.content.value : this.content, senderToken: data.senderToken.present ? data.senderToken.value : this.senderToken, - senderAddress: data.senderAddress.present - ? data.senderAddress.value - : this.senderAddress, + senderAddress: + data.senderAddress.present + ? data.senderAddress.value + : this.senderAddress, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - conversation: data.conversation.present - ? data.conversation.value - : this.conversation, + conversation: + data.conversation.present + ? data.conversation.value + : this.conversation, edited: data.edited.present ? data.edited.value : this.edited, verified: data.verified.present ? data.verified.value : this.verified, ); @@ -585,8 +731,16 @@ class MessageData extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(id, content, senderToken, senderAddress, - createdAt, conversation, edited, verified); + int get hashCode => Object.hash( + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -632,14 +786,14 @@ class MessageCompanion extends UpdateCompanion { required bool edited, required bool verified, this.rowid = const Value.absent(), - }) : id = Value(id), - content = Value(content), - senderToken = Value(senderToken), - senderAddress = Value(senderAddress), - createdAt = Value(createdAt), - conversation = Value(conversation), - edited = Value(edited), - verified = Value(verified); + }) : id = Value(id), + content = Value(content), + senderToken = Value(senderToken), + senderAddress = Value(senderAddress), + createdAt = Value(createdAt), + conversation = Value(conversation), + edited = Value(edited), + verified = Value(verified); static Insertable custom({ Expression? id, Expression? content, @@ -664,16 +818,17 @@ class MessageCompanion extends UpdateCompanion { }); } - MessageCompanion copyWith( - {Value? id, - Value? content, - Value? senderToken, - Value? senderAddress, - Value? createdAt, - Value? conversation, - Value? edited, - Value? verified, - Value? rowid}) { + MessageCompanion copyWith({ + Value? id, + Value? content, + Value? senderToken, + Value? senderAddress, + Value? createdAt, + Value? conversation, + Value? edited, + Value? verified, + Value? rowid, + }) { return MessageCompanion( id: id ?? this.id, content: content ?? this.content, @@ -743,17 +898,33 @@ class Member extends Table with TableInfo { final String? _alias; Member(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn conversationId = GeneratedColumn( - 'conversation_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + 'conversation_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn roleId = GeneratedColumn( - 'role_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'role_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); @override List get $columns => [id, conversationId, accountId, roleId]; @override @@ -767,14 +938,25 @@ class Member extends Table with TableInfo { MemberData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MemberData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - conversationId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}conversation_id']), - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}account_id'])!, - roleId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}role_id'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + conversationId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation_id'], + ), + accountId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + roleId: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role_id'], + )!, ); } @@ -789,11 +971,12 @@ class MemberData extends DataClass implements Insertable { final String? conversationId; final String accountId; final int roleId; - const MemberData( - {required this.id, - this.conversationId, - required this.accountId, - required this.roleId}); + const MemberData({ + required this.id, + this.conversationId, + required this.accountId, + required this.roleId, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -809,16 +992,19 @@ class MemberData extends DataClass implements Insertable { MemberCompanion toCompanion(bool nullToAbsent) { return MemberCompanion( id: Value(id), - conversationId: conversationId == null && nullToAbsent - ? const Value.absent() - : Value(conversationId), + conversationId: + conversationId == null && nullToAbsent + ? const Value.absent() + : Value(conversationId), accountId: Value(accountId), roleId: Value(roleId), ); } - factory MemberData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory MemberData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return MemberData( id: serializer.fromJson(json['id']), @@ -838,24 +1024,25 @@ class MemberData extends DataClass implements Insertable { }; } - MemberData copyWith( - {String? id, - Value conversationId = const Value.absent(), - String? accountId, - int? roleId}) => - MemberData( - id: id ?? this.id, - conversationId: - conversationId.present ? conversationId.value : this.conversationId, - accountId: accountId ?? this.accountId, - roleId: roleId ?? this.roleId, - ); + MemberData copyWith({ + String? id, + Value conversationId = const Value.absent(), + String? accountId, + int? roleId, + }) => MemberData( + id: id ?? this.id, + conversationId: + conversationId.present ? conversationId.value : this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + ); MemberData copyWithCompanion(MemberCompanion data) { return MemberData( id: data.id.present ? data.id.value : this.id, - conversationId: data.conversationId.present - ? data.conversationId.value - : this.conversationId, + conversationId: + data.conversationId.present + ? data.conversationId.value + : this.conversationId, accountId: data.accountId.present ? data.accountId.value : this.accountId, roleId: data.roleId.present ? data.roleId.value : this.roleId, ); @@ -903,9 +1090,9 @@ class MemberCompanion extends UpdateCompanion { required String accountId, required int roleId, this.rowid = const Value.absent(), - }) : id = Value(id), - accountId = Value(accountId), - roleId = Value(roleId); + }) : id = Value(id), + accountId = Value(accountId), + roleId = Value(roleId); static Insertable custom({ Expression? id, Expression? conversationId, @@ -922,12 +1109,13 @@ class MemberCompanion extends UpdateCompanion { }); } - MemberCompanion copyWith( - {Value? id, - Value? conversationId, - Value? accountId, - Value? roleId, - Value? rowid}) { + MemberCompanion copyWith({ + Value? id, + Value? conversationId, + Value? accountId, + Value? roleId, + Value? rowid, + }) { return MemberCompanion( id: id ?? this.id, conversationId: conversationId ?? this.conversationId, @@ -977,11 +1165,19 @@ class Setting extends Table with TableInfo { final String? _alias; Setting(this.attachedDatabase, [this._alias]); late final GeneratedColumn key = GeneratedColumn( - 'key', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn value = GeneratedColumn( - 'value', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [key, value]; @override @@ -995,10 +1191,16 @@ class Setting extends Table with TableInfo { SettingData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return SettingData( - key: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}key'])!, - value: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}value'])!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, ); } @@ -1021,14 +1223,13 @@ class SettingData extends DataClass implements Insertable { } SettingCompanion toCompanion(bool nullToAbsent) { - return SettingCompanion( - key: Value(key), - value: Value(value), - ); + return SettingCompanion(key: Value(key), value: Value(value)); } - factory SettingData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return SettingData( key: serializer.fromJson(json['key']), @@ -1044,10 +1245,8 @@ class SettingData extends DataClass implements Insertable { }; } - SettingData copyWith({String? key, String? value}) => SettingData( - key: key ?? this.key, - value: value ?? this.value, - ); + SettingData copyWith({String? key, String? value}) => + SettingData(key: key ?? this.key, value: value ?? this.value); SettingData copyWithCompanion(SettingCompanion data) { return SettingData( key: data.key.present ? data.key.value : this.key, @@ -1087,8 +1286,8 @@ class SettingCompanion extends UpdateCompanion { required String key, required String value, this.rowid = const Value.absent(), - }) : key = Value(key), - value = Value(value); + }) : key = Value(key), + value = Value(value); static Insertable custom({ Expression? key, Expression? value, @@ -1101,8 +1300,11 @@ class SettingCompanion extends UpdateCompanion { }); } - SettingCompanion copyWith( - {Value? key, Value? value, Value? rowid}) { + SettingCompanion copyWith({ + Value? key, + Value? value, + Value? rowid, + }) { return SettingCompanion( key: key ?? this.key, value: value ?? this.value, @@ -1142,26 +1344,56 @@ class Friend extends Table with TableInfo { final String? _alias; Friend(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); @override - List get $columns => - [id, name, displayName, vaultId, keys, updatedAt]; + List get $columns => [ + id, + name, + displayName, + vaultId, + keys, + updatedAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -1173,18 +1405,36 @@ class Friend extends Table with TableInfo { FriendData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return FriendData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, ); } @@ -1201,13 +1451,14 @@ class FriendData extends DataClass implements Insertable { final String vaultId; final String keys; final BigInt updatedAt; - const FriendData( - {required this.id, - required this.name, - required this.displayName, - required this.vaultId, - required this.keys, - required this.updatedAt}); + const FriendData({ + required this.id, + required this.name, + required this.displayName, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1231,8 +1482,10 @@ class FriendData extends DataClass implements Insertable { ); } - factory FriendData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory FriendData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return FriendData( id: serializer.fromJson(json['id']), @@ -1256,21 +1509,21 @@ class FriendData extends DataClass implements Insertable { }; } - FriendData copyWith( - {String? id, - String? name, - String? displayName, - String? vaultId, - String? keys, - BigInt? updatedAt}) => - FriendData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - vaultId: vaultId ?? this.vaultId, - keys: keys ?? this.keys, - updatedAt: updatedAt ?? this.updatedAt, - ); + FriendData copyWith({ + String? id, + String? name, + String? displayName, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => FriendData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); FriendData copyWithCompanion(FriendCompanion data) { return FriendData( id: data.id.present ? data.id.value : this.id, @@ -1336,12 +1589,12 @@ class FriendCompanion extends UpdateCompanion { required String keys, required BigInt updatedAt, this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - vaultId = Value(vaultId), - keys = Value(keys), - updatedAt = Value(updatedAt); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, Expression? name, @@ -1362,14 +1615,15 @@ class FriendCompanion extends UpdateCompanion { }); } - FriendCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? vaultId, - Value? keys, - Value? updatedAt, - Value? rowid}) { + FriendCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { return FriendCompanion( id: id ?? this.id, name: name ?? this.name, @@ -1429,32 +1683,67 @@ class Request extends Table with TableInfo { final String? _alias; Request(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn self = GeneratedColumn( - 'self', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("self" IN (0, 1))')); + 'self', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("self" IN (0, 1))', + ), + ); late final GeneratedColumn vaultId = GeneratedColumn( - 'vault_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); @override - List get $columns => - [id, name, displayName, self, vaultId, keys, updatedAt]; + List get $columns => [ + id, + name, + displayName, + self, + vaultId, + keys, + updatedAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -1466,20 +1755,41 @@ class Request extends Table with TableInfo { RequestData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return RequestData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - self: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}self'])!, - vaultId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}vault_id'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}updated_at'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + self: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}self'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, ); } @@ -1497,14 +1807,15 @@ class RequestData extends DataClass implements Insertable { final String vaultId; final String keys; final BigInt updatedAt; - const RequestData( - {required this.id, - required this.name, - required this.displayName, - required this.self, - required this.vaultId, - required this.keys, - required this.updatedAt}); + const RequestData({ + required this.id, + required this.name, + required this.displayName, + required this.self, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1530,8 +1841,10 @@ class RequestData extends DataClass implements Insertable { ); } - factory RequestData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory RequestData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return RequestData( id: serializer.fromJson(json['id']), @@ -1557,23 +1870,23 @@ class RequestData extends DataClass implements Insertable { }; } - RequestData copyWith( - {String? id, - String? name, - String? displayName, - bool? self, - String? vaultId, - String? keys, - BigInt? updatedAt}) => - RequestData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - self: self ?? this.self, - vaultId: vaultId ?? this.vaultId, - keys: keys ?? this.keys, - updatedAt: updatedAt ?? this.updatedAt, - ); + RequestData copyWith({ + String? id, + String? name, + String? displayName, + bool? self, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => RequestData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); RequestData copyWithCompanion(RequestCompanion data) { return RequestData( id: data.id.present ? data.id.value : this.id, @@ -1645,13 +1958,13 @@ class RequestCompanion extends UpdateCompanion { required String keys, required BigInt updatedAt, this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - self = Value(self), - vaultId = Value(vaultId), - keys = Value(keys), - updatedAt = Value(updatedAt); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + self = Value(self), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, Expression? name, @@ -1674,15 +1987,16 @@ class RequestCompanion extends UpdateCompanion { }); } - RequestCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? self, - Value? vaultId, - Value? keys, - Value? updatedAt, - Value? rowid}) { + RequestCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? self, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { return RequestCompanion( id: id ?? this.id, name: name ?? this.name, @@ -1748,17 +2062,33 @@ class UnknownProfile extends Table final String? _alias; UnknownProfile(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn keys = GeneratedColumn( - 'keys', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [id, name, displayName, keys]; @override @@ -1772,14 +2102,26 @@ class UnknownProfile extends Table UnknownProfileData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return UnknownProfileData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name'])!, - keys: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}keys'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, ); } @@ -1795,11 +2137,12 @@ class UnknownProfileData extends DataClass final String name; final String displayName; final String keys; - const UnknownProfileData( - {required this.id, - required this.name, - required this.displayName, - required this.keys}); + const UnknownProfileData({ + required this.id, + required this.name, + required this.displayName, + required this.keys, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1819,8 +2162,10 @@ class UnknownProfileData extends DataClass ); } - factory UnknownProfileData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory UnknownProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return UnknownProfileData( id: serializer.fromJson(json['id']), @@ -1840,14 +2185,17 @@ class UnknownProfileData extends DataClass }; } - UnknownProfileData copyWith( - {String? id, String? name, String? displayName, String? keys}) => - UnknownProfileData( - id: id ?? this.id, - name: name ?? this.name, - displayName: displayName ?? this.displayName, - keys: keys ?? this.keys, - ); + UnknownProfileData copyWith({ + String? id, + String? name, + String? displayName, + String? keys, + }) => UnknownProfileData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + ); UnknownProfileData copyWithCompanion(UnknownProfileCompanion data) { return UnknownProfileData( id: data.id.present ? data.id.value : this.id, @@ -1900,10 +2248,10 @@ class UnknownProfileCompanion extends UpdateCompanion { required String displayName, required String keys, this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - displayName = Value(displayName), - keys = Value(keys); + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + keys = Value(keys); static Insertable custom({ Expression? id, Expression? name, @@ -1920,12 +2268,13 @@ class UnknownProfileCompanion extends UpdateCompanion { }); } - UnknownProfileCompanion copyWith( - {Value? id, - Value? name, - Value? displayName, - Value? keys, - Value? rowid}) { + UnknownProfileCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? keys, + Value? rowid, + }) { return UnknownProfileCompanion( id: id ?? this.id, name: name ?? this.name, @@ -1975,14 +2324,26 @@ class Profile extends Table with TableInfo { final String? _alias; Profile(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn pictureContainer = GeneratedColumn( - 'picture_container', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'picture_container', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [id, pictureContainer, data]; @override @@ -1996,12 +2357,21 @@ class Profile extends Table with TableInfo { ProfileData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ProfileData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - pictureContainer: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}picture_container'])!, - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + pictureContainer: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}picture_container'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, ); } @@ -2015,8 +2385,11 @@ class ProfileData extends DataClass implements Insertable { final String id; final String pictureContainer; final String data; - const ProfileData( - {required this.id, required this.pictureContainer, required this.data}); + const ProfileData({ + required this.id, + required this.pictureContainer, + required this.data, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2034,8 +2407,10 @@ class ProfileData extends DataClass implements Insertable { ); } - factory ProfileData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return ProfileData( id: serializer.fromJson(json['id']), @@ -2062,9 +2437,10 @@ class ProfileData extends DataClass implements Insertable { ProfileData copyWithCompanion(ProfileCompanion data) { return ProfileData( id: data.id.present ? data.id.value : this.id, - pictureContainer: data.pictureContainer.present - ? data.pictureContainer.value - : this.pictureContainer, + pictureContainer: + data.pictureContainer.present + ? data.pictureContainer.value + : this.pictureContainer, data: data.data.present ? data.data.value : this.data, ); } @@ -2106,9 +2482,9 @@ class ProfileCompanion extends UpdateCompanion { required String pictureContainer, required String data, this.rowid = const Value.absent(), - }) : id = Value(id), - pictureContainer = Value(pictureContainer), - data = Value(data); + }) : id = Value(id), + pictureContainer = Value(pictureContainer), + data = Value(data); static Insertable custom({ Expression? id, Expression? pictureContainer, @@ -2123,11 +2499,12 @@ class ProfileCompanion extends UpdateCompanion { }); } - ProfileCompanion copyWith( - {Value? id, - Value? pictureContainer, - Value? data, - Value? rowid}) { + ProfileCompanion copyWith({ + Value? id, + Value? pictureContainer, + Value? data, + Value? rowid, + }) { return ProfileCompanion( id: id ?? this.id, pictureContainer: pictureContainer ?? this.pictureContainer, @@ -2172,8 +2549,12 @@ class TrustedLink extends Table with TableInfo { final String? _alias; TrustedLink(this.attachedDatabase, [this._alias]); late final GeneratedColumn domain = GeneratedColumn( - 'domain', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'domain', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [domain]; @override @@ -2187,8 +2568,11 @@ class TrustedLink extends Table with TableInfo { TrustedLinkData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return TrustedLinkData( - domain: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}domain'])!, + domain: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}domain'], + )!, ); } @@ -2209,29 +2593,24 @@ class TrustedLinkData extends DataClass implements Insertable { } TrustedLinkCompanion toCompanion(bool nullToAbsent) { - return TrustedLinkCompanion( - domain: Value(domain), - ); + return TrustedLinkCompanion(domain: Value(domain)); } - factory TrustedLinkData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory TrustedLinkData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return TrustedLinkData( - domain: serializer.fromJson(json['domain']), - ); + return TrustedLinkData(domain: serializer.fromJson(json['domain'])); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'domain': serializer.toJson(domain), - }; + return {'domain': serializer.toJson(domain)}; } - TrustedLinkData copyWith({String? domain}) => TrustedLinkData( - domain: domain ?? this.domain, - ); + TrustedLinkData copyWith({String? domain}) => + TrustedLinkData(domain: domain ?? this.domain); TrustedLinkData copyWithCompanion(TrustedLinkCompanion data) { return TrustedLinkData( domain: data.domain.present ? data.domain.value : this.domain, @@ -2311,26 +2690,56 @@ class LibraryEntry extends Table final String? _alias; LibraryEntry(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.bigInt, requiredDuringInsert: true); + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); late final GeneratedColumn data = GeneratedColumn( - 'data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); late final GeneratedColumn width = GeneratedColumn( - 'width', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); late final GeneratedColumn height = GeneratedColumn( - 'height', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); @override - List get $columns => - [id, type, createdAt, data, width, height]; + List get $columns => [ + id, + type, + createdAt, + data, + width, + height, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2342,18 +2751,36 @@ class LibraryEntry extends Table LibraryEntryData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return LibraryEntryData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}type'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.bigInt, data['${effectivePrefix}created_at'])!, - data: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}data'])!, - width: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}width'])!, - height: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}height'])!, + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + width: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + )!, + height: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + )!, ); } @@ -2371,13 +2798,14 @@ class LibraryEntryData extends DataClass final String data; final int width; final int height; - const LibraryEntryData( - {required this.id, - required this.type, - required this.createdAt, - required this.data, - required this.width, - required this.height}); + const LibraryEntryData({ + required this.id, + required this.type, + required this.createdAt, + required this.data, + required this.width, + required this.height, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2401,8 +2829,10 @@ class LibraryEntryData extends DataClass ); } - factory LibraryEntryData.fromJson(Map json, - {ValueSerializer? serializer}) { + factory LibraryEntryData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return LibraryEntryData( id: serializer.fromJson(json['id']), @@ -2426,21 +2856,21 @@ class LibraryEntryData extends DataClass }; } - LibraryEntryData copyWith( - {String? id, - int? type, - BigInt? createdAt, - String? data, - int? width, - int? height}) => - LibraryEntryData( - id: id ?? this.id, - type: type ?? this.type, - createdAt: createdAt ?? this.createdAt, - data: data ?? this.data, - width: width ?? this.width, - height: height ?? this.height, - ); + LibraryEntryData copyWith({ + String? id, + int? type, + BigInt? createdAt, + String? data, + int? width, + int? height, + }) => LibraryEntryData( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + ); LibraryEntryData copyWithCompanion(LibraryEntryCompanion data) { return LibraryEntryData( id: data.id.present ? data.id.value : this.id, @@ -2504,12 +2934,12 @@ class LibraryEntryCompanion extends UpdateCompanion { required int width, required int height, this.rowid = const Value.absent(), - }) : id = Value(id), - type = Value(type), - createdAt = Value(createdAt), - data = Value(data), - width = Value(width), - height = Value(height); + }) : id = Value(id), + type = Value(type), + createdAt = Value(createdAt), + data = Value(data), + width = Value(width), + height = Value(height); static Insertable custom({ Expression? id, Expression? type, @@ -2530,14 +2960,15 @@ class LibraryEntryCompanion extends UpdateCompanion { }); } - LibraryEntryCompanion copyWith( - {Value? id, - Value? type, - Value? createdAt, - Value? data, - Value? width, - Value? height, - Value? rowid}) { + LibraryEntryCompanion copyWith({ + Value? id, + Value? type, + Value? createdAt, + Value? data, + Value? width, + Value? height, + Value? rowid, + }) { return LibraryEntryCompanion( id: id ?? this.id, type: type ?? this.type, @@ -2608,17 +3039,17 @@ class DatabaseAtV2 extends GeneratedDatabase { allSchemaEntities.whereType>(); @override List get allSchemaEntities => [ - conversation, - message, - member, - setting, - friend, - request, - unknownProfile, - profile, - trustedLink, - libraryEntry - ]; + conversation, + message, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + ]; @override int get schemaVersion => 2; } diff --git a/test/drift/main/generated/schema_v3.dart b/test/drift/main/generated/schema_v3.dart new file mode 100644 index 00000000..0b358753 --- /dev/null +++ b/test/drift/main/generated/schema_v3.dart @@ -0,0 +1,3075 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class Conversation extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Conversation(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn token = GeneratedColumn( + 'token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn lastVersion = GeneratedColumn( + 'last_version', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn readAt = GeneratedColumn( + 'read_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + readAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'conversation'; + @override + Set get $primaryKey => {id}; + @override + ConversationData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ConversationData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + token: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}token'], + )!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + lastVersion: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}last_version'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + readAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}read_at'], + )!, + ); + } + + @override + Conversation createAlias(String alias) { + return Conversation(attachedDatabase, alias); + } +} + +class ConversationData extends DataClass + implements Insertable { + final String id; + final String vaultId; + final int type; + final String data; + final String token; + final String key; + final BigInt lastVersion; + final BigInt updatedAt; + final BigInt readAt; + const ConversationData({ + required this.id, + required this.vaultId, + required this.type, + required this.data, + required this.token, + required this.key, + required this.lastVersion, + required this.updatedAt, + required this.readAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['vault_id'] = Variable(vaultId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['token'] = Variable(token); + map['key'] = Variable(key); + map['last_version'] = Variable(lastVersion); + map['updated_at'] = Variable(updatedAt); + map['read_at'] = Variable(readAt); + return map; + } + + ConversationCompanion toCompanion(bool nullToAbsent) { + return ConversationCompanion( + id: Value(id), + vaultId: Value(vaultId), + type: Value(type), + data: Value(data), + token: Value(token), + key: Value(key), + lastVersion: Value(lastVersion), + updatedAt: Value(updatedAt), + readAt: Value(readAt), + ); + } + + factory ConversationData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ConversationData( + id: serializer.fromJson(json['id']), + vaultId: serializer.fromJson(json['vaultId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + token: serializer.fromJson(json['token']), + key: serializer.fromJson(json['key']), + lastVersion: serializer.fromJson(json['lastVersion']), + updatedAt: serializer.fromJson(json['updatedAt']), + readAt: serializer.fromJson(json['readAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'vaultId': serializer.toJson(vaultId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'token': serializer.toJson(token), + 'key': serializer.toJson(key), + 'lastVersion': serializer.toJson(lastVersion), + 'updatedAt': serializer.toJson(updatedAt), + 'readAt': serializer.toJson(readAt), + }; + } + + ConversationData copyWith({ + String? id, + String? vaultId, + int? type, + String? data, + String? token, + String? key, + BigInt? lastVersion, + BigInt? updatedAt, + BigInt? readAt, + }) => ConversationData( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + readAt: readAt ?? this.readAt, + ); + ConversationData copyWithCompanion(ConversationCompanion data) { + return ConversationData( + id: data.id.present ? data.id.value : this.id, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + token: data.token.present ? data.token.value : this.token, + key: data.key.present ? data.key.value : this.key, + lastVersion: + data.lastVersion.present ? data.lastVersion.value : this.lastVersion, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + readAt: data.readAt.present ? data.readAt.value : this.readAt, + ); + } + + @override + String toString() { + return (StringBuffer('ConversationData(') + ..write('id: $id, ') + ..write('vaultId: $vaultId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('token: $token, ') + ..write('key: $key, ') + ..write('lastVersion: $lastVersion, ') + ..write('updatedAt: $updatedAt, ') + ..write('readAt: $readAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + readAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ConversationData && + other.id == this.id && + other.vaultId == this.vaultId && + other.type == this.type && + other.data == this.data && + other.token == this.token && + other.key == this.key && + other.lastVersion == this.lastVersion && + other.updatedAt == this.updatedAt && + other.readAt == this.readAt); +} + +class ConversationCompanion extends UpdateCompanion { + final Value id; + final Value vaultId; + final Value type; + final Value data; + final Value token; + final Value key; + final Value lastVersion; + final Value updatedAt; + final Value readAt; + final Value rowid; + const ConversationCompanion({ + this.id = const Value.absent(), + this.vaultId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.token = const Value.absent(), + this.key = const Value.absent(), + this.lastVersion = const Value.absent(), + this.updatedAt = const Value.absent(), + this.readAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ConversationCompanion.insert({ + required String id, + required String vaultId, + required int type, + required String data, + required String token, + required String key, + required BigInt lastVersion, + required BigInt updatedAt, + required BigInt readAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + vaultId = Value(vaultId), + type = Value(type), + data = Value(data), + token = Value(token), + key = Value(key), + lastVersion = Value(lastVersion), + updatedAt = Value(updatedAt), + readAt = Value(readAt); + static Insertable custom({ + Expression? id, + Expression? vaultId, + Expression? type, + Expression? data, + Expression? token, + Expression? key, + Expression? lastVersion, + Expression? updatedAt, + Expression? readAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (vaultId != null) 'vault_id': vaultId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (token != null) 'token': token, + if (key != null) 'key': key, + if (lastVersion != null) 'last_version': lastVersion, + if (updatedAt != null) 'updated_at': updatedAt, + if (readAt != null) 'read_at': readAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ConversationCompanion copyWith({ + Value? id, + Value? vaultId, + Value? type, + Value? data, + Value? token, + Value? key, + Value? lastVersion, + Value? updatedAt, + Value? readAt, + Value? rowid, + }) { + return ConversationCompanion( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + readAt: readAt ?? this.readAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (token.present) { + map['token'] = Variable(token.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (lastVersion.present) { + map['last_version'] = Variable(lastVersion.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (readAt.present) { + map['read_at'] = Variable(readAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ConversationCompanion(') + ..write('id: $id, ') + ..write('vaultId: $vaultId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('token: $token, ') + ..write('key: $key, ') + ..write('lastVersion: $lastVersion, ') + ..write('updatedAt: $updatedAt, ') + ..write('readAt: $readAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Message extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Message(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn content = GeneratedColumn( + 'content', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn senderToken = GeneratedColumn( + 'sender_token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn senderAddress = GeneratedColumn( + 'sender_address', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn conversation = GeneratedColumn( + 'conversation', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn edited = GeneratedColumn( + 'edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("edited" IN (0, 1))', + ), + ); + late final GeneratedColumn verified = GeneratedColumn( + 'verified', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("verified" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message'; + @override + Set get $primaryKey => {id}; + @override + MessageData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + content: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + )!, + senderToken: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_token'], + )!, + senderAddress: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_address'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + conversation: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation'], + )!, + edited: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}edited'], + )!, + verified: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}verified'], + )!, + ); + } + + @override + Message createAlias(String alias) { + return Message(attachedDatabase, alias); + } +} + +class MessageData extends DataClass implements Insertable { + final String id; + final String content; + final String senderToken; + final String senderAddress; + final BigInt createdAt; + final String conversation; + final bool edited; + final bool verified; + const MessageData({ + required this.id, + required this.content, + required this.senderToken, + required this.senderAddress, + required this.createdAt, + required this.conversation, + required this.edited, + required this.verified, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['content'] = Variable(content); + map['sender_token'] = Variable(senderToken); + map['sender_address'] = Variable(senderAddress); + map['created_at'] = Variable(createdAt); + map['conversation'] = Variable(conversation); + map['edited'] = Variable(edited); + map['verified'] = Variable(verified); + return map; + } + + MessageCompanion toCompanion(bool nullToAbsent) { + return MessageCompanion( + id: Value(id), + content: Value(content), + senderToken: Value(senderToken), + senderAddress: Value(senderAddress), + createdAt: Value(createdAt), + conversation: Value(conversation), + edited: Value(edited), + verified: Value(verified), + ); + } + + factory MessageData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageData( + id: serializer.fromJson(json['id']), + content: serializer.fromJson(json['content']), + senderToken: serializer.fromJson(json['senderToken']), + senderAddress: serializer.fromJson(json['senderAddress']), + createdAt: serializer.fromJson(json['createdAt']), + conversation: serializer.fromJson(json['conversation']), + edited: serializer.fromJson(json['edited']), + verified: serializer.fromJson(json['verified']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'content': serializer.toJson(content), + 'senderToken': serializer.toJson(senderToken), + 'senderAddress': serializer.toJson(senderAddress), + 'createdAt': serializer.toJson(createdAt), + 'conversation': serializer.toJson(conversation), + 'edited': serializer.toJson(edited), + 'verified': serializer.toJson(verified), + }; + } + + MessageData copyWith({ + String? id, + String? content, + String? senderToken, + String? senderAddress, + BigInt? createdAt, + String? conversation, + bool? edited, + bool? verified, + }) => MessageData( + id: id ?? this.id, + content: content ?? this.content, + senderToken: senderToken ?? this.senderToken, + senderAddress: senderAddress ?? this.senderAddress, + createdAt: createdAt ?? this.createdAt, + conversation: conversation ?? this.conversation, + edited: edited ?? this.edited, + verified: verified ?? this.verified, + ); + MessageData copyWithCompanion(MessageCompanion data) { + return MessageData( + id: data.id.present ? data.id.value : this.id, + content: data.content.present ? data.content.value : this.content, + senderToken: + data.senderToken.present ? data.senderToken.value : this.senderToken, + senderAddress: + data.senderAddress.present + ? data.senderAddress.value + : this.senderAddress, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + conversation: + data.conversation.present + ? data.conversation.value + : this.conversation, + edited: data.edited.present ? data.edited.value : this.edited, + verified: data.verified.present ? data.verified.value : this.verified, + ); + } + + @override + String toString() { + return (StringBuffer('MessageData(') + ..write('id: $id, ') + ..write('content: $content, ') + ..write('senderToken: $senderToken, ') + ..write('senderAddress: $senderAddress, ') + ..write('createdAt: $createdAt, ') + ..write('conversation: $conversation, ') + ..write('edited: $edited, ') + ..write('verified: $verified') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageData && + other.id == this.id && + other.content == this.content && + other.senderToken == this.senderToken && + other.senderAddress == this.senderAddress && + other.createdAt == this.createdAt && + other.conversation == this.conversation && + other.edited == this.edited && + other.verified == this.verified); +} + +class MessageCompanion extends UpdateCompanion { + final Value id; + final Value content; + final Value senderToken; + final Value senderAddress; + final Value createdAt; + final Value conversation; + final Value edited; + final Value verified; + final Value rowid; + const MessageCompanion({ + this.id = const Value.absent(), + this.content = const Value.absent(), + this.senderToken = const Value.absent(), + this.senderAddress = const Value.absent(), + this.createdAt = const Value.absent(), + this.conversation = const Value.absent(), + this.edited = const Value.absent(), + this.verified = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessageCompanion.insert({ + required String id, + required String content, + required String senderToken, + required String senderAddress, + required BigInt createdAt, + required String conversation, + required bool edited, + required bool verified, + this.rowid = const Value.absent(), + }) : id = Value(id), + content = Value(content), + senderToken = Value(senderToken), + senderAddress = Value(senderAddress), + createdAt = Value(createdAt), + conversation = Value(conversation), + edited = Value(edited), + verified = Value(verified); + static Insertable custom({ + Expression? id, + Expression? content, + Expression? senderToken, + Expression? senderAddress, + Expression? createdAt, + Expression? conversation, + Expression? edited, + Expression? verified, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (content != null) 'content': content, + if (senderToken != null) 'sender_token': senderToken, + if (senderAddress != null) 'sender_address': senderAddress, + if (createdAt != null) 'created_at': createdAt, + if (conversation != null) 'conversation': conversation, + if (edited != null) 'edited': edited, + if (verified != null) 'verified': verified, + if (rowid != null) 'rowid': rowid, + }); + } + + MessageCompanion copyWith({ + Value? id, + Value? content, + Value? senderToken, + Value? senderAddress, + Value? createdAt, + Value? conversation, + Value? edited, + Value? verified, + Value? rowid, + }) { + return MessageCompanion( + id: id ?? this.id, + content: content ?? this.content, + senderToken: senderToken ?? this.senderToken, + senderAddress: senderAddress ?? this.senderAddress, + createdAt: createdAt ?? this.createdAt, + conversation: conversation ?? this.conversation, + edited: edited ?? this.edited, + verified: verified ?? this.verified, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (senderToken.present) { + map['sender_token'] = Variable(senderToken.value); + } + if (senderAddress.present) { + map['sender_address'] = Variable(senderAddress.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (conversation.present) { + map['conversation'] = Variable(conversation.value); + } + if (edited.present) { + map['edited'] = Variable(edited.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageCompanion(') + ..write('id: $id, ') + ..write('content: $content, ') + ..write('senderToken: $senderToken, ') + ..write('senderAddress: $senderAddress, ') + ..write('createdAt: $createdAt, ') + ..write('conversation: $conversation, ') + ..write('edited: $edited, ') + ..write('verified: $verified, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Member extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Member(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn conversationId = GeneratedColumn( + 'conversation_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn roleId = GeneratedColumn( + 'role_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, conversationId, accountId, roleId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'member'; + @override + Set get $primaryKey => {id}; + @override + MemberData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemberData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + conversationId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation_id'], + ), + accountId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + roleId: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role_id'], + )!, + ); + } + + @override + Member createAlias(String alias) { + return Member(attachedDatabase, alias); + } +} + +class MemberData extends DataClass implements Insertable { + final String id; + final String? conversationId; + final String accountId; + final int roleId; + const MemberData({ + required this.id, + this.conversationId, + required this.accountId, + required this.roleId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || conversationId != null) { + map['conversation_id'] = Variable(conversationId); + } + map['account_id'] = Variable(accountId); + map['role_id'] = Variable(roleId); + return map; + } + + MemberCompanion toCompanion(bool nullToAbsent) { + return MemberCompanion( + id: Value(id), + conversationId: + conversationId == null && nullToAbsent + ? const Value.absent() + : Value(conversationId), + accountId: Value(accountId), + roleId: Value(roleId), + ); + } + + factory MemberData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemberData( + id: serializer.fromJson(json['id']), + conversationId: serializer.fromJson(json['conversationId']), + accountId: serializer.fromJson(json['accountId']), + roleId: serializer.fromJson(json['roleId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'conversationId': serializer.toJson(conversationId), + 'accountId': serializer.toJson(accountId), + 'roleId': serializer.toJson(roleId), + }; + } + + MemberData copyWith({ + String? id, + Value conversationId = const Value.absent(), + String? accountId, + int? roleId, + }) => MemberData( + id: id ?? this.id, + conversationId: + conversationId.present ? conversationId.value : this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + ); + MemberData copyWithCompanion(MemberCompanion data) { + return MemberData( + id: data.id.present ? data.id.value : this.id, + conversationId: + data.conversationId.present + ? data.conversationId.value + : this.conversationId, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + roleId: data.roleId.present ? data.roleId.value : this.roleId, + ); + } + + @override + String toString() { + return (StringBuffer('MemberData(') + ..write('id: $id, ') + ..write('conversationId: $conversationId, ') + ..write('accountId: $accountId, ') + ..write('roleId: $roleId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, conversationId, accountId, roleId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemberData && + other.id == this.id && + other.conversationId == this.conversationId && + other.accountId == this.accountId && + other.roleId == this.roleId); +} + +class MemberCompanion extends UpdateCompanion { + final Value id; + final Value conversationId; + final Value accountId; + final Value roleId; + final Value rowid; + const MemberCompanion({ + this.id = const Value.absent(), + this.conversationId = const Value.absent(), + this.accountId = const Value.absent(), + this.roleId = const Value.absent(), + this.rowid = const Value.absent(), + }); + MemberCompanion.insert({ + required String id, + this.conversationId = const Value.absent(), + required String accountId, + required int roleId, + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + roleId = Value(roleId); + static Insertable custom({ + Expression? id, + Expression? conversationId, + Expression? accountId, + Expression? roleId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (conversationId != null) 'conversation_id': conversationId, + if (accountId != null) 'account_id': accountId, + if (roleId != null) 'role_id': roleId, + if (rowid != null) 'rowid': rowid, + }); + } + + MemberCompanion copyWith({ + Value? id, + Value? conversationId, + Value? accountId, + Value? roleId, + Value? rowid, + }) { + return MemberCompanion( + id: id ?? this.id, + conversationId: conversationId ?? this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (conversationId.present) { + map['conversation_id'] = Variable(conversationId.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (roleId.present) { + map['role_id'] = Variable(roleId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemberCompanion(') + ..write('id: $id, ') + ..write('conversationId: $conversationId, ') + ..write('accountId: $accountId, ') + ..write('roleId: $roleId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Setting extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Setting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'setting'; + @override + Set get $primaryKey => {key}; + @override + SettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingData( + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + Setting createAlias(String alias) { + return Setting(attachedDatabase, alias); + } +} + +class SettingData extends DataClass implements Insertable { + final String key; + final String value; + const SettingData({required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + SettingCompanion toCompanion(bool nullToAbsent) { + return SettingCompanion(key: Value(key), value: Value(value)); + } + + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingData( + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + SettingData copyWith({String? key, String? value}) => + SettingData(key: key ?? this.key, value: value ?? this.value); + SettingData copyWithCompanion(SettingCompanion data) { + return SettingData( + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingData(') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingData && + other.key == this.key && + other.value == this.value); +} + +class SettingCompanion extends UpdateCompanion { + final Value key; + final Value value; + final Value rowid; + const SettingCompanion({ + this.key = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingCompanion.insert({ + required String key, + required String value, + this.rowid = const Value.absent(), + }) : key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? key, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingCompanion copyWith({ + Value? key, + Value? value, + Value? rowid, + }) { + return SettingCompanion( + key: key ?? this.key, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingCompanion(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Friend extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Friend(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + displayName, + vaultId, + keys, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'friend'; + @override + Set get $primaryKey => {id}; + @override + FriendData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return FriendData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + Friend createAlias(String alias) { + return Friend(attachedDatabase, alias); + } +} + +class FriendData extends DataClass implements Insertable { + final String id; + final String name; + final String displayName; + final String vaultId; + final String keys; + final BigInt updatedAt; + const FriendData({ + required this.id, + required this.name, + required this.displayName, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['vault_id'] = Variable(vaultId); + map['keys'] = Variable(keys); + map['updated_at'] = Variable(updatedAt); + return map; + } + + FriendCompanion toCompanion(bool nullToAbsent) { + return FriendCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + vaultId: Value(vaultId), + keys: Value(keys), + updatedAt: Value(updatedAt), + ); + } + + factory FriendData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return FriendData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + vaultId: serializer.fromJson(json['vaultId']), + keys: serializer.fromJson(json['keys']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'vaultId': serializer.toJson(vaultId), + 'keys': serializer.toJson(keys), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + FriendData copyWith({ + String? id, + String? name, + String? displayName, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => FriendData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); + FriendData copyWithCompanion(FriendCompanion data) { + return FriendData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + keys: data.keys.present ? data.keys.value : this.keys, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('FriendData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, displayName, vaultId, keys, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is FriendData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.vaultId == this.vaultId && + other.keys == this.keys && + other.updatedAt == this.updatedAt); +} + +class FriendCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value vaultId; + final Value keys; + final Value updatedAt; + final Value rowid; + const FriendCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.vaultId = const Value.absent(), + this.keys = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + FriendCompanion.insert({ + required String id, + required String name, + required String displayName, + required String vaultId, + required String keys, + required BigInt updatedAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? vaultId, + Expression? keys, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (vaultId != null) 'vault_id': vaultId, + if (keys != null) 'keys': keys, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + FriendCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { + return FriendCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('FriendCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Request extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Request(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn self = GeneratedColumn( + 'self', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("self" IN (0, 1))', + ), + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + displayName, + self, + vaultId, + keys, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'request'; + @override + Set get $primaryKey => {id}; + @override + RequestData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RequestData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + self: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}self'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + Request createAlias(String alias) { + return Request(attachedDatabase, alias); + } +} + +class RequestData extends DataClass implements Insertable { + final String id; + final String name; + final String displayName; + final bool self; + final String vaultId; + final String keys; + final BigInt updatedAt; + const RequestData({ + required this.id, + required this.name, + required this.displayName, + required this.self, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['self'] = Variable(self); + map['vault_id'] = Variable(vaultId); + map['keys'] = Variable(keys); + map['updated_at'] = Variable(updatedAt); + return map; + } + + RequestCompanion toCompanion(bool nullToAbsent) { + return RequestCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + self: Value(self), + vaultId: Value(vaultId), + keys: Value(keys), + updatedAt: Value(updatedAt), + ); + } + + factory RequestData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RequestData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + self: serializer.fromJson(json['self']), + vaultId: serializer.fromJson(json['vaultId']), + keys: serializer.fromJson(json['keys']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'self': serializer.toJson(self), + 'vaultId': serializer.toJson(vaultId), + 'keys': serializer.toJson(keys), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + RequestData copyWith({ + String? id, + String? name, + String? displayName, + bool? self, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => RequestData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); + RequestData copyWithCompanion(RequestCompanion data) { + return RequestData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + self: data.self.present ? data.self.value : this.self, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + keys: data.keys.present ? data.keys.value : this.keys, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('RequestData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('self: $self, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, displayName, self, vaultId, keys, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RequestData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.self == this.self && + other.vaultId == this.vaultId && + other.keys == this.keys && + other.updatedAt == this.updatedAt); +} + +class RequestCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value self; + final Value vaultId; + final Value keys; + final Value updatedAt; + final Value rowid; + const RequestCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.self = const Value.absent(), + this.vaultId = const Value.absent(), + this.keys = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + RequestCompanion.insert({ + required String id, + required String name, + required String displayName, + required bool self, + required String vaultId, + required String keys, + required BigInt updatedAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + self = Value(self), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? self, + Expression? vaultId, + Expression? keys, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (self != null) 'self': self, + if (vaultId != null) 'vault_id': vaultId, + if (keys != null) 'keys': keys, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + RequestCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? self, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { + return RequestCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (self.present) { + map['self'] = Variable(self.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RequestCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('self: $self, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class UnknownProfile extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UnknownProfile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, name, displayName, keys]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'unknown_profile'; + @override + Set get $primaryKey => {id}; + @override + UnknownProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UnknownProfileData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + ); + } + + @override + UnknownProfile createAlias(String alias) { + return UnknownProfile(attachedDatabase, alias); + } +} + +class UnknownProfileData extends DataClass + implements Insertable { + final String id; + final String name; + final String displayName; + final String keys; + const UnknownProfileData({ + required this.id, + required this.name, + required this.displayName, + required this.keys, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['keys'] = Variable(keys); + return map; + } + + UnknownProfileCompanion toCompanion(bool nullToAbsent) { + return UnknownProfileCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + keys: Value(keys), + ); + } + + factory UnknownProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UnknownProfileData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + keys: serializer.fromJson(json['keys']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'keys': serializer.toJson(keys), + }; + } + + UnknownProfileData copyWith({ + String? id, + String? name, + String? displayName, + String? keys, + }) => UnknownProfileData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + ); + UnknownProfileData copyWithCompanion(UnknownProfileCompanion data) { + return UnknownProfileData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + keys: data.keys.present ? data.keys.value : this.keys, + ); + } + + @override + String toString() { + return (StringBuffer('UnknownProfileData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('keys: $keys') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, displayName, keys); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UnknownProfileData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.keys == this.keys); +} + +class UnknownProfileCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value keys; + final Value rowid; + const UnknownProfileCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.keys = const Value.absent(), + this.rowid = const Value.absent(), + }); + UnknownProfileCompanion.insert({ + required String id, + required String name, + required String displayName, + required String keys, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + keys = Value(keys); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? keys, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (keys != null) 'keys': keys, + if (rowid != null) 'rowid': rowid, + }); + } + + UnknownProfileCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? keys, + Value? rowid, + }) { + return UnknownProfileCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UnknownProfileCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('keys: $keys, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Profile extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Profile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn pictureContainer = GeneratedColumn( + 'picture_container', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, pictureContainer, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'profile'; + @override + Set get $primaryKey => {id}; + @override + ProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProfileData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + pictureContainer: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}picture_container'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + ); + } + + @override + Profile createAlias(String alias) { + return Profile(attachedDatabase, alias); + } +} + +class ProfileData extends DataClass implements Insertable { + final String id; + final String pictureContainer; + final String data; + const ProfileData({ + required this.id, + required this.pictureContainer, + required this.data, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['picture_container'] = Variable(pictureContainer); + map['data'] = Variable(data); + return map; + } + + ProfileCompanion toCompanion(bool nullToAbsent) { + return ProfileCompanion( + id: Value(id), + pictureContainer: Value(pictureContainer), + data: Value(data), + ); + } + + factory ProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProfileData( + id: serializer.fromJson(json['id']), + pictureContainer: serializer.fromJson(json['pictureContainer']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'pictureContainer': serializer.toJson(pictureContainer), + 'data': serializer.toJson(data), + }; + } + + ProfileData copyWith({String? id, String? pictureContainer, String? data}) => + ProfileData( + id: id ?? this.id, + pictureContainer: pictureContainer ?? this.pictureContainer, + data: data ?? this.data, + ); + ProfileData copyWithCompanion(ProfileCompanion data) { + return ProfileData( + id: data.id.present ? data.id.value : this.id, + pictureContainer: + data.pictureContainer.present + ? data.pictureContainer.value + : this.pictureContainer, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('ProfileData(') + ..write('id: $id, ') + ..write('pictureContainer: $pictureContainer, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, pictureContainer, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProfileData && + other.id == this.id && + other.pictureContainer == this.pictureContainer && + other.data == this.data); +} + +class ProfileCompanion extends UpdateCompanion { + final Value id; + final Value pictureContainer; + final Value data; + final Value rowid; + const ProfileCompanion({ + this.id = const Value.absent(), + this.pictureContainer = const Value.absent(), + this.data = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProfileCompanion.insert({ + required String id, + required String pictureContainer, + required String data, + this.rowid = const Value.absent(), + }) : id = Value(id), + pictureContainer = Value(pictureContainer), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? pictureContainer, + Expression? data, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (pictureContainer != null) 'picture_container': pictureContainer, + if (data != null) 'data': data, + if (rowid != null) 'rowid': rowid, + }); + } + + ProfileCompanion copyWith({ + Value? id, + Value? pictureContainer, + Value? data, + Value? rowid, + }) { + return ProfileCompanion( + id: id ?? this.id, + pictureContainer: pictureContainer ?? this.pictureContainer, + data: data ?? this.data, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (pictureContainer.present) { + map['picture_container'] = Variable(pictureContainer.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProfileCompanion(') + ..write('id: $id, ') + ..write('pictureContainer: $pictureContainer, ') + ..write('data: $data, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class TrustedLink extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrustedLink(this.attachedDatabase, [this._alias]); + late final GeneratedColumn domain = GeneratedColumn( + 'domain', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [domain]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trusted_link'; + @override + Set get $primaryKey => {domain}; + @override + TrustedLinkData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrustedLinkData( + domain: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}domain'], + )!, + ); + } + + @override + TrustedLink createAlias(String alias) { + return TrustedLink(attachedDatabase, alias); + } +} + +class TrustedLinkData extends DataClass implements Insertable { + final String domain; + const TrustedLinkData({required this.domain}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['domain'] = Variable(domain); + return map; + } + + TrustedLinkCompanion toCompanion(bool nullToAbsent) { + return TrustedLinkCompanion(domain: Value(domain)); + } + + factory TrustedLinkData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrustedLinkData(domain: serializer.fromJson(json['domain'])); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return {'domain': serializer.toJson(domain)}; + } + + TrustedLinkData copyWith({String? domain}) => + TrustedLinkData(domain: domain ?? this.domain); + TrustedLinkData copyWithCompanion(TrustedLinkCompanion data) { + return TrustedLinkData( + domain: data.domain.present ? data.domain.value : this.domain, + ); + } + + @override + String toString() { + return (StringBuffer('TrustedLinkData(') + ..write('domain: $domain') + ..write(')')) + .toString(); + } + + @override + int get hashCode => domain.hashCode; + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrustedLinkData && other.domain == this.domain); +} + +class TrustedLinkCompanion extends UpdateCompanion { + final Value domain; + final Value rowid; + const TrustedLinkCompanion({ + this.domain = const Value.absent(), + this.rowid = const Value.absent(), + }); + TrustedLinkCompanion.insert({ + required String domain, + this.rowid = const Value.absent(), + }) : domain = Value(domain); + static Insertable custom({ + Expression? domain, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (domain != null) 'domain': domain, + if (rowid != null) 'rowid': rowid, + }); + } + + TrustedLinkCompanion copyWith({Value? domain, Value? rowid}) { + return TrustedLinkCompanion( + domain: domain ?? this.domain, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (domain.present) { + map['domain'] = Variable(domain.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrustedLinkCompanion(') + ..write('domain: $domain, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class LibraryEntry extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LibraryEntry(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + type, + createdAt, + data, + width, + height, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'library_entry'; + @override + Set get $primaryKey => {id}; + @override + LibraryEntryData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LibraryEntryData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + width: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + )!, + height: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + )!, + ); + } + + @override + LibraryEntry createAlias(String alias) { + return LibraryEntry(attachedDatabase, alias); + } +} + +class LibraryEntryData extends DataClass + implements Insertable { + final String id; + final int type; + final BigInt createdAt; + final String data; + final int width; + final int height; + const LibraryEntryData({ + required this.id, + required this.type, + required this.createdAt, + required this.data, + required this.width, + required this.height, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['data'] = Variable(data); + map['width'] = Variable(width); + map['height'] = Variable(height); + return map; + } + + LibraryEntryCompanion toCompanion(bool nullToAbsent) { + return LibraryEntryCompanion( + id: Value(id), + type: Value(type), + createdAt: Value(createdAt), + data: Value(data), + width: Value(width), + height: Value(height), + ); + } + + factory LibraryEntryData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LibraryEntryData( + id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + data: serializer.fromJson(json['data']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'data': serializer.toJson(data), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + }; + } + + LibraryEntryData copyWith({ + String? id, + int? type, + BigInt? createdAt, + String? data, + int? width, + int? height, + }) => LibraryEntryData( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + ); + LibraryEntryData copyWithCompanion(LibraryEntryCompanion data) { + return LibraryEntryData( + id: data.id.present ? data.id.value : this.id, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + data: data.data.present ? data.data.value : this.data, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + ); + } + + @override + String toString() { + return (StringBuffer('LibraryEntryData(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('data: $data, ') + ..write('width: $width, ') + ..write('height: $height') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, type, createdAt, data, width, height); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LibraryEntryData && + other.id == this.id && + other.type == this.type && + other.createdAt == this.createdAt && + other.data == this.data && + other.width == this.width && + other.height == this.height); +} + +class LibraryEntryCompanion extends UpdateCompanion { + final Value id; + final Value type; + final Value createdAt; + final Value data; + final Value width; + final Value height; + final Value rowid; + const LibraryEntryCompanion({ + this.id = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.data = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.rowid = const Value.absent(), + }); + LibraryEntryCompanion.insert({ + required String id, + required int type, + required BigInt createdAt, + required String data, + required int width, + required int height, + this.rowid = const Value.absent(), + }) : id = Value(id), + type = Value(type), + createdAt = Value(createdAt), + data = Value(data), + width = Value(width), + height = Value(height); + static Insertable custom({ + Expression? id, + Expression? type, + Expression? createdAt, + Expression? data, + Expression? width, + Expression? height, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (data != null) 'data': data, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (rowid != null) 'rowid': rowid, + }); + } + + LibraryEntryCompanion copyWith({ + Value? id, + Value? type, + Value? createdAt, + Value? data, + Value? width, + Value? height, + Value? rowid, + }) { + return LibraryEntryCompanion( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LibraryEntryCompanion(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('data: $data, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + late final Conversation conversation = Conversation(this); + late final Message message = Message(this); + late final Member member = Member(this); + late final Setting setting = Setting(this); + late final Friend friend = Friend(this); + late final Request request = Request(this); + late final UnknownProfile unknownProfile = UnknownProfile(this); + late final Profile profile = Profile(this); + late final TrustedLink trustedLink = TrustedLink(this); + late final LibraryEntry libraryEntry = LibraryEntry(this); + late final Index idxConversationUpdated = Index( + 'idx_conversation_updated', + 'CREATE INDEX idx_conversation_updated ON conversation (updated_at)', + ); + late final Index idxMessageCreated = Index( + 'idx_message_created', + 'CREATE INDEX idx_message_created ON message (created_at)', + ); + late final Index idxFriendsUpdated = Index( + 'idx_friends_updated', + 'CREATE INDEX idx_friends_updated ON friend (updated_at)', + ); + late final Index idxLibraryEntryCreated = Index( + 'idx_library_entry_created', + 'CREATE INDEX idx_library_entry_created ON library_entry (created_at)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + conversation, + message, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + idxConversationUpdated, + idxMessageCreated, + idxFriendsUpdated, + idxLibraryEntryCreated, + ]; + @override + int get schemaVersion => 3; +} diff --git a/test/drift/main/generated/schema_v4.dart b/test/drift/main/generated/schema_v4.dart new file mode 100644 index 00000000..eee5c488 --- /dev/null +++ b/test/drift/main/generated/schema_v4.dart @@ -0,0 +1,3172 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class Conversation extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Conversation(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn token = GeneratedColumn( + 'token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn lastVersion = GeneratedColumn( + 'last_version', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn readAt = GeneratedColumn( + 'read_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + readAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'conversation'; + @override + Set get $primaryKey => {id}; + @override + ConversationData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ConversationData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + token: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}token'], + )!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + lastVersion: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}last_version'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + readAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}read_at'], + )!, + ); + } + + @override + Conversation createAlias(String alias) { + return Conversation(attachedDatabase, alias); + } +} + +class ConversationData extends DataClass + implements Insertable { + final String id; + final String vaultId; + final int type; + final String data; + final String token; + final String key; + final BigInt lastVersion; + final BigInt updatedAt; + final BigInt readAt; + const ConversationData({ + required this.id, + required this.vaultId, + required this.type, + required this.data, + required this.token, + required this.key, + required this.lastVersion, + required this.updatedAt, + required this.readAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['vault_id'] = Variable(vaultId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['token'] = Variable(token); + map['key'] = Variable(key); + map['last_version'] = Variable(lastVersion); + map['updated_at'] = Variable(updatedAt); + map['read_at'] = Variable(readAt); + return map; + } + + ConversationCompanion toCompanion(bool nullToAbsent) { + return ConversationCompanion( + id: Value(id), + vaultId: Value(vaultId), + type: Value(type), + data: Value(data), + token: Value(token), + key: Value(key), + lastVersion: Value(lastVersion), + updatedAt: Value(updatedAt), + readAt: Value(readAt), + ); + } + + factory ConversationData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ConversationData( + id: serializer.fromJson(json['id']), + vaultId: serializer.fromJson(json['vaultId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + token: serializer.fromJson(json['token']), + key: serializer.fromJson(json['key']), + lastVersion: serializer.fromJson(json['lastVersion']), + updatedAt: serializer.fromJson(json['updatedAt']), + readAt: serializer.fromJson(json['readAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'vaultId': serializer.toJson(vaultId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'token': serializer.toJson(token), + 'key': serializer.toJson(key), + 'lastVersion': serializer.toJson(lastVersion), + 'updatedAt': serializer.toJson(updatedAt), + 'readAt': serializer.toJson(readAt), + }; + } + + ConversationData copyWith({ + String? id, + String? vaultId, + int? type, + String? data, + String? token, + String? key, + BigInt? lastVersion, + BigInt? updatedAt, + BigInt? readAt, + }) => ConversationData( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + readAt: readAt ?? this.readAt, + ); + ConversationData copyWithCompanion(ConversationCompanion data) { + return ConversationData( + id: data.id.present ? data.id.value : this.id, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + token: data.token.present ? data.token.value : this.token, + key: data.key.present ? data.key.value : this.key, + lastVersion: + data.lastVersion.present ? data.lastVersion.value : this.lastVersion, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + readAt: data.readAt.present ? data.readAt.value : this.readAt, + ); + } + + @override + String toString() { + return (StringBuffer('ConversationData(') + ..write('id: $id, ') + ..write('vaultId: $vaultId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('token: $token, ') + ..write('key: $key, ') + ..write('lastVersion: $lastVersion, ') + ..write('updatedAt: $updatedAt, ') + ..write('readAt: $readAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + readAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ConversationData && + other.id == this.id && + other.vaultId == this.vaultId && + other.type == this.type && + other.data == this.data && + other.token == this.token && + other.key == this.key && + other.lastVersion == this.lastVersion && + other.updatedAt == this.updatedAt && + other.readAt == this.readAt); +} + +class ConversationCompanion extends UpdateCompanion { + final Value id; + final Value vaultId; + final Value type; + final Value data; + final Value token; + final Value key; + final Value lastVersion; + final Value updatedAt; + final Value readAt; + final Value rowid; + const ConversationCompanion({ + this.id = const Value.absent(), + this.vaultId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.token = const Value.absent(), + this.key = const Value.absent(), + this.lastVersion = const Value.absent(), + this.updatedAt = const Value.absent(), + this.readAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ConversationCompanion.insert({ + required String id, + required String vaultId, + required int type, + required String data, + required String token, + required String key, + required BigInt lastVersion, + required BigInt updatedAt, + required BigInt readAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + vaultId = Value(vaultId), + type = Value(type), + data = Value(data), + token = Value(token), + key = Value(key), + lastVersion = Value(lastVersion), + updatedAt = Value(updatedAt), + readAt = Value(readAt); + static Insertable custom({ + Expression? id, + Expression? vaultId, + Expression? type, + Expression? data, + Expression? token, + Expression? key, + Expression? lastVersion, + Expression? updatedAt, + Expression? readAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (vaultId != null) 'vault_id': vaultId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (token != null) 'token': token, + if (key != null) 'key': key, + if (lastVersion != null) 'last_version': lastVersion, + if (updatedAt != null) 'updated_at': updatedAt, + if (readAt != null) 'read_at': readAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ConversationCompanion copyWith({ + Value? id, + Value? vaultId, + Value? type, + Value? data, + Value? token, + Value? key, + Value? lastVersion, + Value? updatedAt, + Value? readAt, + Value? rowid, + }) { + return ConversationCompanion( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + readAt: readAt ?? this.readAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (token.present) { + map['token'] = Variable(token.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (lastVersion.present) { + map['last_version'] = Variable(lastVersion.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (readAt.present) { + map['read_at'] = Variable(readAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ConversationCompanion(') + ..write('id: $id, ') + ..write('vaultId: $vaultId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('token: $token, ') + ..write('key: $key, ') + ..write('lastVersion: $lastVersion, ') + ..write('updatedAt: $updatedAt, ') + ..write('readAt: $readAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Message extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Message(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn content = GeneratedColumn( + 'content', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn senderToken = GeneratedColumn( + 'sender_token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn senderAddress = GeneratedColumn( + 'sender_address', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn conversation = GeneratedColumn( + 'conversation', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn edited = GeneratedColumn( + 'edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("edited" IN (0, 1))', + ), + ); + late final GeneratedColumn verified = GeneratedColumn( + 'verified', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("verified" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message'; + @override + Set get $primaryKey => {id}; + @override + MessageData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + content: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + )!, + senderToken: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_token'], + )!, + senderAddress: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_address'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + conversation: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation'], + )!, + edited: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}edited'], + )!, + verified: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}verified'], + )!, + ); + } + + @override + Message createAlias(String alias) { + return Message(attachedDatabase, alias); + } +} + +class MessageData extends DataClass implements Insertable { + final String id; + final String content; + final String senderToken; + final String senderAddress; + final BigInt createdAt; + final String conversation; + final bool edited; + final bool verified; + const MessageData({ + required this.id, + required this.content, + required this.senderToken, + required this.senderAddress, + required this.createdAt, + required this.conversation, + required this.edited, + required this.verified, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['content'] = Variable(content); + map['sender_token'] = Variable(senderToken); + map['sender_address'] = Variable(senderAddress); + map['created_at'] = Variable(createdAt); + map['conversation'] = Variable(conversation); + map['edited'] = Variable(edited); + map['verified'] = Variable(verified); + return map; + } + + MessageCompanion toCompanion(bool nullToAbsent) { + return MessageCompanion( + id: Value(id), + content: Value(content), + senderToken: Value(senderToken), + senderAddress: Value(senderAddress), + createdAt: Value(createdAt), + conversation: Value(conversation), + edited: Value(edited), + verified: Value(verified), + ); + } + + factory MessageData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageData( + id: serializer.fromJson(json['id']), + content: serializer.fromJson(json['content']), + senderToken: serializer.fromJson(json['senderToken']), + senderAddress: serializer.fromJson(json['senderAddress']), + createdAt: serializer.fromJson(json['createdAt']), + conversation: serializer.fromJson(json['conversation']), + edited: serializer.fromJson(json['edited']), + verified: serializer.fromJson(json['verified']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'content': serializer.toJson(content), + 'senderToken': serializer.toJson(senderToken), + 'senderAddress': serializer.toJson(senderAddress), + 'createdAt': serializer.toJson(createdAt), + 'conversation': serializer.toJson(conversation), + 'edited': serializer.toJson(edited), + 'verified': serializer.toJson(verified), + }; + } + + MessageData copyWith({ + String? id, + String? content, + String? senderToken, + String? senderAddress, + BigInt? createdAt, + String? conversation, + bool? edited, + bool? verified, + }) => MessageData( + id: id ?? this.id, + content: content ?? this.content, + senderToken: senderToken ?? this.senderToken, + senderAddress: senderAddress ?? this.senderAddress, + createdAt: createdAt ?? this.createdAt, + conversation: conversation ?? this.conversation, + edited: edited ?? this.edited, + verified: verified ?? this.verified, + ); + MessageData copyWithCompanion(MessageCompanion data) { + return MessageData( + id: data.id.present ? data.id.value : this.id, + content: data.content.present ? data.content.value : this.content, + senderToken: + data.senderToken.present ? data.senderToken.value : this.senderToken, + senderAddress: + data.senderAddress.present + ? data.senderAddress.value + : this.senderAddress, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + conversation: + data.conversation.present + ? data.conversation.value + : this.conversation, + edited: data.edited.present ? data.edited.value : this.edited, + verified: data.verified.present ? data.verified.value : this.verified, + ); + } + + @override + String toString() { + return (StringBuffer('MessageData(') + ..write('id: $id, ') + ..write('content: $content, ') + ..write('senderToken: $senderToken, ') + ..write('senderAddress: $senderAddress, ') + ..write('createdAt: $createdAt, ') + ..write('conversation: $conversation, ') + ..write('edited: $edited, ') + ..write('verified: $verified') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageData && + other.id == this.id && + other.content == this.content && + other.senderToken == this.senderToken && + other.senderAddress == this.senderAddress && + other.createdAt == this.createdAt && + other.conversation == this.conversation && + other.edited == this.edited && + other.verified == this.verified); +} + +class MessageCompanion extends UpdateCompanion { + final Value id; + final Value content; + final Value senderToken; + final Value senderAddress; + final Value createdAt; + final Value conversation; + final Value edited; + final Value verified; + final Value rowid; + const MessageCompanion({ + this.id = const Value.absent(), + this.content = const Value.absent(), + this.senderToken = const Value.absent(), + this.senderAddress = const Value.absent(), + this.createdAt = const Value.absent(), + this.conversation = const Value.absent(), + this.edited = const Value.absent(), + this.verified = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessageCompanion.insert({ + required String id, + required String content, + required String senderToken, + required String senderAddress, + required BigInt createdAt, + required String conversation, + required bool edited, + required bool verified, + this.rowid = const Value.absent(), + }) : id = Value(id), + content = Value(content), + senderToken = Value(senderToken), + senderAddress = Value(senderAddress), + createdAt = Value(createdAt), + conversation = Value(conversation), + edited = Value(edited), + verified = Value(verified); + static Insertable custom({ + Expression? id, + Expression? content, + Expression? senderToken, + Expression? senderAddress, + Expression? createdAt, + Expression? conversation, + Expression? edited, + Expression? verified, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (content != null) 'content': content, + if (senderToken != null) 'sender_token': senderToken, + if (senderAddress != null) 'sender_address': senderAddress, + if (createdAt != null) 'created_at': createdAt, + if (conversation != null) 'conversation': conversation, + if (edited != null) 'edited': edited, + if (verified != null) 'verified': verified, + if (rowid != null) 'rowid': rowid, + }); + } + + MessageCompanion copyWith({ + Value? id, + Value? content, + Value? senderToken, + Value? senderAddress, + Value? createdAt, + Value? conversation, + Value? edited, + Value? verified, + Value? rowid, + }) { + return MessageCompanion( + id: id ?? this.id, + content: content ?? this.content, + senderToken: senderToken ?? this.senderToken, + senderAddress: senderAddress ?? this.senderAddress, + createdAt: createdAt ?? this.createdAt, + conversation: conversation ?? this.conversation, + edited: edited ?? this.edited, + verified: verified ?? this.verified, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (senderToken.present) { + map['sender_token'] = Variable(senderToken.value); + } + if (senderAddress.present) { + map['sender_address'] = Variable(senderAddress.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (conversation.present) { + map['conversation'] = Variable(conversation.value); + } + if (edited.present) { + map['edited'] = Variable(edited.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageCompanion(') + ..write('id: $id, ') + ..write('content: $content, ') + ..write('senderToken: $senderToken, ') + ..write('senderAddress: $senderAddress, ') + ..write('createdAt: $createdAt, ') + ..write('conversation: $conversation, ') + ..write('edited: $edited, ') + ..write('verified: $verified, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Member extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Member(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn conversationId = GeneratedColumn( + 'conversation_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn roleId = GeneratedColumn( + 'role_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, conversationId, accountId, roleId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'member'; + @override + Set get $primaryKey => {id}; + @override + MemberData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemberData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + conversationId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation_id'], + ), + accountId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + roleId: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role_id'], + )!, + ); + } + + @override + Member createAlias(String alias) { + return Member(attachedDatabase, alias); + } +} + +class MemberData extends DataClass implements Insertable { + final String id; + final String? conversationId; + final String accountId; + final int roleId; + const MemberData({ + required this.id, + this.conversationId, + required this.accountId, + required this.roleId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || conversationId != null) { + map['conversation_id'] = Variable(conversationId); + } + map['account_id'] = Variable(accountId); + map['role_id'] = Variable(roleId); + return map; + } + + MemberCompanion toCompanion(bool nullToAbsent) { + return MemberCompanion( + id: Value(id), + conversationId: + conversationId == null && nullToAbsent + ? const Value.absent() + : Value(conversationId), + accountId: Value(accountId), + roleId: Value(roleId), + ); + } + + factory MemberData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemberData( + id: serializer.fromJson(json['id']), + conversationId: serializer.fromJson(json['conversationId']), + accountId: serializer.fromJson(json['accountId']), + roleId: serializer.fromJson(json['roleId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'conversationId': serializer.toJson(conversationId), + 'accountId': serializer.toJson(accountId), + 'roleId': serializer.toJson(roleId), + }; + } + + MemberData copyWith({ + String? id, + Value conversationId = const Value.absent(), + String? accountId, + int? roleId, + }) => MemberData( + id: id ?? this.id, + conversationId: + conversationId.present ? conversationId.value : this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + ); + MemberData copyWithCompanion(MemberCompanion data) { + return MemberData( + id: data.id.present ? data.id.value : this.id, + conversationId: + data.conversationId.present + ? data.conversationId.value + : this.conversationId, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + roleId: data.roleId.present ? data.roleId.value : this.roleId, + ); + } + + @override + String toString() { + return (StringBuffer('MemberData(') + ..write('id: $id, ') + ..write('conversationId: $conversationId, ') + ..write('accountId: $accountId, ') + ..write('roleId: $roleId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, conversationId, accountId, roleId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemberData && + other.id == this.id && + other.conversationId == this.conversationId && + other.accountId == this.accountId && + other.roleId == this.roleId); +} + +class MemberCompanion extends UpdateCompanion { + final Value id; + final Value conversationId; + final Value accountId; + final Value roleId; + final Value rowid; + const MemberCompanion({ + this.id = const Value.absent(), + this.conversationId = const Value.absent(), + this.accountId = const Value.absent(), + this.roleId = const Value.absent(), + this.rowid = const Value.absent(), + }); + MemberCompanion.insert({ + required String id, + this.conversationId = const Value.absent(), + required String accountId, + required int roleId, + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + roleId = Value(roleId); + static Insertable custom({ + Expression? id, + Expression? conversationId, + Expression? accountId, + Expression? roleId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (conversationId != null) 'conversation_id': conversationId, + if (accountId != null) 'account_id': accountId, + if (roleId != null) 'role_id': roleId, + if (rowid != null) 'rowid': rowid, + }); + } + + MemberCompanion copyWith({ + Value? id, + Value? conversationId, + Value? accountId, + Value? roleId, + Value? rowid, + }) { + return MemberCompanion( + id: id ?? this.id, + conversationId: conversationId ?? this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (conversationId.present) { + map['conversation_id'] = Variable(conversationId.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (roleId.present) { + map['role_id'] = Variable(roleId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemberCompanion(') + ..write('id: $id, ') + ..write('conversationId: $conversationId, ') + ..write('accountId: $accountId, ') + ..write('roleId: $roleId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Setting extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Setting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'setting'; + @override + Set get $primaryKey => {key}; + @override + SettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingData( + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + Setting createAlias(String alias) { + return Setting(attachedDatabase, alias); + } +} + +class SettingData extends DataClass implements Insertable { + final String key; + final String value; + const SettingData({required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + SettingCompanion toCompanion(bool nullToAbsent) { + return SettingCompanion(key: Value(key), value: Value(value)); + } + + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingData( + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + SettingData copyWith({String? key, String? value}) => + SettingData(key: key ?? this.key, value: value ?? this.value); + SettingData copyWithCompanion(SettingCompanion data) { + return SettingData( + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingData(') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingData && + other.key == this.key && + other.value == this.value); +} + +class SettingCompanion extends UpdateCompanion { + final Value key; + final Value value; + final Value rowid; + const SettingCompanion({ + this.key = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingCompanion.insert({ + required String key, + required String value, + this.rowid = const Value.absent(), + }) : key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? key, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingCompanion copyWith({ + Value? key, + Value? value, + Value? rowid, + }) { + return SettingCompanion( + key: key ?? this.key, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingCompanion(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Friend extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Friend(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + displayName, + vaultId, + keys, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'friend'; + @override + Set get $primaryKey => {id}; + @override + FriendData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return FriendData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + Friend createAlias(String alias) { + return Friend(attachedDatabase, alias); + } +} + +class FriendData extends DataClass implements Insertable { + final String id; + final String name; + final String displayName; + final String vaultId; + final String keys; + final BigInt updatedAt; + const FriendData({ + required this.id, + required this.name, + required this.displayName, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['vault_id'] = Variable(vaultId); + map['keys'] = Variable(keys); + map['updated_at'] = Variable(updatedAt); + return map; + } + + FriendCompanion toCompanion(bool nullToAbsent) { + return FriendCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + vaultId: Value(vaultId), + keys: Value(keys), + updatedAt: Value(updatedAt), + ); + } + + factory FriendData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return FriendData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + vaultId: serializer.fromJson(json['vaultId']), + keys: serializer.fromJson(json['keys']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'vaultId': serializer.toJson(vaultId), + 'keys': serializer.toJson(keys), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + FriendData copyWith({ + String? id, + String? name, + String? displayName, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => FriendData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); + FriendData copyWithCompanion(FriendCompanion data) { + return FriendData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + keys: data.keys.present ? data.keys.value : this.keys, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('FriendData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, displayName, vaultId, keys, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is FriendData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.vaultId == this.vaultId && + other.keys == this.keys && + other.updatedAt == this.updatedAt); +} + +class FriendCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value vaultId; + final Value keys; + final Value updatedAt; + final Value rowid; + const FriendCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.vaultId = const Value.absent(), + this.keys = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + FriendCompanion.insert({ + required String id, + required String name, + required String displayName, + required String vaultId, + required String keys, + required BigInt updatedAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? vaultId, + Expression? keys, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (vaultId != null) 'vault_id': vaultId, + if (keys != null) 'keys': keys, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + FriendCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { + return FriendCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('FriendCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Request extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Request(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn self = GeneratedColumn( + 'self', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("self" IN (0, 1))', + ), + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + displayName, + self, + vaultId, + keys, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'request'; + @override + Set get $primaryKey => {id}; + @override + RequestData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RequestData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + self: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}self'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + Request createAlias(String alias) { + return Request(attachedDatabase, alias); + } +} + +class RequestData extends DataClass implements Insertable { + final String id; + final String name; + final String displayName; + final bool self; + final String vaultId; + final String keys; + final BigInt updatedAt; + const RequestData({ + required this.id, + required this.name, + required this.displayName, + required this.self, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['self'] = Variable(self); + map['vault_id'] = Variable(vaultId); + map['keys'] = Variable(keys); + map['updated_at'] = Variable(updatedAt); + return map; + } + + RequestCompanion toCompanion(bool nullToAbsent) { + return RequestCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + self: Value(self), + vaultId: Value(vaultId), + keys: Value(keys), + updatedAt: Value(updatedAt), + ); + } + + factory RequestData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RequestData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + self: serializer.fromJson(json['self']), + vaultId: serializer.fromJson(json['vaultId']), + keys: serializer.fromJson(json['keys']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'self': serializer.toJson(self), + 'vaultId': serializer.toJson(vaultId), + 'keys': serializer.toJson(keys), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + RequestData copyWith({ + String? id, + String? name, + String? displayName, + bool? self, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => RequestData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); + RequestData copyWithCompanion(RequestCompanion data) { + return RequestData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + self: data.self.present ? data.self.value : this.self, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + keys: data.keys.present ? data.keys.value : this.keys, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('RequestData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('self: $self, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, displayName, self, vaultId, keys, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RequestData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.self == this.self && + other.vaultId == this.vaultId && + other.keys == this.keys && + other.updatedAt == this.updatedAt); +} + +class RequestCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value self; + final Value vaultId; + final Value keys; + final Value updatedAt; + final Value rowid; + const RequestCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.self = const Value.absent(), + this.vaultId = const Value.absent(), + this.keys = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + RequestCompanion.insert({ + required String id, + required String name, + required String displayName, + required bool self, + required String vaultId, + required String keys, + required BigInt updatedAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + self = Value(self), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? self, + Expression? vaultId, + Expression? keys, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (self != null) 'self': self, + if (vaultId != null) 'vault_id': vaultId, + if (keys != null) 'keys': keys, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + RequestCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? self, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { + return RequestCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (self.present) { + map['self'] = Variable(self.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RequestCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('self: $self, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class UnknownProfile extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UnknownProfile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn lastFetched = GeneratedColumn( + 'last_fetched', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + displayName, + keys, + lastFetched, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'unknown_profile'; + @override + Set get $primaryKey => {id}; + @override + UnknownProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UnknownProfileData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + lastFetched: + attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_fetched'], + )!, + ); + } + + @override + UnknownProfile createAlias(String alias) { + return UnknownProfile(attachedDatabase, alias); + } +} + +class UnknownProfileData extends DataClass + implements Insertable { + final String id; + final String name; + final String displayName; + final String keys; + final DateTime lastFetched; + const UnknownProfileData({ + required this.id, + required this.name, + required this.displayName, + required this.keys, + required this.lastFetched, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['keys'] = Variable(keys); + map['last_fetched'] = Variable(lastFetched); + return map; + } + + UnknownProfileCompanion toCompanion(bool nullToAbsent) { + return UnknownProfileCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + keys: Value(keys), + lastFetched: Value(lastFetched), + ); + } + + factory UnknownProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UnknownProfileData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + keys: serializer.fromJson(json['keys']), + lastFetched: serializer.fromJson(json['lastFetched']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'keys': serializer.toJson(keys), + 'lastFetched': serializer.toJson(lastFetched), + }; + } + + UnknownProfileData copyWith({ + String? id, + String? name, + String? displayName, + String? keys, + DateTime? lastFetched, + }) => UnknownProfileData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + lastFetched: lastFetched ?? this.lastFetched, + ); + UnknownProfileData copyWithCompanion(UnknownProfileCompanion data) { + return UnknownProfileData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + keys: data.keys.present ? data.keys.value : this.keys, + lastFetched: + data.lastFetched.present ? data.lastFetched.value : this.lastFetched, + ); + } + + @override + String toString() { + return (StringBuffer('UnknownProfileData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('keys: $keys, ') + ..write('lastFetched: $lastFetched') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, displayName, keys, lastFetched); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UnknownProfileData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.keys == this.keys && + other.lastFetched == this.lastFetched); +} + +class UnknownProfileCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value keys; + final Value lastFetched; + final Value rowid; + const UnknownProfileCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.keys = const Value.absent(), + this.lastFetched = const Value.absent(), + this.rowid = const Value.absent(), + }); + UnknownProfileCompanion.insert({ + required String id, + required String name, + required String displayName, + required String keys, + this.lastFetched = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + keys = Value(keys); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? keys, + Expression? lastFetched, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (keys != null) 'keys': keys, + if (lastFetched != null) 'last_fetched': lastFetched, + if (rowid != null) 'rowid': rowid, + }); + } + + UnknownProfileCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? keys, + Value? lastFetched, + Value? rowid, + }) { + return UnknownProfileCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + lastFetched: lastFetched ?? this.lastFetched, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (lastFetched.present) { + map['last_fetched'] = Variable(lastFetched.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UnknownProfileCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('keys: $keys, ') + ..write('lastFetched: $lastFetched, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Profile extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Profile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn pictureContainer = GeneratedColumn( + 'picture_container', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, pictureContainer, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'profile'; + @override + Set get $primaryKey => {id}; + @override + ProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProfileData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + pictureContainer: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}picture_container'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + ); + } + + @override + Profile createAlias(String alias) { + return Profile(attachedDatabase, alias); + } +} + +class ProfileData extends DataClass implements Insertable { + final String id; + final String pictureContainer; + final String data; + const ProfileData({ + required this.id, + required this.pictureContainer, + required this.data, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['picture_container'] = Variable(pictureContainer); + map['data'] = Variable(data); + return map; + } + + ProfileCompanion toCompanion(bool nullToAbsent) { + return ProfileCompanion( + id: Value(id), + pictureContainer: Value(pictureContainer), + data: Value(data), + ); + } + + factory ProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProfileData( + id: serializer.fromJson(json['id']), + pictureContainer: serializer.fromJson(json['pictureContainer']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'pictureContainer': serializer.toJson(pictureContainer), + 'data': serializer.toJson(data), + }; + } + + ProfileData copyWith({String? id, String? pictureContainer, String? data}) => + ProfileData( + id: id ?? this.id, + pictureContainer: pictureContainer ?? this.pictureContainer, + data: data ?? this.data, + ); + ProfileData copyWithCompanion(ProfileCompanion data) { + return ProfileData( + id: data.id.present ? data.id.value : this.id, + pictureContainer: + data.pictureContainer.present + ? data.pictureContainer.value + : this.pictureContainer, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('ProfileData(') + ..write('id: $id, ') + ..write('pictureContainer: $pictureContainer, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, pictureContainer, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProfileData && + other.id == this.id && + other.pictureContainer == this.pictureContainer && + other.data == this.data); +} + +class ProfileCompanion extends UpdateCompanion { + final Value id; + final Value pictureContainer; + final Value data; + final Value rowid; + const ProfileCompanion({ + this.id = const Value.absent(), + this.pictureContainer = const Value.absent(), + this.data = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProfileCompanion.insert({ + required String id, + required String pictureContainer, + required String data, + this.rowid = const Value.absent(), + }) : id = Value(id), + pictureContainer = Value(pictureContainer), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? pictureContainer, + Expression? data, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (pictureContainer != null) 'picture_container': pictureContainer, + if (data != null) 'data': data, + if (rowid != null) 'rowid': rowid, + }); + } + + ProfileCompanion copyWith({ + Value? id, + Value? pictureContainer, + Value? data, + Value? rowid, + }) { + return ProfileCompanion( + id: id ?? this.id, + pictureContainer: pictureContainer ?? this.pictureContainer, + data: data ?? this.data, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (pictureContainer.present) { + map['picture_container'] = Variable(pictureContainer.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProfileCompanion(') + ..write('id: $id, ') + ..write('pictureContainer: $pictureContainer, ') + ..write('data: $data, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class TrustedLink extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrustedLink(this.attachedDatabase, [this._alias]); + late final GeneratedColumn domain = GeneratedColumn( + 'domain', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [domain]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trusted_link'; + @override + Set get $primaryKey => {domain}; + @override + TrustedLinkData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrustedLinkData( + domain: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}domain'], + )!, + ); + } + + @override + TrustedLink createAlias(String alias) { + return TrustedLink(attachedDatabase, alias); + } +} + +class TrustedLinkData extends DataClass implements Insertable { + final String domain; + const TrustedLinkData({required this.domain}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['domain'] = Variable(domain); + return map; + } + + TrustedLinkCompanion toCompanion(bool nullToAbsent) { + return TrustedLinkCompanion(domain: Value(domain)); + } + + factory TrustedLinkData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrustedLinkData(domain: serializer.fromJson(json['domain'])); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return {'domain': serializer.toJson(domain)}; + } + + TrustedLinkData copyWith({String? domain}) => + TrustedLinkData(domain: domain ?? this.domain); + TrustedLinkData copyWithCompanion(TrustedLinkCompanion data) { + return TrustedLinkData( + domain: data.domain.present ? data.domain.value : this.domain, + ); + } + + @override + String toString() { + return (StringBuffer('TrustedLinkData(') + ..write('domain: $domain') + ..write(')')) + .toString(); + } + + @override + int get hashCode => domain.hashCode; + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrustedLinkData && other.domain == this.domain); +} + +class TrustedLinkCompanion extends UpdateCompanion { + final Value domain; + final Value rowid; + const TrustedLinkCompanion({ + this.domain = const Value.absent(), + this.rowid = const Value.absent(), + }); + TrustedLinkCompanion.insert({ + required String domain, + this.rowid = const Value.absent(), + }) : domain = Value(domain); + static Insertable custom({ + Expression? domain, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (domain != null) 'domain': domain, + if (rowid != null) 'rowid': rowid, + }); + } + + TrustedLinkCompanion copyWith({Value? domain, Value? rowid}) { + return TrustedLinkCompanion( + domain: domain ?? this.domain, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (domain.present) { + map['domain'] = Variable(domain.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrustedLinkCompanion(') + ..write('domain: $domain, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class LibraryEntry extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LibraryEntry(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn identifierHash = GeneratedColumn( + 'identifier_hash', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'to-migrate\''), + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + type, + createdAt, + identifierHash, + data, + width, + height, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'library_entry'; + @override + Set get $primaryKey => {id}; + @override + LibraryEntryData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LibraryEntryData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + identifierHash: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}identifier_hash'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + width: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + )!, + height: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + )!, + ); + } + + @override + LibraryEntry createAlias(String alias) { + return LibraryEntry(attachedDatabase, alias); + } +} + +class LibraryEntryData extends DataClass + implements Insertable { + final String id; + final int type; + final BigInt createdAt; + final String identifierHash; + final String data; + final int width; + final int height; + const LibraryEntryData({ + required this.id, + required this.type, + required this.createdAt, + required this.identifierHash, + required this.data, + required this.width, + required this.height, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['identifier_hash'] = Variable(identifierHash); + map['data'] = Variable(data); + map['width'] = Variable(width); + map['height'] = Variable(height); + return map; + } + + LibraryEntryCompanion toCompanion(bool nullToAbsent) { + return LibraryEntryCompanion( + id: Value(id), + type: Value(type), + createdAt: Value(createdAt), + identifierHash: Value(identifierHash), + data: Value(data), + width: Value(width), + height: Value(height), + ); + } + + factory LibraryEntryData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LibraryEntryData( + id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + identifierHash: serializer.fromJson(json['identifierHash']), + data: serializer.fromJson(json['data']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'identifierHash': serializer.toJson(identifierHash), + 'data': serializer.toJson(data), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + }; + } + + LibraryEntryData copyWith({ + String? id, + int? type, + BigInt? createdAt, + String? identifierHash, + String? data, + int? width, + int? height, + }) => LibraryEntryData( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + identifierHash: identifierHash ?? this.identifierHash, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + ); + LibraryEntryData copyWithCompanion(LibraryEntryCompanion data) { + return LibraryEntryData( + id: data.id.present ? data.id.value : this.id, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + identifierHash: + data.identifierHash.present + ? data.identifierHash.value + : this.identifierHash, + data: data.data.present ? data.data.value : this.data, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + ); + } + + @override + String toString() { + return (StringBuffer('LibraryEntryData(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('identifierHash: $identifierHash, ') + ..write('data: $data, ') + ..write('width: $width, ') + ..write('height: $height') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, type, createdAt, identifierHash, data, width, height); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LibraryEntryData && + other.id == this.id && + other.type == this.type && + other.createdAt == this.createdAt && + other.identifierHash == this.identifierHash && + other.data == this.data && + other.width == this.width && + other.height == this.height); +} + +class LibraryEntryCompanion extends UpdateCompanion { + final Value id; + final Value type; + final Value createdAt; + final Value identifierHash; + final Value data; + final Value width; + final Value height; + final Value rowid; + const LibraryEntryCompanion({ + this.id = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.identifierHash = const Value.absent(), + this.data = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.rowid = const Value.absent(), + }); + LibraryEntryCompanion.insert({ + required String id, + required int type, + required BigInt createdAt, + this.identifierHash = const Value.absent(), + required String data, + required int width, + required int height, + this.rowid = const Value.absent(), + }) : id = Value(id), + type = Value(type), + createdAt = Value(createdAt), + data = Value(data), + width = Value(width), + height = Value(height); + static Insertable custom({ + Expression? id, + Expression? type, + Expression? createdAt, + Expression? identifierHash, + Expression? data, + Expression? width, + Expression? height, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (identifierHash != null) 'identifier_hash': identifierHash, + if (data != null) 'data': data, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (rowid != null) 'rowid': rowid, + }); + } + + LibraryEntryCompanion copyWith({ + Value? id, + Value? type, + Value? createdAt, + Value? identifierHash, + Value? data, + Value? width, + Value? height, + Value? rowid, + }) { + return LibraryEntryCompanion( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + identifierHash: identifierHash ?? this.identifierHash, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (identifierHash.present) { + map['identifier_hash'] = Variable(identifierHash.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LibraryEntryCompanion(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('identifierHash: $identifierHash, ') + ..write('data: $data, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV4 extends GeneratedDatabase { + DatabaseAtV4(QueryExecutor e) : super(e); + late final Conversation conversation = Conversation(this); + late final Message message = Message(this); + late final Member member = Member(this); + late final Setting setting = Setting(this); + late final Friend friend = Friend(this); + late final Request request = Request(this); + late final UnknownProfile unknownProfile = UnknownProfile(this); + late final Profile profile = Profile(this); + late final TrustedLink trustedLink = TrustedLink(this); + late final LibraryEntry libraryEntry = LibraryEntry(this); + late final Index idxConversationUpdated = Index( + 'idx_conversation_updated', + 'CREATE INDEX idx_conversation_updated ON conversation (updated_at)', + ); + late final Index idxMessageCreated = Index( + 'idx_message_created', + 'CREATE INDEX idx_message_created ON message (created_at)', + ); + late final Index idxFriendsUpdated = Index( + 'idx_friends_updated', + 'CREATE INDEX idx_friends_updated ON friend (updated_at)', + ); + late final Index idxRequestsUpdated = Index( + 'idx_requests_updated', + 'CREATE INDEX idx_requests_updated ON request (updated_at)', + ); + late final Index idxUnknownProfilesLastFetched = Index( + 'idx_unknown_profiles_last_fetched', + 'CREATE INDEX idx_unknown_profiles_last_fetched ON unknown_profile (last_fetched)', + ); + late final Index idxLibraryEntryCreated = Index( + 'idx_library_entry_created', + 'CREATE INDEX idx_library_entry_created ON library_entry (created_at)', + ); + late final Index idxLibraryEntryIdhash = Index( + 'idx_library_entry_idhash', + 'CREATE INDEX idx_library_entry_idhash ON library_entry (identifier_hash)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + conversation, + message, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + idxConversationUpdated, + idxMessageCreated, + idxFriendsUpdated, + idxRequestsUpdated, + idxUnknownProfilesLastFetched, + idxLibraryEntryCreated, + idxLibraryEntryIdhash, + ]; + @override + int get schemaVersion => 4; +} diff --git a/test/drift/main/generated/schema_v5.dart b/test/drift/main/generated/schema_v5.dart new file mode 100644 index 00000000..7ba1347f --- /dev/null +++ b/test/drift/main/generated/schema_v5.dart @@ -0,0 +1,3172 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class Conversation extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Conversation(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn token = GeneratedColumn( + 'token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn lastVersion = GeneratedColumn( + 'last_version', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn reads = GeneratedColumn( + 'reads', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + @override + List get $columns => [ + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + reads, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'conversation'; + @override + Set get $primaryKey => {id}; + @override + ConversationData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ConversationData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + token: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}token'], + )!, + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + lastVersion: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}last_version'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + reads: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reads'], + )!, + ); + } + + @override + Conversation createAlias(String alias) { + return Conversation(attachedDatabase, alias); + } +} + +class ConversationData extends DataClass + implements Insertable { + final String id; + final String vaultId; + final int type; + final String data; + final String token; + final String key; + final BigInt lastVersion; + final BigInt updatedAt; + final String reads; + const ConversationData({ + required this.id, + required this.vaultId, + required this.type, + required this.data, + required this.token, + required this.key, + required this.lastVersion, + required this.updatedAt, + required this.reads, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['vault_id'] = Variable(vaultId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['token'] = Variable(token); + map['key'] = Variable(key); + map['last_version'] = Variable(lastVersion); + map['updated_at'] = Variable(updatedAt); + map['reads'] = Variable(reads); + return map; + } + + ConversationCompanion toCompanion(bool nullToAbsent) { + return ConversationCompanion( + id: Value(id), + vaultId: Value(vaultId), + type: Value(type), + data: Value(data), + token: Value(token), + key: Value(key), + lastVersion: Value(lastVersion), + updatedAt: Value(updatedAt), + reads: Value(reads), + ); + } + + factory ConversationData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ConversationData( + id: serializer.fromJson(json['id']), + vaultId: serializer.fromJson(json['vaultId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + token: serializer.fromJson(json['token']), + key: serializer.fromJson(json['key']), + lastVersion: serializer.fromJson(json['lastVersion']), + updatedAt: serializer.fromJson(json['updatedAt']), + reads: serializer.fromJson(json['reads']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'vaultId': serializer.toJson(vaultId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'token': serializer.toJson(token), + 'key': serializer.toJson(key), + 'lastVersion': serializer.toJson(lastVersion), + 'updatedAt': serializer.toJson(updatedAt), + 'reads': serializer.toJson(reads), + }; + } + + ConversationData copyWith({ + String? id, + String? vaultId, + int? type, + String? data, + String? token, + String? key, + BigInt? lastVersion, + BigInt? updatedAt, + String? reads, + }) => ConversationData( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + reads: reads ?? this.reads, + ); + ConversationData copyWithCompanion(ConversationCompanion data) { + return ConversationData( + id: data.id.present ? data.id.value : this.id, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + token: data.token.present ? data.token.value : this.token, + key: data.key.present ? data.key.value : this.key, + lastVersion: + data.lastVersion.present ? data.lastVersion.value : this.lastVersion, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + reads: data.reads.present ? data.reads.value : this.reads, + ); + } + + @override + String toString() { + return (StringBuffer('ConversationData(') + ..write('id: $id, ') + ..write('vaultId: $vaultId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('token: $token, ') + ..write('key: $key, ') + ..write('lastVersion: $lastVersion, ') + ..write('updatedAt: $updatedAt, ') + ..write('reads: $reads') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + vaultId, + type, + data, + token, + key, + lastVersion, + updatedAt, + reads, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ConversationData && + other.id == this.id && + other.vaultId == this.vaultId && + other.type == this.type && + other.data == this.data && + other.token == this.token && + other.key == this.key && + other.lastVersion == this.lastVersion && + other.updatedAt == this.updatedAt && + other.reads == this.reads); +} + +class ConversationCompanion extends UpdateCompanion { + final Value id; + final Value vaultId; + final Value type; + final Value data; + final Value token; + final Value key; + final Value lastVersion; + final Value updatedAt; + final Value reads; + final Value rowid; + const ConversationCompanion({ + this.id = const Value.absent(), + this.vaultId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.token = const Value.absent(), + this.key = const Value.absent(), + this.lastVersion = const Value.absent(), + this.updatedAt = const Value.absent(), + this.reads = const Value.absent(), + this.rowid = const Value.absent(), + }); + ConversationCompanion.insert({ + required String id, + required String vaultId, + required int type, + required String data, + required String token, + required String key, + required BigInt lastVersion, + required BigInt updatedAt, + this.reads = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + vaultId = Value(vaultId), + type = Value(type), + data = Value(data), + token = Value(token), + key = Value(key), + lastVersion = Value(lastVersion), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? vaultId, + Expression? type, + Expression? data, + Expression? token, + Expression? key, + Expression? lastVersion, + Expression? updatedAt, + Expression? reads, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (vaultId != null) 'vault_id': vaultId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (token != null) 'token': token, + if (key != null) 'key': key, + if (lastVersion != null) 'last_version': lastVersion, + if (updatedAt != null) 'updated_at': updatedAt, + if (reads != null) 'reads': reads, + if (rowid != null) 'rowid': rowid, + }); + } + + ConversationCompanion copyWith({ + Value? id, + Value? vaultId, + Value? type, + Value? data, + Value? token, + Value? key, + Value? lastVersion, + Value? updatedAt, + Value? reads, + Value? rowid, + }) { + return ConversationCompanion( + id: id ?? this.id, + vaultId: vaultId ?? this.vaultId, + type: type ?? this.type, + data: data ?? this.data, + token: token ?? this.token, + key: key ?? this.key, + lastVersion: lastVersion ?? this.lastVersion, + updatedAt: updatedAt ?? this.updatedAt, + reads: reads ?? this.reads, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (token.present) { + map['token'] = Variable(token.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (lastVersion.present) { + map['last_version'] = Variable(lastVersion.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (reads.present) { + map['reads'] = Variable(reads.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ConversationCompanion(') + ..write('id: $id, ') + ..write('vaultId: $vaultId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('token: $token, ') + ..write('key: $key, ') + ..write('lastVersion: $lastVersion, ') + ..write('updatedAt: $updatedAt, ') + ..write('reads: $reads, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Message extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Message(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn content = GeneratedColumn( + 'content', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn senderToken = GeneratedColumn( + 'sender_token', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn senderAddress = GeneratedColumn( + 'sender_address', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn conversation = GeneratedColumn( + 'conversation', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn edited = GeneratedColumn( + 'edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("edited" IN (0, 1))', + ), + ); + late final GeneratedColumn verified = GeneratedColumn( + 'verified', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("verified" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message'; + @override + Set get $primaryKey => {id}; + @override + MessageData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + content: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + )!, + senderToken: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_token'], + )!, + senderAddress: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_address'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + conversation: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation'], + )!, + edited: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}edited'], + )!, + verified: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}verified'], + )!, + ); + } + + @override + Message createAlias(String alias) { + return Message(attachedDatabase, alias); + } +} + +class MessageData extends DataClass implements Insertable { + final String id; + final String content; + final String senderToken; + final String senderAddress; + final BigInt createdAt; + final String conversation; + final bool edited; + final bool verified; + const MessageData({ + required this.id, + required this.content, + required this.senderToken, + required this.senderAddress, + required this.createdAt, + required this.conversation, + required this.edited, + required this.verified, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['content'] = Variable(content); + map['sender_token'] = Variable(senderToken); + map['sender_address'] = Variable(senderAddress); + map['created_at'] = Variable(createdAt); + map['conversation'] = Variable(conversation); + map['edited'] = Variable(edited); + map['verified'] = Variable(verified); + return map; + } + + MessageCompanion toCompanion(bool nullToAbsent) { + return MessageCompanion( + id: Value(id), + content: Value(content), + senderToken: Value(senderToken), + senderAddress: Value(senderAddress), + createdAt: Value(createdAt), + conversation: Value(conversation), + edited: Value(edited), + verified: Value(verified), + ); + } + + factory MessageData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageData( + id: serializer.fromJson(json['id']), + content: serializer.fromJson(json['content']), + senderToken: serializer.fromJson(json['senderToken']), + senderAddress: serializer.fromJson(json['senderAddress']), + createdAt: serializer.fromJson(json['createdAt']), + conversation: serializer.fromJson(json['conversation']), + edited: serializer.fromJson(json['edited']), + verified: serializer.fromJson(json['verified']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'content': serializer.toJson(content), + 'senderToken': serializer.toJson(senderToken), + 'senderAddress': serializer.toJson(senderAddress), + 'createdAt': serializer.toJson(createdAt), + 'conversation': serializer.toJson(conversation), + 'edited': serializer.toJson(edited), + 'verified': serializer.toJson(verified), + }; + } + + MessageData copyWith({ + String? id, + String? content, + String? senderToken, + String? senderAddress, + BigInt? createdAt, + String? conversation, + bool? edited, + bool? verified, + }) => MessageData( + id: id ?? this.id, + content: content ?? this.content, + senderToken: senderToken ?? this.senderToken, + senderAddress: senderAddress ?? this.senderAddress, + createdAt: createdAt ?? this.createdAt, + conversation: conversation ?? this.conversation, + edited: edited ?? this.edited, + verified: verified ?? this.verified, + ); + MessageData copyWithCompanion(MessageCompanion data) { + return MessageData( + id: data.id.present ? data.id.value : this.id, + content: data.content.present ? data.content.value : this.content, + senderToken: + data.senderToken.present ? data.senderToken.value : this.senderToken, + senderAddress: + data.senderAddress.present + ? data.senderAddress.value + : this.senderAddress, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + conversation: + data.conversation.present + ? data.conversation.value + : this.conversation, + edited: data.edited.present ? data.edited.value : this.edited, + verified: data.verified.present ? data.verified.value : this.verified, + ); + } + + @override + String toString() { + return (StringBuffer('MessageData(') + ..write('id: $id, ') + ..write('content: $content, ') + ..write('senderToken: $senderToken, ') + ..write('senderAddress: $senderAddress, ') + ..write('createdAt: $createdAt, ') + ..write('conversation: $conversation, ') + ..write('edited: $edited, ') + ..write('verified: $verified') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + content, + senderToken, + senderAddress, + createdAt, + conversation, + edited, + verified, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageData && + other.id == this.id && + other.content == this.content && + other.senderToken == this.senderToken && + other.senderAddress == this.senderAddress && + other.createdAt == this.createdAt && + other.conversation == this.conversation && + other.edited == this.edited && + other.verified == this.verified); +} + +class MessageCompanion extends UpdateCompanion { + final Value id; + final Value content; + final Value senderToken; + final Value senderAddress; + final Value createdAt; + final Value conversation; + final Value edited; + final Value verified; + final Value rowid; + const MessageCompanion({ + this.id = const Value.absent(), + this.content = const Value.absent(), + this.senderToken = const Value.absent(), + this.senderAddress = const Value.absent(), + this.createdAt = const Value.absent(), + this.conversation = const Value.absent(), + this.edited = const Value.absent(), + this.verified = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessageCompanion.insert({ + required String id, + required String content, + required String senderToken, + required String senderAddress, + required BigInt createdAt, + required String conversation, + required bool edited, + required bool verified, + this.rowid = const Value.absent(), + }) : id = Value(id), + content = Value(content), + senderToken = Value(senderToken), + senderAddress = Value(senderAddress), + createdAt = Value(createdAt), + conversation = Value(conversation), + edited = Value(edited), + verified = Value(verified); + static Insertable custom({ + Expression? id, + Expression? content, + Expression? senderToken, + Expression? senderAddress, + Expression? createdAt, + Expression? conversation, + Expression? edited, + Expression? verified, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (content != null) 'content': content, + if (senderToken != null) 'sender_token': senderToken, + if (senderAddress != null) 'sender_address': senderAddress, + if (createdAt != null) 'created_at': createdAt, + if (conversation != null) 'conversation': conversation, + if (edited != null) 'edited': edited, + if (verified != null) 'verified': verified, + if (rowid != null) 'rowid': rowid, + }); + } + + MessageCompanion copyWith({ + Value? id, + Value? content, + Value? senderToken, + Value? senderAddress, + Value? createdAt, + Value? conversation, + Value? edited, + Value? verified, + Value? rowid, + }) { + return MessageCompanion( + id: id ?? this.id, + content: content ?? this.content, + senderToken: senderToken ?? this.senderToken, + senderAddress: senderAddress ?? this.senderAddress, + createdAt: createdAt ?? this.createdAt, + conversation: conversation ?? this.conversation, + edited: edited ?? this.edited, + verified: verified ?? this.verified, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (senderToken.present) { + map['sender_token'] = Variable(senderToken.value); + } + if (senderAddress.present) { + map['sender_address'] = Variable(senderAddress.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (conversation.present) { + map['conversation'] = Variable(conversation.value); + } + if (edited.present) { + map['edited'] = Variable(edited.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageCompanion(') + ..write('id: $id, ') + ..write('content: $content, ') + ..write('senderToken: $senderToken, ') + ..write('senderAddress: $senderAddress, ') + ..write('createdAt: $createdAt, ') + ..write('conversation: $conversation, ') + ..write('edited: $edited, ') + ..write('verified: $verified, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Member extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Member(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn conversationId = GeneratedColumn( + 'conversation_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn roleId = GeneratedColumn( + 'role_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, conversationId, accountId, roleId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'member'; + @override + Set get $primaryKey => {id}; + @override + MemberData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemberData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + conversationId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conversation_id'], + ), + accountId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + roleId: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role_id'], + )!, + ); + } + + @override + Member createAlias(String alias) { + return Member(attachedDatabase, alias); + } +} + +class MemberData extends DataClass implements Insertable { + final String id; + final String? conversationId; + final String accountId; + final int roleId; + const MemberData({ + required this.id, + this.conversationId, + required this.accountId, + required this.roleId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || conversationId != null) { + map['conversation_id'] = Variable(conversationId); + } + map['account_id'] = Variable(accountId); + map['role_id'] = Variable(roleId); + return map; + } + + MemberCompanion toCompanion(bool nullToAbsent) { + return MemberCompanion( + id: Value(id), + conversationId: + conversationId == null && nullToAbsent + ? const Value.absent() + : Value(conversationId), + accountId: Value(accountId), + roleId: Value(roleId), + ); + } + + factory MemberData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemberData( + id: serializer.fromJson(json['id']), + conversationId: serializer.fromJson(json['conversationId']), + accountId: serializer.fromJson(json['accountId']), + roleId: serializer.fromJson(json['roleId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'conversationId': serializer.toJson(conversationId), + 'accountId': serializer.toJson(accountId), + 'roleId': serializer.toJson(roleId), + }; + } + + MemberData copyWith({ + String? id, + Value conversationId = const Value.absent(), + String? accountId, + int? roleId, + }) => MemberData( + id: id ?? this.id, + conversationId: + conversationId.present ? conversationId.value : this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + ); + MemberData copyWithCompanion(MemberCompanion data) { + return MemberData( + id: data.id.present ? data.id.value : this.id, + conversationId: + data.conversationId.present + ? data.conversationId.value + : this.conversationId, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + roleId: data.roleId.present ? data.roleId.value : this.roleId, + ); + } + + @override + String toString() { + return (StringBuffer('MemberData(') + ..write('id: $id, ') + ..write('conversationId: $conversationId, ') + ..write('accountId: $accountId, ') + ..write('roleId: $roleId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, conversationId, accountId, roleId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemberData && + other.id == this.id && + other.conversationId == this.conversationId && + other.accountId == this.accountId && + other.roleId == this.roleId); +} + +class MemberCompanion extends UpdateCompanion { + final Value id; + final Value conversationId; + final Value accountId; + final Value roleId; + final Value rowid; + const MemberCompanion({ + this.id = const Value.absent(), + this.conversationId = const Value.absent(), + this.accountId = const Value.absent(), + this.roleId = const Value.absent(), + this.rowid = const Value.absent(), + }); + MemberCompanion.insert({ + required String id, + this.conversationId = const Value.absent(), + required String accountId, + required int roleId, + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + roleId = Value(roleId); + static Insertable custom({ + Expression? id, + Expression? conversationId, + Expression? accountId, + Expression? roleId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (conversationId != null) 'conversation_id': conversationId, + if (accountId != null) 'account_id': accountId, + if (roleId != null) 'role_id': roleId, + if (rowid != null) 'rowid': rowid, + }); + } + + MemberCompanion copyWith({ + Value? id, + Value? conversationId, + Value? accountId, + Value? roleId, + Value? rowid, + }) { + return MemberCompanion( + id: id ?? this.id, + conversationId: conversationId ?? this.conversationId, + accountId: accountId ?? this.accountId, + roleId: roleId ?? this.roleId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (conversationId.present) { + map['conversation_id'] = Variable(conversationId.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (roleId.present) { + map['role_id'] = Variable(roleId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemberCompanion(') + ..write('id: $id, ') + ..write('conversationId: $conversationId, ') + ..write('accountId: $accountId, ') + ..write('roleId: $roleId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Setting extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Setting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'setting'; + @override + Set get $primaryKey => {key}; + @override + SettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingData( + key: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + Setting createAlias(String alias) { + return Setting(attachedDatabase, alias); + } +} + +class SettingData extends DataClass implements Insertable { + final String key; + final String value; + const SettingData({required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + SettingCompanion toCompanion(bool nullToAbsent) { + return SettingCompanion(key: Value(key), value: Value(value)); + } + + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingData( + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + SettingData copyWith({String? key, String? value}) => + SettingData(key: key ?? this.key, value: value ?? this.value); + SettingData copyWithCompanion(SettingCompanion data) { + return SettingData( + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingData(') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingData && + other.key == this.key && + other.value == this.value); +} + +class SettingCompanion extends UpdateCompanion { + final Value key; + final Value value; + final Value rowid; + const SettingCompanion({ + this.key = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingCompanion.insert({ + required String key, + required String value, + this.rowid = const Value.absent(), + }) : key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? key, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingCompanion copyWith({ + Value? key, + Value? value, + Value? rowid, + }) { + return SettingCompanion( + key: key ?? this.key, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingCompanion(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Friend extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Friend(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + displayName, + vaultId, + keys, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'friend'; + @override + Set get $primaryKey => {id}; + @override + FriendData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return FriendData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + Friend createAlias(String alias) { + return Friend(attachedDatabase, alias); + } +} + +class FriendData extends DataClass implements Insertable { + final String id; + final String name; + final String displayName; + final String vaultId; + final String keys; + final BigInt updatedAt; + const FriendData({ + required this.id, + required this.name, + required this.displayName, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['vault_id'] = Variable(vaultId); + map['keys'] = Variable(keys); + map['updated_at'] = Variable(updatedAt); + return map; + } + + FriendCompanion toCompanion(bool nullToAbsent) { + return FriendCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + vaultId: Value(vaultId), + keys: Value(keys), + updatedAt: Value(updatedAt), + ); + } + + factory FriendData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return FriendData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + vaultId: serializer.fromJson(json['vaultId']), + keys: serializer.fromJson(json['keys']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'vaultId': serializer.toJson(vaultId), + 'keys': serializer.toJson(keys), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + FriendData copyWith({ + String? id, + String? name, + String? displayName, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => FriendData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); + FriendData copyWithCompanion(FriendCompanion data) { + return FriendData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + keys: data.keys.present ? data.keys.value : this.keys, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('FriendData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, displayName, vaultId, keys, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is FriendData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.vaultId == this.vaultId && + other.keys == this.keys && + other.updatedAt == this.updatedAt); +} + +class FriendCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value vaultId; + final Value keys; + final Value updatedAt; + final Value rowid; + const FriendCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.vaultId = const Value.absent(), + this.keys = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + FriendCompanion.insert({ + required String id, + required String name, + required String displayName, + required String vaultId, + required String keys, + required BigInt updatedAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? vaultId, + Expression? keys, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (vaultId != null) 'vault_id': vaultId, + if (keys != null) 'keys': keys, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + FriendCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { + return FriendCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('FriendCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Request extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Request(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn self = GeneratedColumn( + 'self', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("self" IN (0, 1))', + ), + ); + late final GeneratedColumn vaultId = GeneratedColumn( + 'vault_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + displayName, + self, + vaultId, + keys, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'request'; + @override + Set get $primaryKey => {id}; + @override + RequestData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RequestData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + self: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}self'], + )!, + vaultId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}vault_id'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + updatedAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + Request createAlias(String alias) { + return Request(attachedDatabase, alias); + } +} + +class RequestData extends DataClass implements Insertable { + final String id; + final String name; + final String displayName; + final bool self; + final String vaultId; + final String keys; + final BigInt updatedAt; + const RequestData({ + required this.id, + required this.name, + required this.displayName, + required this.self, + required this.vaultId, + required this.keys, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['self'] = Variable(self); + map['vault_id'] = Variable(vaultId); + map['keys'] = Variable(keys); + map['updated_at'] = Variable(updatedAt); + return map; + } + + RequestCompanion toCompanion(bool nullToAbsent) { + return RequestCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + self: Value(self), + vaultId: Value(vaultId), + keys: Value(keys), + updatedAt: Value(updatedAt), + ); + } + + factory RequestData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RequestData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + self: serializer.fromJson(json['self']), + vaultId: serializer.fromJson(json['vaultId']), + keys: serializer.fromJson(json['keys']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'self': serializer.toJson(self), + 'vaultId': serializer.toJson(vaultId), + 'keys': serializer.toJson(keys), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + RequestData copyWith({ + String? id, + String? name, + String? displayName, + bool? self, + String? vaultId, + String? keys, + BigInt? updatedAt, + }) => RequestData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + ); + RequestData copyWithCompanion(RequestCompanion data) { + return RequestData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + self: data.self.present ? data.self.value : this.self, + vaultId: data.vaultId.present ? data.vaultId.value : this.vaultId, + keys: data.keys.present ? data.keys.value : this.keys, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('RequestData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('self: $self, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, displayName, self, vaultId, keys, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RequestData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.self == this.self && + other.vaultId == this.vaultId && + other.keys == this.keys && + other.updatedAt == this.updatedAt); +} + +class RequestCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value self; + final Value vaultId; + final Value keys; + final Value updatedAt; + final Value rowid; + const RequestCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.self = const Value.absent(), + this.vaultId = const Value.absent(), + this.keys = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + RequestCompanion.insert({ + required String id, + required String name, + required String displayName, + required bool self, + required String vaultId, + required String keys, + required BigInt updatedAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + self = Value(self), + vaultId = Value(vaultId), + keys = Value(keys), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? self, + Expression? vaultId, + Expression? keys, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (self != null) 'self': self, + if (vaultId != null) 'vault_id': vaultId, + if (keys != null) 'keys': keys, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + RequestCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? self, + Value? vaultId, + Value? keys, + Value? updatedAt, + Value? rowid, + }) { + return RequestCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + self: self ?? this.self, + vaultId: vaultId ?? this.vaultId, + keys: keys ?? this.keys, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (self.present) { + map['self'] = Variable(self.value); + } + if (vaultId.present) { + map['vault_id'] = Variable(vaultId.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RequestCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('self: $self, ') + ..write('vaultId: $vaultId, ') + ..write('keys: $keys, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class UnknownProfile extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UnknownProfile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn keys = GeneratedColumn( + 'keys', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn lastFetched = GeneratedColumn( + 'last_fetched', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + displayName, + keys, + lastFetched, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'unknown_profile'; + @override + Set get $primaryKey => {id}; + @override + UnknownProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UnknownProfileData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + displayName: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + keys: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}keys'], + )!, + lastFetched: + attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_fetched'], + )!, + ); + } + + @override + UnknownProfile createAlias(String alias) { + return UnknownProfile(attachedDatabase, alias); + } +} + +class UnknownProfileData extends DataClass + implements Insertable { + final String id; + final String name; + final String displayName; + final String keys; + final DateTime lastFetched; + const UnknownProfileData({ + required this.id, + required this.name, + required this.displayName, + required this.keys, + required this.lastFetched, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['display_name'] = Variable(displayName); + map['keys'] = Variable(keys); + map['last_fetched'] = Variable(lastFetched); + return map; + } + + UnknownProfileCompanion toCompanion(bool nullToAbsent) { + return UnknownProfileCompanion( + id: Value(id), + name: Value(name), + displayName: Value(displayName), + keys: Value(keys), + lastFetched: Value(lastFetched), + ); + } + + factory UnknownProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UnknownProfileData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + displayName: serializer.fromJson(json['displayName']), + keys: serializer.fromJson(json['keys']), + lastFetched: serializer.fromJson(json['lastFetched']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'displayName': serializer.toJson(displayName), + 'keys': serializer.toJson(keys), + 'lastFetched': serializer.toJson(lastFetched), + }; + } + + UnknownProfileData copyWith({ + String? id, + String? name, + String? displayName, + String? keys, + DateTime? lastFetched, + }) => UnknownProfileData( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + lastFetched: lastFetched ?? this.lastFetched, + ); + UnknownProfileData copyWithCompanion(UnknownProfileCompanion data) { + return UnknownProfileData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + keys: data.keys.present ? data.keys.value : this.keys, + lastFetched: + data.lastFetched.present ? data.lastFetched.value : this.lastFetched, + ); + } + + @override + String toString() { + return (StringBuffer('UnknownProfileData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('keys: $keys, ') + ..write('lastFetched: $lastFetched') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, displayName, keys, lastFetched); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UnknownProfileData && + other.id == this.id && + other.name == this.name && + other.displayName == this.displayName && + other.keys == this.keys && + other.lastFetched == this.lastFetched); +} + +class UnknownProfileCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value displayName; + final Value keys; + final Value lastFetched; + final Value rowid; + const UnknownProfileCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.displayName = const Value.absent(), + this.keys = const Value.absent(), + this.lastFetched = const Value.absent(), + this.rowid = const Value.absent(), + }); + UnknownProfileCompanion.insert({ + required String id, + required String name, + required String displayName, + required String keys, + this.lastFetched = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + displayName = Value(displayName), + keys = Value(keys); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? displayName, + Expression? keys, + Expression? lastFetched, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (displayName != null) 'display_name': displayName, + if (keys != null) 'keys': keys, + if (lastFetched != null) 'last_fetched': lastFetched, + if (rowid != null) 'rowid': rowid, + }); + } + + UnknownProfileCompanion copyWith({ + Value? id, + Value? name, + Value? displayName, + Value? keys, + Value? lastFetched, + Value? rowid, + }) { + return UnknownProfileCompanion( + id: id ?? this.id, + name: name ?? this.name, + displayName: displayName ?? this.displayName, + keys: keys ?? this.keys, + lastFetched: lastFetched ?? this.lastFetched, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (keys.present) { + map['keys'] = Variable(keys.value); + } + if (lastFetched.present) { + map['last_fetched'] = Variable(lastFetched.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UnknownProfileCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('displayName: $displayName, ') + ..write('keys: $keys, ') + ..write('lastFetched: $lastFetched, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Profile extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Profile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn pictureContainer = GeneratedColumn( + 'picture_container', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, pictureContainer, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'profile'; + @override + Set get $primaryKey => {id}; + @override + ProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProfileData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + pictureContainer: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}picture_container'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + ); + } + + @override + Profile createAlias(String alias) { + return Profile(attachedDatabase, alias); + } +} + +class ProfileData extends DataClass implements Insertable { + final String id; + final String pictureContainer; + final String data; + const ProfileData({ + required this.id, + required this.pictureContainer, + required this.data, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['picture_container'] = Variable(pictureContainer); + map['data'] = Variable(data); + return map; + } + + ProfileCompanion toCompanion(bool nullToAbsent) { + return ProfileCompanion( + id: Value(id), + pictureContainer: Value(pictureContainer), + data: Value(data), + ); + } + + factory ProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProfileData( + id: serializer.fromJson(json['id']), + pictureContainer: serializer.fromJson(json['pictureContainer']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'pictureContainer': serializer.toJson(pictureContainer), + 'data': serializer.toJson(data), + }; + } + + ProfileData copyWith({String? id, String? pictureContainer, String? data}) => + ProfileData( + id: id ?? this.id, + pictureContainer: pictureContainer ?? this.pictureContainer, + data: data ?? this.data, + ); + ProfileData copyWithCompanion(ProfileCompanion data) { + return ProfileData( + id: data.id.present ? data.id.value : this.id, + pictureContainer: + data.pictureContainer.present + ? data.pictureContainer.value + : this.pictureContainer, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('ProfileData(') + ..write('id: $id, ') + ..write('pictureContainer: $pictureContainer, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, pictureContainer, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProfileData && + other.id == this.id && + other.pictureContainer == this.pictureContainer && + other.data == this.data); +} + +class ProfileCompanion extends UpdateCompanion { + final Value id; + final Value pictureContainer; + final Value data; + final Value rowid; + const ProfileCompanion({ + this.id = const Value.absent(), + this.pictureContainer = const Value.absent(), + this.data = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProfileCompanion.insert({ + required String id, + required String pictureContainer, + required String data, + this.rowid = const Value.absent(), + }) : id = Value(id), + pictureContainer = Value(pictureContainer), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? pictureContainer, + Expression? data, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (pictureContainer != null) 'picture_container': pictureContainer, + if (data != null) 'data': data, + if (rowid != null) 'rowid': rowid, + }); + } + + ProfileCompanion copyWith({ + Value? id, + Value? pictureContainer, + Value? data, + Value? rowid, + }) { + return ProfileCompanion( + id: id ?? this.id, + pictureContainer: pictureContainer ?? this.pictureContainer, + data: data ?? this.data, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (pictureContainer.present) { + map['picture_container'] = Variable(pictureContainer.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProfileCompanion(') + ..write('id: $id, ') + ..write('pictureContainer: $pictureContainer, ') + ..write('data: $data, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class TrustedLink extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrustedLink(this.attachedDatabase, [this._alias]); + late final GeneratedColumn domain = GeneratedColumn( + 'domain', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [domain]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trusted_link'; + @override + Set get $primaryKey => {domain}; + @override + TrustedLinkData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrustedLinkData( + domain: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}domain'], + )!, + ); + } + + @override + TrustedLink createAlias(String alias) { + return TrustedLink(attachedDatabase, alias); + } +} + +class TrustedLinkData extends DataClass implements Insertable { + final String domain; + const TrustedLinkData({required this.domain}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['domain'] = Variable(domain); + return map; + } + + TrustedLinkCompanion toCompanion(bool nullToAbsent) { + return TrustedLinkCompanion(domain: Value(domain)); + } + + factory TrustedLinkData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrustedLinkData(domain: serializer.fromJson(json['domain'])); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return {'domain': serializer.toJson(domain)}; + } + + TrustedLinkData copyWith({String? domain}) => + TrustedLinkData(domain: domain ?? this.domain); + TrustedLinkData copyWithCompanion(TrustedLinkCompanion data) { + return TrustedLinkData( + domain: data.domain.present ? data.domain.value : this.domain, + ); + } + + @override + String toString() { + return (StringBuffer('TrustedLinkData(') + ..write('domain: $domain') + ..write(')')) + .toString(); + } + + @override + int get hashCode => domain.hashCode; + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrustedLinkData && other.domain == this.domain); +} + +class TrustedLinkCompanion extends UpdateCompanion { + final Value domain; + final Value rowid; + const TrustedLinkCompanion({ + this.domain = const Value.absent(), + this.rowid = const Value.absent(), + }); + TrustedLinkCompanion.insert({ + required String domain, + this.rowid = const Value.absent(), + }) : domain = Value(domain); + static Insertable custom({ + Expression? domain, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (domain != null) 'domain': domain, + if (rowid != null) 'rowid': rowid, + }); + } + + TrustedLinkCompanion copyWith({Value? domain, Value? rowid}) { + return TrustedLinkCompanion( + domain: domain ?? this.domain, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (domain.present) { + map['domain'] = Variable(domain.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrustedLinkCompanion(') + ..write('domain: $domain, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class LibraryEntry extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LibraryEntry(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + ); + late final GeneratedColumn identifierHash = GeneratedColumn( + 'identifier_hash', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'to-migrate\''), + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + type, + createdAt, + identifierHash, + data, + width, + height, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'library_entry'; + @override + Set get $primaryKey => {id}; + @override + LibraryEntryData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LibraryEntryData( + id: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: + attachedDatabase.typeMapping.read( + DriftSqlType.bigInt, + data['${effectivePrefix}created_at'], + )!, + identifierHash: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}identifier_hash'], + )!, + data: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + width: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + )!, + height: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + )!, + ); + } + + @override + LibraryEntry createAlias(String alias) { + return LibraryEntry(attachedDatabase, alias); + } +} + +class LibraryEntryData extends DataClass + implements Insertable { + final String id; + final int type; + final BigInt createdAt; + final String identifierHash; + final String data; + final int width; + final int height; + const LibraryEntryData({ + required this.id, + required this.type, + required this.createdAt, + required this.identifierHash, + required this.data, + required this.width, + required this.height, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['identifier_hash'] = Variable(identifierHash); + map['data'] = Variable(data); + map['width'] = Variable(width); + map['height'] = Variable(height); + return map; + } + + LibraryEntryCompanion toCompanion(bool nullToAbsent) { + return LibraryEntryCompanion( + id: Value(id), + type: Value(type), + createdAt: Value(createdAt), + identifierHash: Value(identifierHash), + data: Value(data), + width: Value(width), + height: Value(height), + ); + } + + factory LibraryEntryData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LibraryEntryData( + id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + identifierHash: serializer.fromJson(json['identifierHash']), + data: serializer.fromJson(json['data']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'identifierHash': serializer.toJson(identifierHash), + 'data': serializer.toJson(data), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + }; + } + + LibraryEntryData copyWith({ + String? id, + int? type, + BigInt? createdAt, + String? identifierHash, + String? data, + int? width, + int? height, + }) => LibraryEntryData( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + identifierHash: identifierHash ?? this.identifierHash, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + ); + LibraryEntryData copyWithCompanion(LibraryEntryCompanion data) { + return LibraryEntryData( + id: data.id.present ? data.id.value : this.id, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + identifierHash: + data.identifierHash.present + ? data.identifierHash.value + : this.identifierHash, + data: data.data.present ? data.data.value : this.data, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + ); + } + + @override + String toString() { + return (StringBuffer('LibraryEntryData(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('identifierHash: $identifierHash, ') + ..write('data: $data, ') + ..write('width: $width, ') + ..write('height: $height') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, type, createdAt, identifierHash, data, width, height); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LibraryEntryData && + other.id == this.id && + other.type == this.type && + other.createdAt == this.createdAt && + other.identifierHash == this.identifierHash && + other.data == this.data && + other.width == this.width && + other.height == this.height); +} + +class LibraryEntryCompanion extends UpdateCompanion { + final Value id; + final Value type; + final Value createdAt; + final Value identifierHash; + final Value data; + final Value width; + final Value height; + final Value rowid; + const LibraryEntryCompanion({ + this.id = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.identifierHash = const Value.absent(), + this.data = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.rowid = const Value.absent(), + }); + LibraryEntryCompanion.insert({ + required String id, + required int type, + required BigInt createdAt, + this.identifierHash = const Value.absent(), + required String data, + required int width, + required int height, + this.rowid = const Value.absent(), + }) : id = Value(id), + type = Value(type), + createdAt = Value(createdAt), + data = Value(data), + width = Value(width), + height = Value(height); + static Insertable custom({ + Expression? id, + Expression? type, + Expression? createdAt, + Expression? identifierHash, + Expression? data, + Expression? width, + Expression? height, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (identifierHash != null) 'identifier_hash': identifierHash, + if (data != null) 'data': data, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (rowid != null) 'rowid': rowid, + }); + } + + LibraryEntryCompanion copyWith({ + Value? id, + Value? type, + Value? createdAt, + Value? identifierHash, + Value? data, + Value? width, + Value? height, + Value? rowid, + }) { + return LibraryEntryCompanion( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + identifierHash: identifierHash ?? this.identifierHash, + data: data ?? this.data, + width: width ?? this.width, + height: height ?? this.height, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (identifierHash.present) { + map['identifier_hash'] = Variable(identifierHash.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LibraryEntryCompanion(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('identifierHash: $identifierHash, ') + ..write('data: $data, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV5 extends GeneratedDatabase { + DatabaseAtV5(QueryExecutor e) : super(e); + late final Conversation conversation = Conversation(this); + late final Message message = Message(this); + late final Member member = Member(this); + late final Setting setting = Setting(this); + late final Friend friend = Friend(this); + late final Request request = Request(this); + late final UnknownProfile unknownProfile = UnknownProfile(this); + late final Profile profile = Profile(this); + late final TrustedLink trustedLink = TrustedLink(this); + late final LibraryEntry libraryEntry = LibraryEntry(this); + late final Index idxConversationUpdated = Index( + 'idx_conversation_updated', + 'CREATE INDEX idx_conversation_updated ON conversation (updated_at)', + ); + late final Index idxMessageCreated = Index( + 'idx_message_created', + 'CREATE INDEX idx_message_created ON message (created_at)', + ); + late final Index idxFriendsUpdated = Index( + 'idx_friends_updated', + 'CREATE INDEX idx_friends_updated ON friend (updated_at)', + ); + late final Index idxRequestsUpdated = Index( + 'idx_requests_updated', + 'CREATE INDEX idx_requests_updated ON request (updated_at)', + ); + late final Index idxUnknownProfilesLastFetched = Index( + 'idx_unknown_profiles_last_fetched', + 'CREATE INDEX idx_unknown_profiles_last_fetched ON unknown_profile (last_fetched)', + ); + late final Index idxLibraryEntryCreated = Index( + 'idx_library_entry_created', + 'CREATE INDEX idx_library_entry_created ON library_entry (created_at)', + ); + late final Index idxLibraryEntryIdhash = Index( + 'idx_library_entry_idhash', + 'CREATE INDEX idx_library_entry_idhash ON library_entry (identifier_hash)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + conversation, + message, + member, + setting, + friend, + request, + unknownProfile, + profile, + trustedLink, + libraryEntry, + idxConversationUpdated, + idxMessageCreated, + idxFriendsUpdated, + idxRequestsUpdated, + idxUnknownProfilesLastFetched, + idxLibraryEntryCreated, + idxLibraryEntryIdhash, + ]; + @override + int get schemaVersion => 5; +} diff --git a/test/messages/message_formatter_test.dart b/test/messages/message_formatter_test.dart new file mode 100644 index 00000000..c132f258 --- /dev/null +++ b/test/messages/message_formatter_test.dart @@ -0,0 +1,451 @@ +import 'package:chat_interface/pages/chat/messages/message_formatter.dart'; +import 'package:chat_interface/util/logging_framework.dart'; +import 'package:flutter/rendering.dart'; +import 'package:test/test.dart'; + +void main() { + group("message formatter expectations", () { + group("normal text", () { + test("should keep text unchanged", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("hello world", TextStyle(fontSize: 14)); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("hello world")); + expect(spans[0].style!, equals(TextStyle(fontSize: 14.0))); + }); + }); + + group("bold formatting", () { + test("should apply bold formatting", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**hello world**", TextStyle(fontSize: 14)); + + expect(spans.length, equals(3)); + expect(spans[1].text, equals("hello world")); + expect(spans[1].style!.fontWeight, equals(FontWeight.bold)); + }); + + test("should skip bold formatting patterns", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**hello world**", TextStyle(fontSize: 14), skipPatterns: true); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("hello world")); + expect(spans[0].style!.fontWeight, equals(FontWeight.bold)); + }); + + test("should handle broken bold patterns (left)", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("***hello world**", TextStyle(fontSize: 14)); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("***hello world**")); + expect(spans[0].style!, equals(TextStyle(fontSize: 14.0))); + }); + + test("should handle broken bold patterns (right)", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("*hello world**", TextStyle(fontSize: 14)); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("*hello world**")); + expect(spans[0].style!, equals(TextStyle(fontSize: 14.0))); + }); + }); + + group("italic formatting", () { + test("should apply italic formatting", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("*hello world*", TextStyle(fontSize: 14)); + + expect(spans.length, equals(3)); + expect(spans[1].text, equals("hello world")); + expect(spans[1].style!.fontStyle, equals(FontStyle.italic)); + }); + + test("should skip italic formatting patterns", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("*hello world*", TextStyle(fontSize: 14), skipPatterns: true); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("hello world")); + expect(spans[0].style!.fontStyle, equals(FontStyle.italic)); + }); + }); + + group("bold and italic formatting", () { + test("should apply both bold and italic together", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("***hello world***", TextStyle(fontSize: 14)); + + expect(spans.length, equals(3)); + expect(spans[1].text, equals("hello world")); + expect(spans[1].style!.fontStyle, equals(FontStyle.italic)); + expect(spans[1].style!.fontWeight, equals(FontWeight.bold)); + }); + + test("should skip bold and italic formatting patterns", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("***hello world***", TextStyle(fontSize: 14), skipPatterns: true); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("hello world")); + expect(spans[0].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[0].style!.fontStyle, equals(FontStyle.italic)); + }); + }); + + group("underline formatting", () { + test("should apply underline formatting", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("__hello world__", TextStyle(fontSize: 14)); + + expect(spans.length, equals(3)); + expect(spans[1].text, equals("hello world")); + expect(spans[1].style!.decoration, equals(TextDecoration.underline)); + }); + + test("should skip underline formatting patterns", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("__hello world__", TextStyle(fontSize: 14), skipPatterns: true); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("hello world")); + expect(spans[0].style!.decoration, equals(TextDecoration.underline)); + }); + + test("should handle broken underline patterns (left)", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("__hello world_", TextStyle(fontSize: 14)); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("__hello world_")); + expect(spans[0].style!, equals(TextStyle(fontSize: 14.0))); + }); + + test("should handle broken underline patterns (right)", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("_hello world__", TextStyle(fontSize: 14)); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("_hello world__")); + expect(spans[0].style!, equals(TextStyle(fontSize: 14.0))); + }); + }); + + group("strikethrough formatting", () { + test("should apply strikethrough formatting", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("~~hello world~~", TextStyle(fontSize: 14)); + + expect(spans.length, equals(3)); + expect(spans[1].text, equals("hello world")); + expect(spans[1].style!.decoration, equals(TextDecoration.lineThrough)); + }); + + test("should skip strikethrough formatting patterns", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("~~hello world~~", TextStyle(fontSize: 14), skipPatterns: true); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("hello world")); + expect(spans[0].style!.decoration, equals(TextDecoration.lineThrough)); + }); + + test("should handle broken strikethrough patterns (left)", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("~~hello world~", TextStyle(fontSize: 14)); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("~~hello world~")); + expect(spans[0].style!, equals(TextStyle(fontSize: 14.0))); + }); + + test("should handle broken strikethrough patterns (right)", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("~hello world~~", TextStyle(fontSize: 14)); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("~hello world~~")); + expect(spans[0].style!, equals(TextStyle(fontSize: 14.0))); + }); + }); + + group("combined formatting", () { + test("should handle bold and italic sequentially", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**bold** *italic*", TextStyle(fontSize: 14)); + + expect(spans.length, equals(7)); + expect(spans[1].text, equals("bold")); + expect(spans[1].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[5].text, equals("italic")); + expect(spans[5].style!.fontStyle, equals(FontStyle.italic)); + }); + + test("should handle normal text between formatted text", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**bold** normal *italic*", TextStyle(fontSize: 14)); + + expect(spans.length, equals(7)); + expect(spans[1].text, equals("bold")); + expect(spans[1].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[3].text, equals(" normal ")); + expect(spans[5].text, equals("italic")); + expect(spans[5].style!.fontStyle, equals(FontStyle.italic)); + }); + + test("should handle underline and strikethrough combination", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("__underlined__ ~~strikethrough~~", TextStyle(fontSize: 14)); + + expect(spans.length, equals(7)); + expect(spans[1].text, equals("underlined")); + expect(spans[1].style!.decoration, equals(TextDecoration.underline)); + expect(spans[5].text, equals("strikethrough")); + expect(spans[5].style!.decoration, equals(TextDecoration.lineThrough)); + }); + + test("should handle all formatting types together", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**bold** *italic* ~~strike~~ __underline__", TextStyle(fontSize: 14)); + + expect(spans.length, equals(15)); + expect(spans[1].text, equals("bold")); + expect(spans[1].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[5].text, equals("italic")); + expect(spans[5].style!.fontStyle, equals(FontStyle.italic)); + expect(spans[9].text, equals("strike")); + expect(spans[9].style!.decoration, equals(TextDecoration.lineThrough)); + expect(spans[13].text, equals("underline")); + expect(spans[13].style!.decoration, equals(TextDecoration.underline)); + }); + }); + + group("nested formatting", () { + test("should nest underline inside bold", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**bold __underline inside__ bold**", TextStyle(fontSize: 14)); + + expect(spans.length, equals(7)); + expect(spans[1].text, equals("bold ")); + expect(spans[1].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[3].text, equals("underline inside")); + expect(spans[3].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[3].style!.decoration, equals(TextDecoration.underline)); + expect(spans[5].text, equals(" bold")); + expect(spans[5].style!.fontWeight, equals(FontWeight.bold)); + }); + + test("should nest even when no text inbetween", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**__underline inside__**", TextStyle(fontSize: 14)); + + sendLog(spans); + expect(spans.length, equals(5)); + expect(spans[2].text, equals("underline inside")); + expect(spans[2].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[2].style!.decoration, equals(TextDecoration.underline)); + }); + + test("should skip patterns in nested", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**__underline inside__**", TextStyle(fontSize: 14), skipPatterns: true); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("underline inside")); + expect(spans[0].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[0].style!.decoration, equals(TextDecoration.underline)); + }); + + test("should skip patterns in nested 2", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("__**underline inside**__", TextStyle(fontSize: 14), skipPatterns: true); + + sendLog(spans); + + expect(spans.length, equals(1)); + expect(spans[0].text, equals("underline inside")); + expect(spans[0].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[0].style!.decoration, equals(TextDecoration.underline)); + }); + + test("should nest strikethrough inside underline", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("__underline ~~strikethrough inside~~ underline__", TextStyle(fontSize: 14)); + + expect(spans.length, equals(7)); + expect(spans[1].text, equals("underline ")); + expect(spans[1].style!.decoration, equals(TextDecoration.underline)); + expect(spans[3].text, equals("strikethrough inside")); + expect( + spans[3].style!.decoration, + equals(TextDecoration.combine([TextDecoration.underline, TextDecoration.lineThrough])), + ); + expect(spans[5].text, equals(" underline")); + expect(spans[5].style!.decoration, equals(TextDecoration.underline)); + }); + + test("should handle overlapping bold and strikethrough", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("~~strike **bold strike** strike~~", TextStyle(fontSize: 14)); + + expect(spans.length, equals(7)); + expect(spans[1].text, equals("strike ")); + expect(spans[1].style!.decoration, equals(TextDecoration.lineThrough)); + expect(spans[3].text, equals("bold strike")); + expect(spans[3].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[3].style!.decoration, equals(TextDecoration.lineThrough)); + expect(spans[5].text, equals(" strike")); + expect(spans[5].style!.decoration, equals(TextDecoration.lineThrough)); + }); + + test("should handle complex nesting with multiple formats", () { + TextEvaluator eval = TextEvaluator(); + final spans = eval.evaluate("**bold ~~strike __underline__ strike~~ bold**", TextStyle(fontSize: 14)); + + expect(spans.length, equals(11)); + expect(spans[1].text, equals("bold ")); + expect(spans[1].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[3].text, equals("strike ")); + expect(spans[3].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[3].style!.decoration, equals(TextDecoration.lineThrough)); + expect(spans[5].text, equals("underline")); + expect(spans[5].style!.fontWeight, equals(FontWeight.bold)); + expect( + spans[5].style!.decoration, + equals(TextDecoration.combine([TextDecoration.underline, TextDecoration.lineThrough])), + ); + expect(spans[7].text, equals(" strike")); + expect(spans[7].style!.fontWeight, equals(FontWeight.bold)); + expect(spans[7].style!.decoration, equals(TextDecoration.lineThrough)); + expect(spans[9].text, equals(" bold")); + expect(spans[9].style!.fontWeight, equals(FontWeight.bold)); + }); + }); + + group("works while typing", () { + test("should handle incomplete bold during typing", () { + TextEvaluator eval = TextEvaluator(); + + // Typing "*" + var spans = eval.evaluate("*", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("*")); + + // Typing "**" + spans = eval.evaluate("**", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("**")); + + // Typing "**h" + spans = eval.evaluate("**h", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("**h")); + + // Typing "**hello" + spans = eval.evaluate("**hello", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("**hello")); + }); + + test("should handle incomplete italic during typing", () { + TextEvaluator eval = TextEvaluator(); + + // Typing "*i" + var spans = eval.evaluate("*i", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("*i")); + + // Typing "*italic" + spans = eval.evaluate("*italic", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("*italic")); + }); + + test("should handle incomplete strikethrough during typing", () { + TextEvaluator eval = TextEvaluator(); + + // Typing "~" + var spans = eval.evaluate("~", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("~")); + + // Typing "~~" + spans = eval.evaluate("~~", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("~~")); + + // Typing "~~s" + spans = eval.evaluate("~~s", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("~~s")); + + // Typing "~~s~~" + spans = eval.evaluate("~~s~~", TextStyle(fontSize: 14)); + expect(spans.length, equals(3)); + expect(spans[1].text, equals("s")); + expect(spans[1].style!.decoration, equals(TextDecoration.lineThrough)); + }); + + test("should handle incomplete underline during typing", () { + TextEvaluator eval = TextEvaluator(); + + // Typing "_" + var spans = eval.evaluate("_", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("_")); + + // Typing "__" + spans = eval.evaluate("__", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("__")); + + // Typing "__u" + spans = eval.evaluate("__u", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("__u")); + }); + + test("should handle formatting completing during typing", () { + TextEvaluator eval = TextEvaluator(); + + // Typing "**bold*" + var spans = eval.evaluate("**bold*", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("**bold*")); + + // Typing "**bold**" + spans = eval.evaluate("**bold**", TextStyle(fontSize: 14)); + expect(spans.length, equals(3)); + expect(spans[1].text, equals("bold")); + expect(spans[1].style!.fontWeight, equals(FontWeight.bold)); + }); + + test("should handle nested formatting during typing", () { + TextEvaluator eval = TextEvaluator(); + + // Typing "**bold __" + var spans = eval.evaluate("**bold __", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("**bold __")); + sendLog("'**bold __' completed"); + + // Typing "**bold __under" + spans = eval.evaluate("**bold __under", TextStyle(fontSize: 14)); + expect(spans.length, equals(1)); + expect(spans[0].text, equals("**bold __under")); + sendLog("'**bold __under' completed"); + + // Complete underline inside bold + spans = eval.evaluate("**bold __under__", TextStyle(fontSize: 14)); + expect(spans.length, equals(4)); + expect(spans[0].text, equals("**bold ")); + expect(spans[2].text, equals("under")); + expect(spans[2].style!.decoration, equals(TextDecoration.underline)); + sendLog("'**bold __under__' completed"); + }); + }); + }); +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 94119b4d..aec35482 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,13 +8,13 @@ #include #include +#include #include #include #include #include #include #include -#include #include #include @@ -23,6 +23,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + FlutterWebRTCPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); JustAudioWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("JustAudioWindowsPlugin")); PasteboardPluginRegisterWithRegistrar( @@ -35,8 +37,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("SodiumLibsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); - TrayManagerPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0b00acd3..11139c5d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,18 +5,19 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows flutter_secure_storage_windows + flutter_webrtc just_audio_windows pasteboard permission_handler_windows screen_retriever_windows sodium_libs sqlite3_flutter_libs - tray_manager url_launcher_windows window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + libspaceship ) set(PLUGIN_BUNDLED_LIBRARIES)