Merge branch 'main' into 2576-level-up-ui-tweaks
This commit is contained in:
commit
7a6a4d2be3
67 changed files with 2086 additions and 771 deletions
42
.github/dependabot.yml
vendored
42
.github/dependabot.yml
vendored
|
|
@ -1,20 +1,22 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
allow:
|
||||
- dependency-name: "*"
|
||||
commit-message:
|
||||
prefix: "build: "
|
||||
include: "scope"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
allow:
|
||||
- dependency-name: "*"
|
||||
commit-message:
|
||||
prefix: "build: "
|
||||
include: "scope"
|
||||
# #Pangea
|
||||
# version: 2
|
||||
# updates:
|
||||
# - package-ecosystem: "pub"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "daily"
|
||||
# allow:
|
||||
# - dependency-name: "*"
|
||||
# commit-message:
|
||||
# prefix: "build: "
|
||||
# include: "scope"
|
||||
# - package-ecosystem: "github-actions"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "daily"
|
||||
# allow:
|
||||
# - dependency-name: "*"
|
||||
# commit-message:
|
||||
# prefix: "build: "
|
||||
# include: "scope"
|
||||
# Pangea#
|
||||
|
|
|
|||
41
.github/workflows/auto_pull_request.yaml
vendored
41
.github/workflows/auto_pull_request.yaml
vendored
|
|
@ -1,41 +0,0 @@
|
|||
name: Auto Pull Request
|
||||
|
||||
#on:
|
||||
# schedule:
|
||||
# - cron: '0 0 * * 0' # Run at midnight (00:00) every Sunday
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- auto-pr # Change this to match your main branch name
|
||||
|
||||
jobs:
|
||||
auto_pull_request:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Test
|
||||
run: echo ${{ github.head_ref }}.${{ github.sha }}
|
||||
|
||||
- name: Pull changes from FluffyChat
|
||||
run: |
|
||||
git config --global user.email "${{ vars.CI_EMAIL }}"
|
||||
git config --global user.name "${{ vars.CI_USERNAME }}"
|
||||
git remote add fluffychat https://github.com/krille-chan/fluffychat
|
||||
git fetch fluffychat main
|
||||
git merge --no-edit fluffychat/main --allow-unrelated-histories
|
||||
|
||||
- name: Push changes
|
||||
run: |
|
||||
git push origin HEAD:main-update-fluffy-automatic
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
title: Updated fork with new fluffy changes
|
||||
body: |
|
||||
This is an automatic PR created by GitHub Actions.
|
||||
branch: main
|
||||
79
.github/workflows/build-ios.yml
vendored
79
.github/workflows/build-ios.yml
vendored
|
|
@ -1,79 +0,0 @@
|
|||
name: build-ios
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
screenshot:
|
||||
type: string
|
||||
required: true
|
||||
ipa:
|
||||
description: 'Run IPA build'
|
||||
type: string
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
screenshot:
|
||||
description: 'Run screenshot build'
|
||||
type: choice
|
||||
options: ['true', 'false']
|
||||
required: true
|
||||
ipa:
|
||||
description: 'Run IPA build'
|
||||
type: choice
|
||||
options: ['true', 'false']
|
||||
required: true
|
||||
|
||||
|
||||
jobs:
|
||||
build-ios:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ios
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: 'echo "$API_KEY" | base64 --decode > AuthKey.p8'
|
||||
shell: bash
|
||||
env:
|
||||
API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
||||
|
||||
- run: bundle install
|
||||
|
||||
- run: bundle exec fastlane versioning
|
||||
|
||||
- name: Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
- run: flutter build ios --simulator --target=integration_test/screenshot_test.dart
|
||||
if: ${{ inputs.screenshot == 'true' }}
|
||||
|
||||
- name: Archive integration ipa
|
||||
if: ${{ inputs.screenshot == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-simulator-build
|
||||
path: build/ios/iphonesimulator/Runner.app
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# Build ios Release
|
||||
- run: flutter build ios --release --config-only --no-codesign --target=lib/main.dart
|
||||
if: ${{ inputs.ipa == 'true' }}
|
||||
|
||||
- run: bundle exec fastlane build
|
||||
if: ${{ inputs.ipa == 'true' }}
|
||||
|
||||
- name: Archive ipa
|
||||
if: ${{ inputs.ipa == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Runner.ipa
|
||||
path: ios/Runner.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
39
.github/workflows/dart_format.yaml
vendored
39
.github/workflows/dart_format.yaml
vendored
|
|
@ -1,39 +0,0 @@
|
|||
# name: Dart Code Formatter
|
||||
|
||||
# on:
|
||||
# pull_request:
|
||||
# push:
|
||||
# branches: main
|
||||
|
||||
# jobs:
|
||||
# format:
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - name: Checkout code
|
||||
# uses: actions/checkout@v3
|
||||
# with:
|
||||
# ref: ${{ github.head_ref }}
|
||||
|
||||
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
# - uses: subosito/flutter-action@v2
|
||||
# with:
|
||||
# flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
# cache: true
|
||||
|
||||
# - name: Auto-format Dart code
|
||||
# run: |
|
||||
# dart format lib/ test/
|
||||
# dart run import_sorter:main --no-comments
|
||||
# if ! git diff --exit-code; then
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
# git add .
|
||||
# git commit -m "generated"
|
||||
# git push
|
||||
# fi
|
||||
|
||||
# - name: Check for unformatted files
|
||||
# if: ${{ failure() }}
|
||||
# run: |
|
||||
# echo "Code was formatted. Please verify the changes in the PR."
|
||||
200
.github/workflows/integrate.yaml
vendored
200
.github/workflows/integrate.yaml
vendored
|
|
@ -1,107 +1,105 @@
|
|||
name: Pull Request Workflow
|
||||
# #Pangea
|
||||
# name: Pull Request Workflow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
# on:
|
||||
# pull_request:
|
||||
# merge_group:
|
||||
|
||||
jobs:
|
||||
code_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ./scripts/generate-locale-config.sh
|
||||
- run: git diff --exit-code
|
||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
- run: flutter gen-l10n
|
||||
- name: Check formatting
|
||||
run: dart format lib/ test/ --set-exit-if-changed
|
||||
- name: Check import formatting
|
||||
run: dart run import_sorter:main --no-comments --exit-if-changed
|
||||
- name: Check license compliance
|
||||
run: dart run license_checker check-licenses -c licenses.yaml --problematic
|
||||
- run: flutter analyze
|
||||
# #Pangea - Commented out the following lines, we already have fcm enabled by default
|
||||
# - name: Apply google services patch
|
||||
# run: git apply ./scripts/enable-android-google-services.patch
|
||||
# Pangea#
|
||||
- run: flutter analyze
|
||||
- run: flutter test
|
||||
# jobs:
|
||||
# code_tests:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - run: ./scripts/generate-locale-config.sh
|
||||
# - run: git diff --exit-code
|
||||
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
# - uses: subosito/flutter-action@v2
|
||||
# with:
|
||||
# flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
# cache: true
|
||||
# - run: flutter pub get
|
||||
# - run: flutter gen-l10n
|
||||
# - name: Check formatting
|
||||
# run: dart format lib/ test/ --set-exit-if-changed
|
||||
# - name: Check import formatting
|
||||
# run: dart run import_sorter:main --no-comments --exit-if-changed
|
||||
# - name: Check license compliance
|
||||
# run: dart run license_checker check-licenses -c licenses.yaml --problematic
|
||||
# - run: flutter analyze
|
||||
# - name: Apply google services patch
|
||||
# run: git apply ./scripts/enable-android-google-services.patch
|
||||
# - run: flutter analyze
|
||||
# - run: flutter test
|
||||
|
||||
build_apk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "zulu"
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: false
|
||||
- run: flutter pub get
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
android: false
|
||||
- run: flutter build apk --debug
|
||||
# build_apk:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
# - uses: actions/setup-java@v4
|
||||
# with:
|
||||
# java-version: ${{ env.JAVA_VERSION }}
|
||||
# distribution: "zulu"
|
||||
# - uses: subosito/flutter-action@v2
|
||||
# with:
|
||||
# flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
# cache: false
|
||||
# - run: flutter pub get
|
||||
# - name: Free Disk Space (Ubuntu)
|
||||
# uses: jlumbroso/free-disk-space@main
|
||||
# with:
|
||||
# # this might remove tools that are actually needed,
|
||||
# # if set to "true" but frees about 6 GB
|
||||
# tool-cache: false
|
||||
# android: false
|
||||
# - run: flutter build apk --debug
|
||||
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: false
|
||||
- run: flutter pub get
|
||||
- name: Prepare web
|
||||
run: ./scripts/prepare-web.sh
|
||||
- run: flutter build web
|
||||
# build_web:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
# - uses: subosito/flutter-action@v2
|
||||
# with:
|
||||
# flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
# cache: false
|
||||
# - run: flutter pub get
|
||||
# - name: Prepare web
|
||||
# run: ./scripts/prepare-web.sh
|
||||
# - run: flutter build web
|
||||
|
||||
# #Pangea
|
||||
# commented out because we do not build Pangea Chat to linux
|
||||
# build_debug_linux:
|
||||
# strategy:
|
||||
# matrix:
|
||||
# arch: [ x64, arm64 ]
|
||||
# runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
# - name: Install dependencies
|
||||
# run: sudo apt-get update && sudo apt-get install git wget curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev libwebkit2gtk-4.1-dev -y
|
||||
# - name: Install Flutter
|
||||
# run: |
|
||||
# git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git
|
||||
# ./flutter/bin/flutter doctor
|
||||
# - run: ./flutter/bin/flutter pub get
|
||||
# - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }}
|
||||
# Pangea#
|
||||
# commented out because we do not build Pangea Chat to linux
|
||||
# build_debug_linux:
|
||||
# strategy:
|
||||
# matrix:
|
||||
# arch: [ x64, arm64 ]
|
||||
# runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
# - name: Install dependencies
|
||||
# run: sudo apt-get update && sudo apt-get install git wget curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev libwebkit2gtk-4.1-dev -y
|
||||
# - name: Install Flutter
|
||||
# run: |
|
||||
# git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git
|
||||
# ./flutter/bin/flutter doctor
|
||||
# - run: ./flutter/bin/flutter pub get
|
||||
# - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }}
|
||||
|
||||
build_debug_ios:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
- name: Setup Xcode version
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: latest
|
||||
- run: brew install sqlcipher
|
||||
- run: flutter pub get
|
||||
- run: flutter build ipa --no-codesign
|
||||
# build_debug_ios:
|
||||
# runs-on: macos-15
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
# - uses: subosito/flutter-action@v2
|
||||
# with:
|
||||
# flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
# cache: true
|
||||
# - name: Setup Xcode version
|
||||
# uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
# with:
|
||||
# xcode-version: latest
|
||||
# - run: brew install sqlcipher
|
||||
# - run: flutter pub get
|
||||
# - run: flutter build ipa --no-codesign
|
||||
# Pangea#
|
||||
|
|
|
|||
11
.github/workflows/manual.yml
vendored
11
.github/workflows/manual.yml
vendored
|
|
@ -77,13 +77,4 @@ jobs:
|
|||
bundle install
|
||||
bundle update fastlane
|
||||
bundle exec fastlane deploy_internal_test
|
||||
cd ..
|
||||
|
||||
deploy_ios_testflight: # stashed on old.yml
|
||||
environment:
|
||||
name: ${{ inputs.environment }}
|
||||
env:
|
||||
WEB_APP_ENV: ${{ vars.WEB_APP_ENV }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
cd ..
|
||||
26
.github/workflows/matrix_notification.yaml
vendored
26
.github/workflows/matrix_notification.yaml
vendored
|
|
@ -1,26 +0,0 @@
|
|||
# name: Matrix Notification
|
||||
|
||||
# on:
|
||||
# issues:
|
||||
# types: [ opened ]
|
||||
# issue_comment:
|
||||
# types: [ created ]
|
||||
|
||||
# jobs:
|
||||
# notify:
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - name: Send Matrix Notification
|
||||
# env:
|
||||
# MATRIX_URL: https://matrix.janian.de/_matrix/client/v3/rooms/${{ secrets.MATRIX_MANAGEMENT_ROOM }}/send/m.room.message
|
||||
# run: |
|
||||
# if [ "${{ github.event.action }}" == "opened" ]; then
|
||||
# PAYLOAD="{\"msgtype\": \"m.notice\", \"body\": \"New Issue from ${{ github.event.issue.user.login }}\\n${{ github.event.issue.title }}\\n\\n${{ github.event.issue.body }}\\n\\nURL: ${{ github.event.issue.html_url }}\"}"
|
||||
# elif [ "${{ github.event.action }}" == "created" ]; then
|
||||
# PAYLOAD="{\"msgtype\": \"m.notice\", \"body\": \"New Comment from ${{ github.event.comment.user.login }}\\n\\n${{ github.event.comment.body }}\\n\\nURL: ${{ github.event.comment.html_url }}\"}"
|
||||
# fi
|
||||
# curl -X POST -H "Authorization: Bearer ${{ secrets.MATRIX_BOT_TOKEN }}" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d "$PAYLOAD" \
|
||||
# $MATRIX_URL
|
||||
44
.github/workflows/old.yml
vendored
44
.github/workflows/old.yml
vendored
|
|
@ -1,44 +0,0 @@
|
|||
name: Old Release Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master-unused
|
||||
|
||||
concurrency:
|
||||
group: release_workflow
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy_ios_internal:
|
||||
runs-on: macos-latest
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cat ../.github/workflows/versions.env >> $GITHUB_ENV
|
||||
|
||||
- name: Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
# Build ios Release
|
||||
- run: flutter build ios --release --config-only --no-codesign --target=lib/main.dart
|
||||
|
||||
- name: Deploy ios
|
||||
run: |
|
||||
mkdir -p build/ios
|
||||
cp build/app/outputs/bundle/release/app-release.aab build/ios/
|
||||
cd ios
|
||||
bundle install
|
||||
bundle update fastlane
|
||||
cd ..
|
||||
- name: Execute fastlane signing
|
||||
env:
|
||||
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
|
||||
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64 }}
|
||||
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
||||
run: bundle exec fastlane ios beta
|
||||
63
.github/workflows/release.yaml
vendored
63
.github/workflows/release.yaml
vendored
|
|
@ -142,36 +142,39 @@ jobs:
|
|||
asset_path: build/app/outputs/flutter-apk/app-release.apk
|
||||
asset_name: pangeachat.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
build_linux:
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [ x64 ]
|
||||
runs-on: ubuntu-latest
|
||||
needs: create_release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev libwebkit2gtk-4.1-dev -y
|
||||
- name: Install dependencies for audio-player
|
||||
run: sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
- name: Install Flutter
|
||||
run: |
|
||||
git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git
|
||||
./flutter/bin/flutter doctor
|
||||
- run: ./flutter/bin/flutter pub get
|
||||
- run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }}
|
||||
- name: Create archive
|
||||
run: tar -czf pangeachat-linux-${{ matrix.arch }}.tar.gz -C build/linux/${{ matrix.arch }}/release/bundle/ .
|
||||
- name: Upload to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAGES_DEPLOY_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: pangeachat-linux-${{ matrix.arch }}.tar.gz
|
||||
asset_name: pangeachat-linux-${{ matrix.arch }}.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
|
||||
# #Pangea
|
||||
# build_linux:
|
||||
# strategy:
|
||||
# matrix:
|
||||
# arch: [ x64 ]
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: create_release
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
# - name: Install dependencies
|
||||
# run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev libwebkit2gtk-4.1-dev -y
|
||||
# - name: Install dependencies for audio-player
|
||||
# run: sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
# - name: Install Flutter
|
||||
# run: |
|
||||
# git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git
|
||||
# ./flutter/bin/flutter doctor
|
||||
# - run: ./flutter/bin/flutter pub get
|
||||
# - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }}
|
||||
# - name: Create archive
|
||||
# run: tar -czf pangeachat-linux-${{ matrix.arch }}.tar.gz -C build/linux/${{ matrix.arch }}/release/bundle/ .
|
||||
# - name: Upload to release
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.PAGES_DEPLOY_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
# asset_path: pangeachat-linux-${{ matrix.arch }}.tar.gz
|
||||
# asset_name: pangeachat-linux-${{ matrix.arch }}.tar.gz
|
||||
# asset_content_type: application/gzip
|
||||
# Pangea#
|
||||
|
||||
deploy_web:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
39
.github/workflows/upload-release-ios.yml
vendored
39
.github/workflows/upload-release-ios.yml
vendored
|
|
@ -1,39 +0,0 @@
|
|||
name: upload-release-ios
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
new_release:
|
||||
required: true
|
||||
type: string
|
||||
description: "The new release version number"
|
||||
new_release_notes:
|
||||
required: true
|
||||
type: string
|
||||
description: "The release notes for the new release"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download app
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Runner.ipa
|
||||
path: ios/
|
||||
|
||||
- run: 'echo "$API_KEY" | base64 --decode > AuthKey.p8'
|
||||
shell: bash
|
||||
env:
|
||||
API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
||||
|
||||
- run: bundle install
|
||||
|
||||
- run: bundle exec fastlane upload_testflight
|
||||
env:
|
||||
RELEASE_NOTES: ${{ inputs.new_release_notes }}
|
||||
- run: bundle exec fastlane upload_metadata_app_store
|
||||
|
|
@ -653,6 +653,9 @@ abstract class AppRoutes {
|
|||
state,
|
||||
ChatMembersPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
// #Pangea
|
||||
filter: state.uri.queryParameters['filter'],
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
|
|||
|
|
@ -5017,5 +5017,21 @@
|
|||
"directMessage": "Direct Message",
|
||||
"newDirectMessage": "New direct message",
|
||||
"speakingExercisesTooltip": "Speaking",
|
||||
"noChatsFoundHereYet": "No chats found here yet"
|
||||
"noChatsFoundHereYet": "No chats found here yet",
|
||||
"endNow": "End now",
|
||||
"setDuration": "Set duration",
|
||||
"activityEnded": "That’s a wrap for this activity! Big thanks to everyone for chatting, learning, and making this space so lively. Language grows with conversation, and every word exchanged brings us closer to confidence and fluency.\n\nKeep practicing, stay curious, and don’t be shy to keep the conversation going!",
|
||||
"duration": "Duration",
|
||||
"transcriptionFailed": "Failed to transcribe audio",
|
||||
"aUserIsKnocking": "1 user is requesting to join your space",
|
||||
"usersAreKnocking": "{users} users are requesting to join your space",
|
||||
"@usersAreKnocking": {
|
||||
"type": "int",
|
||||
"placeholders": {
|
||||
"users": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"failedToFetchTranscription": "Failed to fetch transcription"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1650,6 +1650,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
showEmojiPicker = false;
|
||||
});
|
||||
}
|
||||
|
||||
void setSelectedEvent(Event event) {
|
||||
setState(() {
|
||||
selectedEvents.clear();
|
||||
selectedEvents.add(event);
|
||||
});
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
void clearSingleSelectedEvent() {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
|||
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_event_list.dart';
|
||||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pangea/activities/pinned_activity_message.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
|
@ -188,6 +190,13 @@ class ChatView extends StatelessWidget {
|
|||
if (scrollUpBannerEventId != null) {
|
||||
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
|
||||
}
|
||||
// #Pangea
|
||||
if (controller.room.activityPlan != null &&
|
||||
controller.room.activityPlan!.endAt != null &&
|
||||
controller.room.activityPlan!.endAt!.isAfter(DateTime.now())) {
|
||||
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
|
||||
}
|
||||
// Pangea#
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actionsIconTheme: IconThemeData(
|
||||
|
|
@ -226,6 +235,9 @@ class ChatView extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PinnedEvents(controller),
|
||||
// #Pangea
|
||||
PinnedActivityMessage(controller),
|
||||
// Pangea#
|
||||
if (scrollUpBannerEventId != null)
|
||||
ChatAppBarListTile(
|
||||
leading: IconButton(
|
||||
|
|
|
|||
|
|
@ -185,6 +185,23 @@ class HtmlMessage extends StatelessWidget {
|
|||
result.add(html.substring(lastEnd)); // Remaining text after last tag
|
||||
}
|
||||
|
||||
final replyTagIndex = result.indexWhere(
|
||||
(string) => string.contains('<mx-reply>'),
|
||||
);
|
||||
if (replyTagIndex != -1) {
|
||||
final closingReplyTagIndex = result.indexWhere(
|
||||
(string) => string.contains('</mx-reply>'),
|
||||
replyTagIndex,
|
||||
);
|
||||
if (closingReplyTagIndex != -1) {
|
||||
result.replaceRange(
|
||||
replyTagIndex,
|
||||
closingReplyTagIndex + 1,
|
||||
[result.sublist(replyTagIndex, closingReplyTagIndex + 1).join()],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final PangeaToken token in tokens ?? []) {
|
||||
final String tokenText = token.text.content;
|
||||
final substringIndex = result.indexWhere(
|
||||
|
|
@ -309,11 +326,10 @@ class HtmlMessage extends StatelessWidget {
|
|||
|
||||
final tokenWidth = renderer.tokenTextWidthForContainer(
|
||||
context,
|
||||
node.innerHtml,
|
||||
node.text,
|
||||
);
|
||||
|
||||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CompositedTransformTarget(
|
||||
link: token != null && renderer.assignTokenKey
|
||||
? MatrixState.pAnyState
|
||||
|
|
@ -364,7 +380,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
text: TextSpan(
|
||||
children: [
|
||||
LinkifySpan(
|
||||
text: node.innerHtml,
|
||||
text: node.text,
|
||||
style: renderer.style(
|
||||
context,
|
||||
color: renderer.backgroundColor(
|
||||
|
|
@ -407,7 +423,10 @@ class HtmlMessage extends StatelessWidget {
|
|||
avatar: user.avatarUrl,
|
||||
uri: href,
|
||||
outerContext: context,
|
||||
fontSize: fontSize,
|
||||
// #Pangea
|
||||
// fontSize: fontSize,
|
||||
fontSize: renderer.fontSize(context) ?? fontSize,
|
||||
// Pangea#
|
||||
color: linkStyle.color,
|
||||
// #Pangea
|
||||
userId: user.id,
|
||||
|
|
@ -428,7 +447,10 @@ class HtmlMessage extends StatelessWidget {
|
|||
avatar: room?.avatar,
|
||||
uri: href,
|
||||
outerContext: context,
|
||||
fontSize: fontSize,
|
||||
// #Pangea
|
||||
// fontSize: fontSize,
|
||||
fontSize: renderer.fontSize(context) ?? fontSize,
|
||||
// Pangea#
|
||||
color: linkStyle.color,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_state_event.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/file_description.dart';
|
||||
|
|
@ -121,6 +123,18 @@ class Message extends StatelessWidget {
|
|||
if (event.type == EventTypes.RoomCreate) {
|
||||
return RoomCreationStateEvent(event: event);
|
||||
}
|
||||
// #Pangea
|
||||
if (event.type == PangeaEventTypes.activityPlan) {
|
||||
final state = event.room.getState(PangeaEventTypes.activityPlan);
|
||||
if (state == null || state is! Event) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return state.originServerTs == event.originServerTs
|
||||
? ActivityStateEvent(event: event)
|
||||
: const SizedBox();
|
||||
}
|
||||
// Pangea#
|
||||
return StateMessage(event);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -161,7 +162,14 @@ class _Reaction extends StatelessWidget {
|
|||
color: color,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
// #Pangea
|
||||
// issue: https://github.com/pangeachat/client/issues/3100
|
||||
// fix: https://github.com/flutter/flutter/issues/119623#issuecomment-2476719745
|
||||
padding: PlatformInfos.isIOS
|
||||
? const EdgeInsets.fromLTRB(5.5, 1, 3, 2.5)
|
||||
: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
// Pangea#
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -79,10 +79,20 @@ class PinnedEvents extends StatelessWidget {
|
|||
iconSize: 18,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.push_pin),
|
||||
tooltip: L10n.of(context).unpin,
|
||||
onPressed: controller.room.canSendEvent(EventTypes.RoomPinnedEvents)
|
||||
? () => controller.unpinEvent(event!.eventId)
|
||||
: null,
|
||||
// #Pangea
|
||||
// tooltip: L10n.of(context).unpin,
|
||||
// onPressed: controller.room.canSendEvent(EventTypes.RoomPinnedEvents)
|
||||
// ? () => controller.unpinEvent(event!.eventId)
|
||||
// : null,
|
||||
tooltip:
|
||||
controller.room.canChangeStateEvent(EventTypes.RoomPinnedEvents)
|
||||
? L10n.of(context).unpin
|
||||
: null,
|
||||
onPressed:
|
||||
controller.room.canChangeStateEvent(EventTypes.RoomPinnedEvents)
|
||||
? () => controller.unpinEvent(event!.eventId)
|
||||
: null,
|
||||
// Pangea#
|
||||
),
|
||||
onTap: () => _displayPinnedEventsDialog(context),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -743,20 +743,24 @@ class ChatListController extends State<ChatList>
|
|||
value: ChatContextAction.open,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: room.avatar,
|
||||
size: Avatar.defaultSize / 2,
|
||||
name: displayname,
|
||||
// #Pangea
|
||||
userId: room.directChatMatrixID,
|
||||
// Pangea#
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
displayname,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 128),
|
||||
child: Text(
|
||||
displayname,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -89,39 +89,45 @@ class NaviRailItem extends StatelessWidget {
|
|||
// color: isSelected
|
||||
// ? theme.colorScheme.primaryContainer
|
||||
// : theme.colorScheme.surfaceContainerHigh,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ??
|
||||
(isSelected
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.surfaceContainerHigh),
|
||||
borderRadius: borderRadius,
|
||||
child: UnreadRoomsBadge(
|
||||
filter: unreadBadgeFilter ?? (_) => false,
|
||||
badgePosition: BadgePosition.topEnd(
|
||||
top: -4,
|
||||
end: isColumnMode ? 8 : 4,
|
||||
),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: isColumnMode ? 16.0 : 12.0,
|
||||
vertical: isColumnMode ? 8.0 : 6.0,
|
||||
),
|
||||
// Pangea#
|
||||
child: Tooltip(
|
||||
message: toolTip,
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ??
|
||||
(isSelected
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.surfaceContainerHigh),
|
||||
borderRadius: borderRadius,
|
||||
onTap: onTap,
|
||||
child: unreadBadgeFilter == null
|
||||
? icon
|
||||
: UnreadRoomsBadge(
|
||||
filter: unreadBadgeFilter,
|
||||
badgePosition: BadgePosition.topEnd(
|
||||
// #Pangea
|
||||
// top: -12,
|
||||
// end: -8,
|
||||
top: -20,
|
||||
end: -16,
|
||||
// Pangea#
|
||||
),
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: isColumnMode ? 16.0 : 12.0,
|
||||
vertical: isColumnMode ? 8.0 : 6.0,
|
||||
),
|
||||
// Pangea#
|
||||
child: Tooltip(
|
||||
message: toolTip,
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: onTap,
|
||||
// #Pangea
|
||||
child: icon,
|
||||
// child: unreadBadgeFilter == null
|
||||
// ? icon
|
||||
// : UnreadRoomsBadge(
|
||||
// filter: unreadBadgeFilter,
|
||||
// badgePosition: BadgePosition.topEnd(
|
||||
// top: -12,
|
||||
// end: -8,
|
||||
// ),
|
||||
// child: icon,
|
||||
// ),
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,8 +9,18 @@ import 'chat_members_view.dart';
|
|||
|
||||
class ChatMembersPage extends StatefulWidget {
|
||||
final String roomId;
|
||||
// #Pangea
|
||||
final String? filter;
|
||||
// Pangea#
|
||||
|
||||
const ChatMembersPage({required this.roomId, super.key});
|
||||
// #Pangea
|
||||
// const ChatMembersPage({required this.roomId, super.key});
|
||||
const ChatMembersPage({
|
||||
required this.roomId,
|
||||
this.filter,
|
||||
super.key,
|
||||
});
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
State<ChatMembersPage> createState() => ChatMembersController();
|
||||
|
|
@ -24,6 +34,22 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
|
||||
final TextEditingController filterController = TextEditingController();
|
||||
|
||||
// #Pangea
|
||||
@override
|
||||
void didUpdateWidget(ChatMembersPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Update the membership filter if the widget's filter changes
|
||||
if (oldWidget.filter != widget.filter) {
|
||||
setState(() {
|
||||
membershipFilter = Membership.values.firstWhere(
|
||||
(membership) => membership.name == widget.filter,
|
||||
orElse: () => Membership.join,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
void setMembershipFilter(Membership membership) {
|
||||
membershipFilter = membership;
|
||||
setFilter();
|
||||
|
|
@ -79,6 +105,19 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
|
||||
if (!mounted) return;
|
||||
|
||||
// #Pangea
|
||||
final availableFilters = (participants ?? [])
|
||||
.map(
|
||||
(p) => p.membership,
|
||||
)
|
||||
.toSet();
|
||||
|
||||
if (availableFilters.length == 1 &&
|
||||
membershipFilter != availableFilters.first) {
|
||||
membershipFilter = availableFilters.first;
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
setState(() {
|
||||
members = participants;
|
||||
});
|
||||
|
|
@ -110,6 +149,15 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
false,
|
||||
)
|
||||
.listen(refreshMembers);
|
||||
|
||||
// #Pangea
|
||||
if (widget.filter != null) {
|
||||
membershipFilter = Membership.values.firstWhere(
|
||||
(membership) => membership.name == widget.filter,
|
||||
orElse: () => Membership.join,
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -345,7 +345,20 @@ class _InviteContactListTile extends StatelessWidget {
|
|||
// color: theme.colorScheme.secondary,
|
||||
// ),
|
||||
// ),
|
||||
subtitle: LevelDisplayName(userId: profile.userId),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// https://github.com/pangeachat/client/issues/3047
|
||||
const SizedBox(height: 2.0),
|
||||
Text(
|
||||
profile.userId,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
LevelDisplayName(userId: profile.userId),
|
||||
],
|
||||
),
|
||||
// Pangea#
|
||||
trailing: TextButton.icon(
|
||||
onPressed: isMember ? null : onTap,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ class NewGroupController extends State<NewGroup> {
|
|||
TextEditingController nameController = TextEditingController();
|
||||
|
||||
// #Pangea
|
||||
// bool publicGroup = false;
|
||||
// bool groupCanBeFound = false;
|
||||
ActivityPlanModel? selectedActivity;
|
||||
Uint8List? selectedActivityImage;
|
||||
String? selectedActivityImageFilename;
|
||||
|
|
@ -45,12 +47,8 @@ class NewGroupController extends State<NewGroup> {
|
|||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
bool requiredCodeToJoin = false;
|
||||
// bool publicGroup = false;
|
||||
|
||||
bool get canSubmit => nameController.text.trim().isNotEmpty;
|
||||
// Pangea#
|
||||
bool groupCanBeFound = false;
|
||||
|
||||
Uint8List? avatar;
|
||||
|
||||
|
|
@ -71,7 +69,6 @@ class NewGroupController extends State<NewGroup> {
|
|||
// #Pangea
|
||||
// void setPublicGroup(bool b) =>
|
||||
// setState(() => publicGroup = groupCanBeFound = b);
|
||||
void setRequireCode(bool b) => setState(() => requiredCodeToJoin = b);
|
||||
|
||||
void setSelectedActivity(
|
||||
ActivityPlanModel? activity,
|
||||
|
|
@ -103,7 +100,9 @@ class NewGroupController extends State<NewGroup> {
|
|||
}
|
||||
// Pangea#
|
||||
|
||||
void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b);
|
||||
// #Pangea
|
||||
// void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b);
|
||||
// Pangea#
|
||||
|
||||
void selectPhoto() async {
|
||||
final photo = await selectFiles(
|
||||
|
|
@ -230,10 +229,8 @@ class NewGroupController extends State<NewGroup> {
|
|||
name: nameController.text,
|
||||
introChatName: L10n.of(context).introductions,
|
||||
announcementsChatName: L10n.of(context).announcements,
|
||||
visibility:
|
||||
groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private,
|
||||
joinRules:
|
||||
requiredCodeToJoin ? sdk.JoinRules.knock : sdk.JoinRules.public,
|
||||
visibility: sdk.Visibility.private,
|
||||
joinRules: sdk.JoinRules.knock,
|
||||
avatar: avatar,
|
||||
avatarUrl: avatarUrl,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_carousel.dart';
|
||||
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
|
|
@ -38,20 +37,6 @@ class NewGroupView extends StatelessWidget {
|
|||
: L10n.of(context).newChat,
|
||||
// Pangea#
|
||||
),
|
||||
actions: [
|
||||
if (controller.createGroupType == CreateGroupType.space)
|
||||
TextButton(
|
||||
onPressed: controller.loading
|
||||
? null
|
||||
: () => SpaceCodeUtil.joinWithSpaceCodeDialog(context),
|
||||
child: Text(
|
||||
L10n.of(context).joinByCode,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
// #Pangea
|
||||
|
|
@ -141,51 +126,33 @@ class NewGroupView extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
// #Pangea
|
||||
if (controller.createGroupType == CreateGroupType.space)
|
||||
// Pangea#
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
secondary: const Icon(Icons.public_outlined),
|
||||
// #Pangea
|
||||
// title: Text(
|
||||
// controller.createGroupType == CreateGroupType.space
|
||||
// ? L10n.of(context).spaceIsPublic
|
||||
// : L10n.of(context).groupIsPublic,
|
||||
// ),
|
||||
title: Text(L10n.of(context).requireCodeToJoin),
|
||||
// value: controller.publicGroup,
|
||||
// onChanged:
|
||||
// controller.loading ? null : controller.setPublicGroup,
|
||||
value: controller.requiredCodeToJoin,
|
||||
onChanged:
|
||||
controller.loading ? null : controller.setRequireCode,
|
||||
// Pangea#
|
||||
),
|
||||
// #Pangea
|
||||
if (controller.createGroupType == CreateGroupType.space)
|
||||
// Pangea#
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child:
|
||||
// #Pangea
|
||||
// controller.publicGroup ?
|
||||
// Pangea#
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
secondary: const Icon(Icons.search_outlined),
|
||||
// #Pangea
|
||||
// title: Text(L10n.of(context).groupCanBeFoundViaSearch),
|
||||
title: Text(L10n.of(context).canFindInSearch),
|
||||
// Pangea#
|
||||
value: controller.groupCanBeFound,
|
||||
onChanged:
|
||||
controller.loading ? null : controller.setGroupCanBeFound,
|
||||
),
|
||||
// #Pangea
|
||||
// : const SizedBox.shrink(),
|
||||
// Pangea#
|
||||
),
|
||||
// SwitchListTile.adaptive(
|
||||
// contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
// secondary: const Icon(Icons.public_outlined),
|
||||
// title: Text(
|
||||
// controller.createGroupType == CreateGroupType.space
|
||||
// ? L10n.of(context).spaceIsPublic
|
||||
// : L10n.of(context).groupIsPublic,
|
||||
// ),
|
||||
// value: controller.publicGroup,
|
||||
// onChanged: controller.loading ? null : controller.setPublicGroup,
|
||||
// ),
|
||||
// AnimatedSize(
|
||||
// duration: FluffyThemes.animationDuration,
|
||||
// curve: FluffyThemes.animationCurve,
|
||||
// child: controller.publicGroup
|
||||
// ? SwitchListTile.adaptive(
|
||||
// contentPadding:
|
||||
// const EdgeInsets.symmetric(horizontal: 32),
|
||||
// secondary: const Icon(Icons.search_outlined),
|
||||
// title: Text(L10n.of(context).groupCanBeFoundViaSearch),
|
||||
// value: controller.groupCanBeFound,
|
||||
// onChanged: controller.loading
|
||||
// ? null
|
||||
// : controller.setGroupCanBeFound,
|
||||
// )
|
||||
// : const SizedBox.shrink(),
|
||||
// ),
|
||||
// AnimatedSize(
|
||||
// duration: FluffyThemes.animationDuration,
|
||||
// curve: FluffyThemes.animationCurve,
|
||||
|
|
|
|||
62
lib/pangea/activities/activity_aware_builder.dart
Normal file
62
lib/pangea/activities/activity_aware_builder.dart
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ActivityAwareBuilder extends StatefulWidget {
|
||||
final DateTime? deadline;
|
||||
final Widget Function(bool) builder;
|
||||
|
||||
const ActivityAwareBuilder({
|
||||
super.key,
|
||||
required this.builder,
|
||||
this.deadline,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityAwareBuilder> createState() => ActivityAwareBuilderState();
|
||||
}
|
||||
|
||||
class ActivityAwareBuilderState extends State<ActivityAwareBuilder> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ActivityAwareBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.deadline != widget.deadline) {
|
||||
_setTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setTimer() {
|
||||
final now = DateTime.now();
|
||||
final delay = widget.deadline?.difference(now);
|
||||
|
||||
if (delay != null && delay > Duration.zero) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(delay, () {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(
|
||||
widget.deadline != null && widget.deadline!.isAfter(DateTime.now()),
|
||||
);
|
||||
}
|
||||
}
|
||||
3
lib/pangea/activities/activity_constants.dart
Normal file
3
lib/pangea/activities/activity_constants.dart
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class ActivityConstants {
|
||||
static const String activityFinishedAsset = "EndActivityMsg.png";
|
||||
}
|
||||
282
lib/pangea/activities/activity_duration_popup.dart
Normal file
282
lib/pangea/activities/activity_duration_popup.dart
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
||||
class ActivityDurationPopup extends StatefulWidget {
|
||||
final Duration initialValue;
|
||||
const ActivityDurationPopup({
|
||||
super.key,
|
||||
required this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityDurationPopup> createState() => ActivityDurationPopupState();
|
||||
}
|
||||
|
||||
class ActivityDurationPopupState extends State<ActivityDurationPopup> {
|
||||
final TextEditingController _daysController = TextEditingController();
|
||||
final TextEditingController _hoursController = TextEditingController();
|
||||
final TextEditingController _minutesController = TextEditingController();
|
||||
|
||||
String? error;
|
||||
|
||||
final List<Duration> _durations = [
|
||||
const Duration(minutes: 15),
|
||||
const Duration(minutes: 30),
|
||||
const Duration(minutes: 45),
|
||||
const Duration(minutes: 60),
|
||||
const Duration(hours: 1, minutes: 30),
|
||||
const Duration(hours: 2),
|
||||
const Duration(hours: 24),
|
||||
const Duration(days: 2),
|
||||
const Duration(days: 7),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_daysController.text = widget.initialValue.inDays.toString();
|
||||
_hoursController.text =
|
||||
widget.initialValue.inHours.remainder(24).toString();
|
||||
_minutesController.text =
|
||||
widget.initialValue.inMinutes.remainder(60).toString();
|
||||
|
||||
_daysController.addListener(() => setState(() => error = null));
|
||||
_hoursController.addListener(() => setState(() => error = null));
|
||||
_minutesController.addListener(() => setState(() => error = null));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_daysController.dispose();
|
||||
_hoursController.dispose();
|
||||
_minutesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setDuration({int? days, int? hours, int? minutes}) {
|
||||
setState(() {
|
||||
if (days != null) _daysController.text = days.toString();
|
||||
if (hours != null) _hoursController.text = hours.toString();
|
||||
if (minutes != null) _minutesController.text = minutes.toString();
|
||||
});
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
final days = duration.inDays;
|
||||
final hours = duration.inHours.remainder(24);
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
|
||||
final List<String> parts = [];
|
||||
if (days > 0) parts.add("${days}d");
|
||||
if (hours > 0) parts.add("${hours}h");
|
||||
if (minutes > 0) parts.add("${minutes}m");
|
||||
if (parts.isEmpty) return "0m";
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
Duration get _duration {
|
||||
final days = int.tryParse(_daysController.text) ?? 0;
|
||||
final hours = int.tryParse(_hoursController.text) ?? 0;
|
||||
final minutes = int.tryParse(_minutesController.text) ?? 0;
|
||||
return Duration(days: days, hours: hours, minutes: minutes);
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
final days = int.tryParse(_daysController.text);
|
||||
final hours = int.tryParse(_hoursController.text);
|
||||
final minutes = int.tryParse(_minutesController.text);
|
||||
|
||||
if (days == null || hours == null || minutes == null) {
|
||||
setState(() {
|
||||
error = "Invalid duration";
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(_duration);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Dialog(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 350.0,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).setDuration,
|
||||
style: const TextStyle(fontSize: 20.0, height: 1.2),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
width: 2,
|
||||
color: theme.colorScheme.primary.withAlpha(100),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12.0,
|
||||
bottom: 12.0,
|
||||
right: 24.0,
|
||||
left: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SelectionArea(
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
_DatePickerInput(
|
||||
type: "d",
|
||||
controller: _daysController,
|
||||
),
|
||||
_DatePickerInput(
|
||||
type: "h",
|
||||
controller: _hoursController,
|
||||
),
|
||||
_DatePickerInput(
|
||||
type: "m",
|
||||
controller: _minutesController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.alarm,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: error != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
error!,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.error,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 24.0,
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 10.0,
|
||||
runSpacing: 10.0,
|
||||
children: _durations
|
||||
.map(
|
||||
(d) => InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
_setDuration(
|
||||
days: d.inDays,
|
||||
hours: d.inHours.remainder(24),
|
||||
minutes: d.inMinutes.remainder(60),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 0.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer
|
||||
.withAlpha(_duration == d ? 200 : 100),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(_formatDuration(d)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _submit,
|
||||
child: Text(L10n.of(context).confirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DatePickerInput extends StatelessWidget {
|
||||
final String type;
|
||||
final TextEditingController controller;
|
||||
|
||||
const _DatePickerInput({
|
||||
required this.type,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 35.0,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
textAlign: TextAlign.end,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.all(0.0),
|
||||
hintText: "0",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 20.0,
|
||||
color: theme.colorScheme.onSurfaceVariant.withAlpha(100),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 20.0,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
Text(type, style: const TextStyle(fontSize: 20.0)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
272
lib/pangea/activities/activity_state_event.dart
Normal file
272
lib/pangea/activities/activity_state_event.dart
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_constants.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_duration_popup.dart';
|
||||
import 'package:fluffychat/pangea/activities/countdown.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class ActivityStateEvent extends StatefulWidget {
|
||||
final Event event;
|
||||
|
||||
const ActivityStateEvent({required this.event, super.key});
|
||||
|
||||
@override
|
||||
State<ActivityStateEvent> createState() => ActivityStateEventState();
|
||||
}
|
||||
|
||||
class ActivityStateEventState extends State<ActivityStateEvent> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
final delay = activityPlan?.endAt != null
|
||||
? activityPlan!.endAt!.difference(now)
|
||||
: null;
|
||||
|
||||
if (delay != null && delay > Duration.zero) {
|
||||
_timer = Timer(delay, () {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ActivityPlanModel? get activityPlan {
|
||||
try {
|
||||
return ActivityPlanModel.fromJson(widget.event.content);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get _activityIsOver {
|
||||
return activityPlan?.endAt != null &&
|
||||
DateTime.now().isAfter(activityPlan!.endAt!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (activityPlan == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
final double imageWidth = isColumnMode ? 240.0 : 175.0;
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 400.0,
|
||||
),
|
||||
margin: const EdgeInsets.all(18.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(_activityIsOver ? 24.0 : 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: _activityIsOver
|
||||
? Column(
|
||||
spacing: 12.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).activityEnded,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
CachedNetworkImage(
|
||||
width: 120.0,
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${ActivityConstants.activityFinishedAsset}",
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
activityPlan!.markdown,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontSize: AppConfig.fontSizeFactor *
|
||||
AppConfig.messageFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: _activityIsOver
|
||||
? const SizedBox()
|
||||
: IntrinsicHeight(
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Container(
|
||||
height: imageWidth,
|
||||
width: imageWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: activityPlan!.imageURL != null
|
||||
? activityPlan!.imageURL!.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(
|
||||
activityPlan!.imageURL!,
|
||||
),
|
||||
width: imageWidth,
|
||||
height: imageWidth,
|
||||
cacheKey: activityPlan!.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activityPlan!.imageURL!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) =>
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 9.0,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox.expand(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: () async {
|
||||
final Duration? duration =
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivityDurationPopup(
|
||||
initialValue:
|
||||
activityPlan?.duration ??
|
||||
const Duration(days: 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (duration == null) return;
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.event.room
|
||||
.sendActivityPlan(
|
||||
activityPlan!.copyWith(
|
||||
endAt:
|
||||
DateTime.now().add(duration),
|
||||
duration: duration,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CountDown(
|
||||
deadline: activityPlan!.endAt,
|
||||
iconSize: 20.0,
|
||||
textSize: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
), // Optional spacing between buttons
|
||||
Expanded(
|
||||
child: SizedBox.expand(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor:
|
||||
theme.colorScheme.error,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: () {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.event.room
|
||||
.sendActivityPlan(
|
||||
activityPlan!.copyWith(
|
||||
endAt: DateTime.now(),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
L10n.of(context).endNow,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
lib/pangea/activities/countdown.dart
Normal file
98
lib/pangea/activities/countdown.dart
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
||||
class CountDown extends StatefulWidget {
|
||||
final DateTime? deadline;
|
||||
|
||||
final double? iconSize;
|
||||
final double? textSize;
|
||||
|
||||
const CountDown({
|
||||
super.key,
|
||||
required this.deadline,
|
||||
this.iconSize,
|
||||
this.textSize,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CountDown> createState() => CountDownState();
|
||||
}
|
||||
|
||||
class CountDownState extends State<CountDown> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? _formatDuration(Duration duration) {
|
||||
final days = duration.inDays;
|
||||
final hours = duration.inHours.remainder(24);
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
|
||||
final List<String> parts = [];
|
||||
if (days > 0) parts.add("${days}d");
|
||||
if (hours > 0) parts.add("${hours}h");
|
||||
if (minutes > 0) parts.add("${minutes}m");
|
||||
if (seconds > 0 && minutes <= 0) parts.add("${seconds}s");
|
||||
if (parts.isEmpty) return null;
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
Duration? get _remainingTime {
|
||||
if (widget.deadline == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
return widget.deadline!.difference(now);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final remainingTime = _remainingTime;
|
||||
final durationString = _formatDuration(remainingTime ?? Duration.zero);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 250.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer_outlined,
|
||||
size: widget.iconSize ?? 28.0,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
remainingTime != null &&
|
||||
remainingTime >= Duration.zero &&
|
||||
durationString != null
|
||||
? durationString
|
||||
: L10n.of(context).duration,
|
||||
style: TextStyle(fontSize: widget.textSize ?? 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
100
lib/pangea/activities/pinned_activity_message.dart
Normal file
100
lib/pangea/activities/pinned_activity_message.dart
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_aware_builder.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_duration_popup.dart';
|
||||
import 'package:fluffychat/pangea/activities/countdown.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class PinnedActivityMessage extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const PinnedActivityMessage(this.controller, {super.key});
|
||||
|
||||
Future<void> _scrollToEvent() async {
|
||||
final eventId = _activityPlanEvent?.eventId;
|
||||
if (eventId != null) controller.scrollToEventId(eventId);
|
||||
}
|
||||
|
||||
Event? get _activityPlanEvent => controller.timeline?.events.firstWhereOrNull(
|
||||
(event) => event.type == PangeaEventTypes.activityPlan,
|
||||
);
|
||||
|
||||
ActivityPlanModel? get _activityPlan => controller.room.activityPlan;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ActivityAwareBuilder(
|
||||
deadline: _activityPlan?.endAt,
|
||||
builder: (isActive) {
|
||||
if (!isActive || _activityPlan == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ChatAppBarListTile(
|
||||
title: _activityPlan!.title,
|
||||
leading: IconButton(
|
||||
splashRadius: 18,
|
||||
iconSize: 18,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.push_pin),
|
||||
onPressed: () {},
|
||||
),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () async {
|
||||
final Duration? duration = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivityDurationPopup(
|
||||
initialValue:
|
||||
_activityPlan?.duration ?? const Duration(days: 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (duration == null) return;
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => controller.room.sendActivityPlan(
|
||||
_activityPlan!.copyWith(
|
||||
endAt: DateTime.now().add(duration),
|
||||
duration: duration,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: CountDown(
|
||||
deadline: _activityPlan!.endAt,
|
||||
iconSize: 16.0,
|
||||
textSize: 14.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: _scrollToEvent,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@ class ActivityPlanModel {
|
|||
final String instructions;
|
||||
final List<Vocab> vocab;
|
||||
final String? imageURL;
|
||||
final DateTime? endAt;
|
||||
final Duration? duration;
|
||||
|
||||
ActivityPlanModel({
|
||||
required this.req,
|
||||
|
|
@ -19,31 +21,70 @@ class ActivityPlanModel {
|
|||
required this.instructions,
|
||||
required this.vocab,
|
||||
this.imageURL,
|
||||
this.endAt,
|
||||
this.duration,
|
||||
}) : bookmarkId =
|
||||
"${title.hashCode ^ learningObjective.hashCode ^ instructions.hashCode ^ imageURL.hashCode ^ vocab.map((v) => v.hashCode).reduce((a, b) => a ^ b)}";
|
||||
|
||||
ActivityPlanModel copyWith({
|
||||
String? title,
|
||||
String? learningObjective,
|
||||
String? instructions,
|
||||
List<Vocab>? vocab,
|
||||
String? imageURL,
|
||||
DateTime? endAt,
|
||||
Duration? duration,
|
||||
}) {
|
||||
return ActivityPlanModel(
|
||||
req: req,
|
||||
title: title ?? this.title,
|
||||
learningObjective: learningObjective ?? this.learningObjective,
|
||||
instructions: instructions ?? this.instructions,
|
||||
vocab: vocab ?? this.vocab,
|
||||
imageURL: imageURL ?? this.imageURL,
|
||||
endAt: endAt ?? this.endAt,
|
||||
duration: duration ?? this.duration,
|
||||
);
|
||||
}
|
||||
|
||||
factory ActivityPlanModel.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityPlanModel(
|
||||
imageURL: json[ModelKey.activityPlanImageURL],
|
||||
instructions: json[ModelKey.activityPlanInstructions],
|
||||
req: ActivityPlanRequest.fromJson(json[ModelKey.activityPlanRequest]),
|
||||
title: json[ModelKey.activityPlanTitle],
|
||||
learningObjective: json[ModelKey.activityPlanLearningObjective],
|
||||
instructions: json[ModelKey.activityPlanInstructions],
|
||||
vocab: List<Vocab>.from(
|
||||
json[ModelKey.activityPlanVocab].map((vocab) => Vocab.fromJson(vocab)),
|
||||
),
|
||||
imageURL: json[ModelKey.activityPlanImageURL],
|
||||
endAt: json[ModelKey.activityPlanEndAt] != null
|
||||
? DateTime.parse(json[ModelKey.activityPlanEndAt])
|
||||
: null,
|
||||
duration: json[ModelKey.activityPlanDuration] != null
|
||||
? Duration(
|
||||
days: json[ModelKey.activityPlanDuration]['days'] ?? 0,
|
||||
hours: json[ModelKey.activityPlanDuration]['hours'] ?? 0,
|
||||
minutes: json[ModelKey.activityPlanDuration]['minutes'] ?? 0,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
ModelKey.activityPlanBookmarkId: bookmarkId,
|
||||
ModelKey.activityPlanImageURL: imageURL,
|
||||
ModelKey.activityPlanInstructions: instructions,
|
||||
ModelKey.activityPlanRequest: req.toJson(),
|
||||
ModelKey.activityPlanTitle: title,
|
||||
ModelKey.activityPlanLearningObjective: learningObjective,
|
||||
ModelKey.activityPlanInstructions: instructions,
|
||||
ModelKey.activityPlanVocab: vocab.map((vocab) => vocab.toJson()).toList(),
|
||||
ModelKey.activityPlanImageURL: imageURL,
|
||||
ModelKey.activityPlanBookmarkId: bookmarkId,
|
||||
ModelKey.activityPlanEndAt: endAt?.toIso8601String(),
|
||||
ModelKey.activityPlanDuration: {
|
||||
'days': duration?.inDays ?? 0,
|
||||
'hours': duration?.inHours.remainder(24) ?? 0,
|
||||
'minutes': duration?.inMinutes.remainder(60) ?? 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,13 @@ class ActivityRoomSelectionState extends State<ActivityRoomSelection> {
|
|||
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||
});
|
||||
|
||||
final room = widget.controller.room;
|
||||
if (room != null && room.isSpace) {
|
||||
_launchableRooms = _launchableRooms.where((r) {
|
||||
return room.spaceChildren.any((child) => child.roomId == r.id);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
_hasBotDM = Matrix.of(context).client.rooms.any((room) {
|
||||
if (room.isDirectChat &&
|
||||
room.directChatMatrixID == BotName.byEnvironment) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart';
|
|||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -26,6 +28,9 @@ class VocabDetailsView extends StatelessWidget {
|
|||
|
||||
ConstructUses get _construct => constructId.constructUses;
|
||||
|
||||
String? get _userL1 =>
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode;
|
||||
|
||||
/// Get the language code for the current lemma
|
||||
String? get _userL2 =>
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
|
@ -49,14 +54,34 @@ class VocabDetailsView extends StatelessWidget {
|
|||
: _construct.lemmaCategory.darkColor(context));
|
||||
|
||||
return AnalyticsDetailsViewContent(
|
||||
title: WordTextWithAudioButton(
|
||||
text: _construct.lemma,
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
color: textColor,
|
||||
title: Column(
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return ShrinkableText(
|
||||
text: _construct.lemma,
|
||||
maxWidth: constraints.maxWidth - 40.0,
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
color: textColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (MatrixState.pangeaController.languageController.showTrancription)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: PhoneticTranscriptionWidget(
|
||||
text: _construct.lemma,
|
||||
textLanguage:
|
||||
MatrixState.pangeaController.languageController.userL2!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: textColor.withAlpha((0.7 * 255).toInt()),
|
||||
fontSize: 18,
|
||||
),
|
||||
iconSize: _iconSize * 0.8,
|
||||
),
|
||||
),
|
||||
iconSize: _iconSize,
|
||||
uniqueID: "${_construct.lemma}-${_construct.category}",
|
||||
langCode: _userL2!,
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -13,14 +13,17 @@ class ProgressBarBackground extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: details.height,
|
||||
width: details.totalWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Container(
|
||||
height: details.height,
|
||||
width: details.totalWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
color: details.borderColor.withAlpha(50),
|
||||
),
|
||||
color: details.borderColor.withAlpha(50),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -304,7 +304,6 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
final double _buttonWidth = 125.0;
|
||||
final double _buttonHeight = 84.0;
|
||||
final double _miniButtonWidth = 50.0;
|
||||
|
||||
|
|
@ -327,6 +326,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
onPressed: () => context.go('/rooms/${room.id}/details/permissions'),
|
||||
visible: (room.isRoomAdmin && !room.isDirectChat) || room.isSpace,
|
||||
enabled: room.isRoomAdmin && !room.isDirectChat,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.access,
|
||||
|
|
@ -389,6 +389,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
icon: const Icon(Icons.download_outlined, size: 30.0),
|
||||
onPressed: widget.controller.downloadChatAction,
|
||||
visible: room.ownPowerLevel >= 50 && !room.isSpace,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.botSettings,
|
||||
|
|
@ -411,6 +412,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
onPressed: widget.controller.setRoomCapacity,
|
||||
visible:
|
||||
!room.isSpace && !room.isDirectChat && room.canSendDefaultStates,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.leave,
|
||||
|
|
@ -492,24 +494,15 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth;
|
||||
final fullButtonCapacity =
|
||||
(availableWidth / _buttonWidth).floor() - 1;
|
||||
final miniButtonCapacity =
|
||||
(availableWidth / _miniButtonWidth).floor() - 1;
|
||||
final fullButtonCapacity = (availableWidth / 120.0).floor() - 1;
|
||||
|
||||
final mini = fullButtonCapacity < 4;
|
||||
final capacity = mini ? miniButtonCapacity : fullButtonCapacity;
|
||||
|
||||
List<ButtonDetails> mainViewButtons =
|
||||
final List<ButtonDetails> mainViewButtons =
|
||||
buttons.where((button) => button.showInMainView).toList();
|
||||
final List<ButtonDetails> otherButtons =
|
||||
buttons.where((button) => !button.showInMainView).toList();
|
||||
|
||||
if (capacity < mainViewButtons.length) {
|
||||
otherButtons.addAll(mainViewButtons.skip(capacity));
|
||||
mainViewButtons = mainViewButtons.take(capacity).toList();
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(mainViewButtons.length + 1, (index) {
|
||||
|
|
@ -518,44 +511,46 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
return const SizedBox();
|
||||
}
|
||||
|
||||
return PopupMenuButton(
|
||||
useRootNavigator: true,
|
||||
onSelected: (button) => button.onPressed?.call(),
|
||||
itemBuilder: (context) {
|
||||
return otherButtons
|
||||
.map(
|
||||
(button) => PopupMenuItem(
|
||||
value: button,
|
||||
child: Row(
|
||||
children: [
|
||||
button.icon,
|
||||
const SizedBox(width: 8),
|
||||
Text(button.title),
|
||||
],
|
||||
return Expanded(
|
||||
child: PopupMenuButton(
|
||||
useRootNavigator: true,
|
||||
onSelected: (button) => button.onPressed?.call(),
|
||||
itemBuilder: (context) {
|
||||
return otherButtons
|
||||
.map(
|
||||
(button) => PopupMenuItem(
|
||||
value: button,
|
||||
child: Row(
|
||||
children: [
|
||||
button.icon,
|
||||
const SizedBox(width: 8),
|
||||
Text(button.title),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: ButtonDetails(
|
||||
title: L10n.of(context).more,
|
||||
icon: const Icon(Icons.more_horiz_outlined),
|
||||
visible: true,
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: ButtonDetails(
|
||||
title: L10n.of(context).more,
|
||||
icon: const Icon(Icons.more_horiz_outlined),
|
||||
visible: true,
|
||||
),
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
),
|
||||
width: mini ? _miniButtonWidth : _buttonWidth,
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final button = buttons[index];
|
||||
return RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: button,
|
||||
width: mini ? _miniButtonWidth : _buttonWidth,
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
return Expanded(
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: button,
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
@ -567,14 +562,6 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
|
||||
class RoomDetailsButton extends StatelessWidget {
|
||||
final bool mini;
|
||||
// final bool visible;
|
||||
// final bool enabled;
|
||||
|
||||
// final String title;
|
||||
// final Widget icon;
|
||||
// final VoidCallback? onPressed;
|
||||
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
final ButtonDetails buttonDetails;
|
||||
|
|
@ -582,14 +569,8 @@ class RoomDetailsButton extends StatelessWidget {
|
|||
const RoomDetailsButton({
|
||||
super.key,
|
||||
required this.buttonDetails,
|
||||
// required this.visible,
|
||||
// required this.title,
|
||||
// required this.icon,
|
||||
required this.mini,
|
||||
required this.width,
|
||||
required this.height,
|
||||
// this.enabled = true,
|
||||
// this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -613,7 +594,7 @@ class RoomDetailsButton extends StatelessWidget {
|
|||
child: Opacity(
|
||||
opacity: buttonDetails.enabled ? 1.0 : 0.5,
|
||||
child: Container(
|
||||
width: width,
|
||||
alignment: Alignment.center,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: hovered
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ class ModelKey {
|
|||
static const String activityPlanVocab = "vocab";
|
||||
static const String activityPlanImageURL = "image_url";
|
||||
static const String activityPlanBookmarkId = "bookmark_id";
|
||||
static const String activityPlanEndAt = "end_at";
|
||||
static const String activityPlanDuration = "duration";
|
||||
|
||||
static const String activityRequestTopic = "topic";
|
||||
static const String activityRequestMode = "mode";
|
||||
|
|
|
|||
|
|
@ -86,4 +86,7 @@ class PApiUrls {
|
|||
static String rcProductsTrial = "${PApiUrls.subscriptionEndpoint}/free_trial";
|
||||
|
||||
static String rcSubscription = PApiUrls.subscriptionEndpoint;
|
||||
|
||||
static String phoneticTranscription =
|
||||
"${PApiUrls.choreoEndpoint}/phonetic_transcription";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@ class PangeaTokenText {
|
|||
);
|
||||
}
|
||||
|
||||
static PangeaTokenText fromString(String content) {
|
||||
return PangeaTokenText(
|
||||
offset: 0,
|
||||
content: content,
|
||||
length: content.length,
|
||||
);
|
||||
}
|
||||
|
||||
static const String _offsetKey = "offset";
|
||||
static const String _contentKey = "content";
|
||||
static const String _lengthKey = "length";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
|
|
|||
|
|
@ -277,68 +277,13 @@ extension EventsRoomExtension on Room {
|
|||
}) async {
|
||||
BookmarkedActivitiesRepo.save(activity);
|
||||
|
||||
String? imageURL = activity.imageURL;
|
||||
final eventId = await pangeaSendTextEvent(
|
||||
activity.markdown,
|
||||
messageTag: ModelKey.messageTagActivityPlan,
|
||||
);
|
||||
|
||||
Uint8List? bytes = avatar;
|
||||
if (imageURL != null && bytes == null) {
|
||||
try {
|
||||
final resp = await http
|
||||
.get(Uri.parse(imageURL))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
bytes = resp.bodyBytes;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"avatarURL": imageURL,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes != null && imageURL == null) {
|
||||
final url = await client.uploadContent(
|
||||
bytes,
|
||||
filename: filename,
|
||||
);
|
||||
imageURL = url.toString();
|
||||
}
|
||||
|
||||
MatrixFile? file;
|
||||
if (filename != null && bytes != null) {
|
||||
file = MatrixFile(
|
||||
bytes: bytes,
|
||||
name: filename,
|
||||
);
|
||||
}
|
||||
|
||||
if (file != null) {
|
||||
final content = <String, dynamic>{
|
||||
'msgtype': file.msgType,
|
||||
'body': file.name,
|
||||
'filename': file.name,
|
||||
'url': imageURL,
|
||||
ModelKey.messageTags: ModelKey.messageTagActivityPlan,
|
||||
};
|
||||
await sendEvent(content);
|
||||
}
|
||||
|
||||
if (canSendDefaultStates) {
|
||||
if (canChangeStateEvent(PangeaEventTypes.activityPlan)) {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityPlan,
|
||||
"",
|
||||
activity.toJson(),
|
||||
);
|
||||
|
||||
if (eventId != null) {
|
||||
await setPinnedEvents([eventId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,4 +112,14 @@ class LanguageController {
|
|||
// final model = activeL2 != null ? PangeaLanguage.byLangCode(activeL2) : null;
|
||||
// return model;
|
||||
}
|
||||
|
||||
bool get showTrancription =>
|
||||
(_pangeaController.languageController.userL1 != null &&
|
||||
_pangeaController.languageController.userL2 != null &&
|
||||
_pangeaController.languageController.userL1?.script !=
|
||||
_pangeaController.languageController.userL2?.script) ||
|
||||
(_pangeaController.languageController.userL1?.script !=
|
||||
LanguageKeys.unknownLanguage ||
|
||||
_pangeaController.languageController.userL2?.script ==
|
||||
LanguageKeys.unknownLanguage);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ import 'package:fluffychat/pangea/learning_settings/enums/l2_support_enum.dart';
|
|||
class LanguageModel {
|
||||
final String langCode;
|
||||
final String displayName;
|
||||
final String script;
|
||||
final L2SupportEnum l2Support;
|
||||
|
||||
LanguageModel({
|
||||
required this.langCode,
|
||||
required this.displayName,
|
||||
this.script = LanguageKeys.unknownLanguage,
|
||||
this.l2Support = L2SupportEnum.na,
|
||||
});
|
||||
|
||||
|
|
@ -28,12 +30,14 @@ class LanguageModel {
|
|||
l2Support: json['l2_support'] != null
|
||||
? L2SupportEnum.na.fromStorageString(json['l2_support'])
|
||||
: L2SupportEnum.na,
|
||||
script: json['script'] ?? LanguageKeys.unknownLanguage,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() => {
|
||||
'language_code': langCode,
|
||||
'language_name': displayName,
|
||||
'script': script,
|
||||
'l2_support': l2Support.storageString,
|
||||
};
|
||||
|
||||
|
|
@ -80,3 +84,27 @@ class LanguageModel {
|
|||
@override
|
||||
int get hashCode => langCode.hashCode;
|
||||
}
|
||||
|
||||
class LanguageArc {
|
||||
final LanguageModel l1;
|
||||
final LanguageModel l2;
|
||||
|
||||
LanguageArc({
|
||||
required this.l1,
|
||||
required this.l2,
|
||||
});
|
||||
|
||||
factory LanguageArc.fromJson(Map<String, dynamic> json) {
|
||||
return LanguageArc(
|
||||
l1: LanguageModel.fromJson(json['l1'] as Map<String, dynamic>),
|
||||
l2: LanguageModel.fromJson(json['l2'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'l1': l1.toJson(),
|
||||
'l2': l2.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,13 +192,15 @@ class SettingsLearningView extends StatelessWidget {
|
|||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context)
|
||||
.noIdenticalLanguages,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context)
|
||||
.noIdenticalLanguages,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -239,15 +239,17 @@ class LanguageDropDownEntry extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
languageModel.getDisplayName(context) ?? "",
|
||||
style: const TextStyle().copyWith(
|
||||
color: enabled
|
||||
? Theme.of(context).textTheme.bodyLarge!.color
|
||||
: Theme.of(context).disabledColor,
|
||||
fontSize: 14,
|
||||
Flexible(
|
||||
child: Text(
|
||||
languageModel.getDisplayName(context) ?? "",
|
||||
style: const TextStyle().copyWith(
|
||||
color: enabled
|
||||
? Theme.of(context).textTheme.bodyLarge!.color
|
||||
: Theme.of(context).disabledColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (isL2List && languageModel.l2Support != L2SupportEnum.full)
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class ConstructXpWidgetState extends State<ConstructXpWidget>
|
|||
Stream<AnalyticsStreamUpdate> get stream =>
|
||||
MatrixState.pangeaController.getAnalytics.analyticsStream.stream;
|
||||
|
||||
Widget get svg => constructLemmaCategory?.icon() ?? const SizedBox();
|
||||
Widget? get svg => constructLemmaCategory?.icon();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
@ -106,9 +106,10 @@ class ConstructXpWidgetState extends State<ConstructXpWidget>
|
|||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onTap: svg != null ? widget.onTap : null,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
cursor:
|
||||
svg != null ? SystemMouseCursors.click : SystemMouseCursors.basic,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -28,17 +28,35 @@ class LemmaReactionPickerState extends State<LemmaReactionPicker> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.cId.getLemmaInfo().then((info) {
|
||||
loading = false;
|
||||
setState(() => displayEmoji = info.emoji);
|
||||
}).catchError((e, s) {
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
setState(() => loading = false);
|
||||
});
|
||||
_refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LemmaReactionPicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.cId != widget.cId) {
|
||||
_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void setEmoji(String emoji) => widget.controller.sendEmojiAction(emoji);
|
||||
|
||||
Future<void> _refresh() async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
displayEmoji = [];
|
||||
});
|
||||
|
||||
try {
|
||||
final info = await widget.cId.getLemmaInfo();
|
||||
displayEmoji = info.emoji;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ class OnboardingComplete extends StatelessWidget {
|
|||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha(20),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh
|
||||
.withAlpha(170),
|
||||
borderRadius: BorderRadius.circular(
|
||||
10.0,
|
||||
),
|
||||
|
|
@ -38,7 +41,7 @@ class OnboardingComplete extends StatelessWidget {
|
|||
L10n.of(context).getStartedComplete,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontSize: 14,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PhoneticTranscriptionRepo {
|
||||
static final GetStorage _storage =
|
||||
GetStorage('phonetic_transcription_storage');
|
||||
|
||||
static void set(
|
||||
PhoneticTranscriptionRequest request,
|
||||
PhoneticTranscriptionResponse response,
|
||||
) {
|
||||
response.expireAt ??= DateTime.now().add(const Duration(days: 100));
|
||||
_storage.write(request.storageKey, response.toJson());
|
||||
}
|
||||
|
||||
static Future<PhoneticTranscriptionResponse> _fetch(
|
||||
PhoneticTranscriptionRequest request,
|
||||
) async {
|
||||
final cachedJson = _storage.read(request.storageKey);
|
||||
final cached = cachedJson == null
|
||||
? null
|
||||
: PhoneticTranscriptionResponse.fromJson(cachedJson);
|
||||
|
||||
if (cached != null) {
|
||||
if (DateTime.now().isBefore(cached.expireAt!)) {
|
||||
return cached;
|
||||
} else {
|
||||
_storage.remove(request.storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.phoneticTranscription,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = PhoneticTranscriptionResponse.fromJson(decodedBody);
|
||||
set(request, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
static Future<PhoneticTranscriptionResponse> get(
|
||||
PhoneticTranscriptionRequest request,
|
||||
) async {
|
||||
try {
|
||||
return await _fetch(request);
|
||||
} catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, data: request.toJson());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
||||
class PhoneticTranscriptionRequest {
|
||||
final LanguageArc arc;
|
||||
final PangeaTokenText content;
|
||||
final bool requiresTokenization;
|
||||
|
||||
PhoneticTranscriptionRequest({
|
||||
required this.arc,
|
||||
required this.content,
|
||||
this.requiresTokenization = false,
|
||||
});
|
||||
|
||||
factory PhoneticTranscriptionRequest.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneticTranscriptionRequest(
|
||||
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
|
||||
content:
|
||||
PangeaTokenText.fromJson(json['content'] as Map<String, dynamic>),
|
||||
requiresTokenization: json['requires_tokenization'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'arc': arc.toJson(),
|
||||
'content': content.toJson(),
|
||||
'requires_tokenization': requiresTokenization,
|
||||
};
|
||||
}
|
||||
|
||||
String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}';
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
||||
enum PhoneticTranscriptionDelimEnum { sp, noSp }
|
||||
|
||||
extension PhoneticTranscriptionDelimEnumExt on PhoneticTranscriptionDelimEnum {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case PhoneticTranscriptionDelimEnum.sp:
|
||||
return " ";
|
||||
case PhoneticTranscriptionDelimEnum.noSp:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static PhoneticTranscriptionDelimEnum fromString(String s) {
|
||||
switch (s) {
|
||||
case " ":
|
||||
return PhoneticTranscriptionDelimEnum.sp;
|
||||
case "":
|
||||
return PhoneticTranscriptionDelimEnum.noSp;
|
||||
default:
|
||||
return PhoneticTranscriptionDelimEnum.sp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneticTranscriptionToken {
|
||||
final LanguageArc arc;
|
||||
final PangeaTokenText tokenL2;
|
||||
final PangeaTokenText phoneticL1Transcription;
|
||||
|
||||
PhoneticTranscriptionToken({
|
||||
required this.arc,
|
||||
required this.tokenL2,
|
||||
required this.phoneticL1Transcription,
|
||||
});
|
||||
|
||||
factory PhoneticTranscriptionToken.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneticTranscriptionToken(
|
||||
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
|
||||
tokenL2:
|
||||
PangeaTokenText.fromJson(json['token_l2'] as Map<String, dynamic>),
|
||||
phoneticL1Transcription: PangeaTokenText.fromJson(
|
||||
json['phonetic_l1_transcription'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'arc': arc.toJson(),
|
||||
'token_l2': tokenL2.toJson(),
|
||||
'phonetic_l1_transcription': phoneticL1Transcription.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
class PhoneticTranscription {
|
||||
final LanguageArc arc;
|
||||
final PangeaTokenText transcriptionL2;
|
||||
final List<PhoneticTranscriptionToken> phoneticTranscription;
|
||||
final PhoneticTranscriptionDelimEnum delim;
|
||||
|
||||
PhoneticTranscription({
|
||||
required this.arc,
|
||||
required this.transcriptionL2,
|
||||
required this.phoneticTranscription,
|
||||
this.delim = PhoneticTranscriptionDelimEnum.sp,
|
||||
});
|
||||
|
||||
factory PhoneticTranscription.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneticTranscription(
|
||||
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
|
||||
transcriptionL2: PangeaTokenText.fromJson(
|
||||
json['transcription_l2'] as Map<String, dynamic>,
|
||||
),
|
||||
phoneticTranscription: (json['phonetic_transcription'] as List)
|
||||
.map(
|
||||
(e) =>
|
||||
PhoneticTranscriptionToken.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
delim: json['delim'] != null
|
||||
? PhoneticTranscriptionDelimEnumExt.fromString(
|
||||
json['delim'] as String,
|
||||
)
|
||||
: PhoneticTranscriptionDelimEnum.sp,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'arc': arc.toJson(),
|
||||
'transcription_l2': transcriptionL2.toJson(),
|
||||
'phonetic_transcription':
|
||||
phoneticTranscription.map((e) => e.toJson()).toList(),
|
||||
'delim': delim.value,
|
||||
};
|
||||
}
|
||||
|
||||
class PhoneticTranscriptionResponse {
|
||||
final LanguageArc arc;
|
||||
final PangeaTokenText content;
|
||||
final Map<String, dynamic>
|
||||
tokenization; // You can define a typesafe model if needed
|
||||
final PhoneticTranscription phoneticTranscriptionResult;
|
||||
DateTime? expireAt;
|
||||
|
||||
PhoneticTranscriptionResponse({
|
||||
required this.arc,
|
||||
required this.content,
|
||||
required this.tokenization,
|
||||
required this.phoneticTranscriptionResult,
|
||||
this.expireAt,
|
||||
});
|
||||
|
||||
factory PhoneticTranscriptionResponse.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneticTranscriptionResponse(
|
||||
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
|
||||
content:
|
||||
PangeaTokenText.fromJson(json['content'] as Map<String, dynamic>),
|
||||
tokenization: Map<String, dynamic>.from(json['tokenization'] as Map),
|
||||
phoneticTranscriptionResult: PhoneticTranscription.fromJson(
|
||||
json['phonetic_transcription_result'] as Map<String, dynamic>,
|
||||
),
|
||||
expireAt: json['expireAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expireAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'arc': arc.toJson(),
|
||||
'content': content.toJson(),
|
||||
'tokenization': tokenization,
|
||||
'phonetic_transcription_result': phoneticTranscriptionResult.toJson(),
|
||||
'expireAt': expireAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PhoneticTranscriptionResponse &&
|
||||
runtimeType == other.runtimeType &&
|
||||
arc == other.arc &&
|
||||
content == other.content &&
|
||||
tokenization == other.tokenization &&
|
||||
phoneticTranscriptionResult == other.phoneticTranscriptionResult;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
arc.hashCode ^
|
||||
content.hashCode ^
|
||||
tokenization.hashCode ^
|
||||
phoneticTranscriptionResult.hashCode;
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PhoneticTranscriptionWidget extends StatefulWidget {
|
||||
final String text;
|
||||
final LanguageModel textLanguage;
|
||||
final TextStyle? style;
|
||||
final double? iconSize;
|
||||
final Color? iconColor;
|
||||
|
||||
const PhoneticTranscriptionWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.textLanguage,
|
||||
this.style,
|
||||
this.iconSize,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PhoneticTranscriptionWidget> createState() =>
|
||||
_PhoneticTranscriptionWidgetState();
|
||||
}
|
||||
|
||||
class _PhoneticTranscriptionWidgetState
|
||||
extends State<PhoneticTranscriptionWidget> {
|
||||
bool _isPlaying = false;
|
||||
bool _isLoading = false;
|
||||
Object? _error;
|
||||
|
||||
String? _transcription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchTranscription();
|
||||
}
|
||||
|
||||
Future<void> _fetchTranscription() async {
|
||||
try {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
_transcription = null;
|
||||
});
|
||||
|
||||
if (MatrixState.pangeaController.languageController.userL1 == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception('User L1 is not set'),
|
||||
data: {
|
||||
'text': widget.text,
|
||||
'textLanguageCode': widget.textLanguage.langCode,
|
||||
},
|
||||
);
|
||||
_error = Exception('User L1 is not set');
|
||||
return;
|
||||
}
|
||||
final req = PhoneticTranscriptionRequest(
|
||||
arc: LanguageArc(
|
||||
l1: MatrixState.pangeaController.languageController.userL1!,
|
||||
l2: widget.textLanguage,
|
||||
),
|
||||
content: PangeaTokenText.fromString(widget.text),
|
||||
// arc can be omitted for default empty map
|
||||
);
|
||||
final res = await PhoneticTranscriptionRepo.get(req);
|
||||
_transcription = res.phoneticTranscriptionResult.phoneticTranscription
|
||||
.first.phoneticL1Transcription.content;
|
||||
} catch (e, s) {
|
||||
_error = e;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'text': widget.text,
|
||||
'textLanguageCode': widget.textLanguage.langCode,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleAudioTap(BuildContext context) async {
|
||||
if (_isPlaying) {
|
||||
await TtsController.stop();
|
||||
setState(() => _isPlaying = false);
|
||||
} else {
|
||||
await TtsController.tryToSpeak(
|
||||
widget.text,
|
||||
context: context,
|
||||
targetID: 'phonetic-transcription-${widget.text}',
|
||||
langCode: widget.textLanguage.langCode,
|
||||
onStart: () {
|
||||
if (mounted) setState(() => _isPlaying = true);
|
||||
},
|
||||
onStop: () {
|
||||
if (mounted) setState(() => _isPlaying = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HoverBuilder(
|
||||
builder: (context, hovering) {
|
||||
return GestureDetector(
|
||||
onTap: () => _handleAudioTap(context),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: hovering
|
||||
? Colors.grey.withAlpha((0.2 * 255).round())
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_error != null)
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: widget.iconSize ?? 24,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).failedToFetchTranscription,
|
||||
style: widget.style,
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (_isLoading || _transcription == null)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: Text(
|
||||
"/$_transcription/",
|
||||
style: widget.style ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (_transcription != null && _error == null)
|
||||
Tooltip(
|
||||
message: _isPlaying
|
||||
? L10n.of(context).stop
|
||||
: L10n.of(context).playAudio,
|
||||
child: Icon(
|
||||
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
|
||||
size: widget.iconSize ?? 24,
|
||||
color:
|
||||
widget.iconColor ?? Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -190,4 +190,20 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
return null; // TODO: Add to L10n
|
||||
}
|
||||
}
|
||||
|
||||
/// The minimum number of tokens in a message for this activity type to be available.
|
||||
/// Matching activities don't make sense for a single-word message.
|
||||
int get minTokensForMatchActivity {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return 2;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.morphId:
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,11 @@ class PracticeSelection {
|
|||
return [];
|
||||
}
|
||||
|
||||
if (tokens.length < activityType.minTokensForMatchActivity) {
|
||||
// if we only have one token, we don't need to do an emoji activity
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
PracticeTarget(
|
||||
activityType: activityType,
|
||||
|
|
|
|||
|
|
@ -200,12 +200,14 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
|
|||
Row(
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
(chunk?.avatarUrl != null)
|
||||
(chunk?.avatarUrl != null || chunk?.roomType != 'm.space')
|
||||
? Avatar(
|
||||
mxContent: chunk?.avatarUrl,
|
||||
name: chunk?.name,
|
||||
size: 160.0,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderRadius: BorderRadius.circular(
|
||||
chunk?.roomType != 'm.space' ? 80 : 24.0,
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
|
|
@ -242,7 +244,11 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
|
|||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
chunk?.topic ??
|
||||
L10n.of(context).noSpaceDescriptionYet,
|
||||
(chunk?.roomType != 'm.space'
|
||||
? L10n.of(context)
|
||||
.noChatDescriptionYet
|
||||
: L10n.of(context)
|
||||
.noSpaceDescriptionYet),
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: null,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
|
||||
|
|
@ -89,15 +90,16 @@ class KnockingUsersIndicatorState extends State<KnockingUsersIndicator> {
|
|||
Expanded(
|
||||
child: Text(
|
||||
_knockingUsers.length == 1
|
||||
? "1 user is requesting to join your space"
|
||||
: "${_knockingUsers.length} users are requesting to join your space",
|
||||
? L10n.of(context).aUserIsKnocking
|
||||
: L10n.of(context)
|
||||
.usersAreKnocking(_knockingUsers.length),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => context.push(
|
||||
"/rooms/${widget.room.id}/details/members",
|
||||
"/rooms/${widget.room.id}/details/members?filter=knock",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ class LeaderboardParticipantListState
|
|||
return LoadParticipantsUtil(
|
||||
space: widget.space,
|
||||
builder: (participantsLoader) {
|
||||
final participants = participantsLoader.filteredParticipants("");
|
||||
final participants = participantsLoader
|
||||
.filteredParticipants("")
|
||||
.where((p) => p.membership == Membership.join)
|
||||
.toList();
|
||||
|
||||
return AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ class TtsController {
|
|||
static final StreamController<bool> loadingChoreoStream =
|
||||
StreamController<bool>.broadcast();
|
||||
|
||||
static AudioPlayer? audioPlayer;
|
||||
|
||||
static bool get _useAlternativeTTS {
|
||||
return PlatformInfos.isWindows;
|
||||
}
|
||||
|
|
@ -120,6 +122,7 @@ class TtsController {
|
|||
// https://pub.dev/packages/flutter_tts
|
||||
final result =
|
||||
await (_useAlternativeTTS ? _alternativeTTS.stop() : _tts.stop());
|
||||
audioPlayer?.stop();
|
||||
|
||||
if (!_useAlternativeTTS && result != 1) {
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -170,8 +173,6 @@ class TtsController {
|
|||
onStart: onStart,
|
||||
onStop: onStop,
|
||||
);
|
||||
|
||||
onStop?.call();
|
||||
}
|
||||
|
||||
/// A safer version of speak, that handles the case of
|
||||
|
|
@ -187,6 +188,8 @@ class TtsController {
|
|||
VoidCallback? onStop,
|
||||
}) async {
|
||||
chatController?.stopMediaStream.add(null);
|
||||
MatrixState.pangeaController.matrixState.audioPlayer?.stop();
|
||||
|
||||
await _setSpeakingLanguage(langCode);
|
||||
|
||||
final enableTTS = MatrixState
|
||||
|
|
@ -306,17 +309,18 @@ class TtsController {
|
|||
|
||||
if (ttsRes == null) return;
|
||||
|
||||
final audioPlayer = AudioPlayer();
|
||||
try {
|
||||
Logs().i('Speaking from choreo: $text, langCode: $langCode');
|
||||
final audioContent = base64Decode(ttsRes.audioContent);
|
||||
await audioPlayer.setAudioSource(
|
||||
audioPlayer?.dispose();
|
||||
audioPlayer = AudioPlayer();
|
||||
await audioPlayer!.setAudioSource(
|
||||
BytesAudioSource(
|
||||
audioContent,
|
||||
ttsRes.mimeType,
|
||||
),
|
||||
);
|
||||
await audioPlayer.play();
|
||||
await audioPlayer!.play();
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: 'Error playing audio',
|
||||
|
|
@ -327,7 +331,8 @@ class TtsController {
|
|||
},
|
||||
);
|
||||
} finally {
|
||||
await audioPlayer.dispose();
|
||||
audioPlayer?.dispose();
|
||||
audioPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class TokenRenderingUtil {
|
|||
return readingAssistanceMode == ReadingAssistanceMode.transitionMode;
|
||||
}
|
||||
|
||||
double? _fontSize(BuildContext context) => showCenterStyling
|
||||
double? fontSize(BuildContext context) => showCenterStyling
|
||||
? overlayController != null && overlayController!.maxWidth > 600
|
||||
? Theme.of(context).textTheme.titleLarge?.fontSize
|
||||
: Theme.of(context).textTheme.bodyLarge?.fontSize
|
||||
|
|
@ -38,14 +38,14 @@ class TokenRenderingUtil {
|
|||
Color? color,
|
||||
}) =>
|
||||
existingStyle.copyWith(
|
||||
fontSize: _fontSize(context),
|
||||
fontSize: fontSize(context),
|
||||
decoration: TextDecoration.underline,
|
||||
decorationThickness: 4,
|
||||
decorationColor: color ?? Colors.white.withAlpha(0),
|
||||
);
|
||||
|
||||
double tokenTextWidthForContainer(BuildContext context, String text) {
|
||||
final tokenSizeKey = "$text-${_fontSize(context)}";
|
||||
final tokenSizeKey = "$text-${fontSize(context)}";
|
||||
if (_tokensWidthCache.containsKey(tokenSizeKey)) {
|
||||
return _tokensWidthCache[tokenSizeKey]!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
: audioFile != null
|
||||
? AudioPlayerWidget(
|
||||
null,
|
||||
eventId: widget.messageEvent.eventId,
|
||||
eventId: "${widget.messageEvent.eventId}_practice",
|
||||
roomId: widget.messageEvent.room.id,
|
||||
senderId: widget.messageEvent.senderId,
|
||||
matrixFile: audioFile,
|
||||
|
|
|
|||
|
|
@ -109,8 +109,19 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
initializeTokensAndMode();
|
||||
super.initState();
|
||||
initializeTokensAndMode();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => widget.chatController.setSelectedEvent(event),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => widget.chatController.clearSelectedEvents(),
|
||||
);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initializeTokensAndMode() async {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ class OverlayHeaderState extends State<OverlayHeader> {
|
|||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final pinned = controller.selectedEvents.length == 1 &&
|
||||
controller.room.pinnedEventIds.contains(
|
||||
controller.selectedEvents.first.eventId,
|
||||
);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -102,9 +106,11 @@ class OverlayHeaderState extends State<OverlayHeader> {
|
|||
),
|
||||
if (controller.canPinSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.push_pin_outlined),
|
||||
icon: pinned
|
||||
? const Icon(Icons.push_pin)
|
||||
: const Icon(Icons.push_pin_outlined),
|
||||
onPressed: controller.pinEvent,
|
||||
tooltip: l10n.pinMessage,
|
||||
tooltip: pinned ? l10n.unpin : l10n.pinMessage,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
if (controller.canEditSelectedEvents &&
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import 'package:fluffychat/pages/chat/events/message_content.dart';
|
|||
import 'package:fluffychat/pages/chat/events/reply_content.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
|
|
@ -160,7 +163,7 @@ class OverlayMessage extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
L10n.of(context).transcriptionFailed,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
|
|
@ -170,14 +173,37 @@ class OverlayMessage extends StatelessWidget {
|
|||
)
|
||||
: overlayController.transcription != null
|
||||
? SingleChildScrollView(
|
||||
child: Text(
|
||||
overlayController.transcription!.transcript.text,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
).copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
overlayController
|
||||
.transcription!.transcript.text,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
).copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
if (MatrixState.pangeaController
|
||||
.languageController.showTrancription)
|
||||
PhoneticTranscriptionWidget(
|
||||
text: overlayController
|
||||
.transcription!.transcript.text,
|
||||
textLanguage: PLanguageStore.byLangCode(
|
||||
pangeaMessageEvent!
|
||||
.messageDisplayLangCode,
|
||||
) ??
|
||||
LanguageModel.unknown,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
),
|
||||
iconColor: textColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
|
|
|
|||
|
|
@ -138,6 +138,10 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
|
|||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 4.0,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
|||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -37,11 +38,10 @@ enum SelectMode {
|
|||
case SelectMode.audio:
|
||||
return l10n.playAudio;
|
||||
case SelectMode.translate:
|
||||
case SelectMode.speechTranslation:
|
||||
return l10n.translationTooltip;
|
||||
case SelectMode.practice:
|
||||
return l10n.practice;
|
||||
case SelectMode.speechTranslation:
|
||||
return l10n.speechToTextTooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,6 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
SelectMode? _selectedMode;
|
||||
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
bool _isLoadingAudio = false;
|
||||
PangeaAudioFile? _audioBytes;
|
||||
File? _audioFile;
|
||||
|
|
@ -94,17 +93,26 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
Completer<String>? _transcriptionCompleter;
|
||||
|
||||
AudioPlayer? get _audioPlayer => Matrix.of(context).audioPlayer!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_onPlayerStateChanged = _audioPlayer.playerStateStream.listen((state) {
|
||||
|
||||
final matrix = Matrix.of(context);
|
||||
matrix.audioPlayer?.dispose();
|
||||
matrix.audioPlayer = AudioPlayer();
|
||||
matrix.voiceMessageEventId.value =
|
||||
widget.overlayController.pangeaMessageEvent?.eventId;
|
||||
|
||||
_onPlayerStateChanged = _audioPlayer?.playerStateStream.listen((state) {
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
_updateMode(null);
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_onAudioPositionChanged ??= _audioPlayer.positionStream.listen((state) {
|
||||
_onAudioPositionChanged ??= _audioPlayer?.positionStream.listen((state) {
|
||||
if (_audioBytes != null) {
|
||||
widget.overlayController.highlightCurrentText(
|
||||
state.inMilliseconds,
|
||||
|
|
@ -120,7 +128,10 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_audioPlayer.dispose();
|
||||
_audioPlayer?.dispose();
|
||||
Matrix.of(context).audioPlayer = null;
|
||||
Matrix.of(context).voiceMessageEventId.value = null;
|
||||
|
||||
_onPlayerStateChanged?.cancel();
|
||||
_onAudioPositionChanged?.cancel();
|
||||
super.dispose();
|
||||
|
|
@ -151,8 +162,8 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
if (mode == null) {
|
||||
setState(() {
|
||||
_audioPlayer.stop();
|
||||
_audioPlayer.seek(null);
|
||||
_audioPlayer?.stop();
|
||||
_audioPlayer?.seek(null);
|
||||
_selectedMode = null;
|
||||
});
|
||||
return;
|
||||
|
|
@ -167,8 +178,8 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
_playAudio();
|
||||
return;
|
||||
} else {
|
||||
_audioPlayer.stop();
|
||||
_audioPlayer.seek(null);
|
||||
_audioPlayer?.stop();
|
||||
_audioPlayer?.seek(null);
|
||||
}
|
||||
|
||||
if (_selectedMode == SelectMode.practice) {
|
||||
|
|
@ -233,11 +244,12 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
Future<void> _playAudio() async {
|
||||
try {
|
||||
if (_audioPlayer.playerState.playing) {
|
||||
await _audioPlayer.pause();
|
||||
if (_audioPlayer != null && _audioPlayer!.playerState.playing) {
|
||||
await _audioPlayer?.pause();
|
||||
return;
|
||||
} else if (_audioPlayer.position != Duration.zero) {
|
||||
await _audioPlayer.play();
|
||||
} else if (_audioPlayer?.position != Duration.zero) {
|
||||
TtsController.stop();
|
||||
await _audioPlayer?.play();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -248,16 +260,18 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
if (_audioBytes == null) return;
|
||||
|
||||
if (_audioFile != null) {
|
||||
await _audioPlayer.setFilePath(_audioFile!.path);
|
||||
await _audioPlayer?.setFilePath(_audioFile!.path);
|
||||
} else {
|
||||
await _audioPlayer.setAudioSource(
|
||||
await _audioPlayer?.setAudioSource(
|
||||
BytesAudioSource(
|
||||
_audioBytes!.bytes,
|
||||
_audioBytes!.mimeType,
|
||||
),
|
||||
);
|
||||
}
|
||||
_audioPlayer.play();
|
||||
|
||||
TtsController.stop();
|
||||
_audioPlayer?.play();
|
||||
} catch (e, s) {
|
||||
setState(() => _audioError = e.toString());
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -427,7 +441,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
if (mode == SelectMode.audio) {
|
||||
return Icon(
|
||||
_audioPlayer.playerState.playing == true
|
||||
_audioPlayer?.playerState.playing == true
|
||||
? Icons.pause_outlined
|
||||
: Icons.volume_up,
|
||||
size: 20,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import '../../config/app_config.dart';
|
||||
|
||||
extension VisibleInGuiExtension on List<Event> {
|
||||
|
|
@ -46,7 +47,12 @@ extension IsStateExtension on Event {
|
|||
// if we enabled to hide all redacted events, don't show those
|
||||
(!AppConfig.hideRedactedEvents || !redacted) &&
|
||||
// if we enabled to hide all unknown events, don't show those
|
||||
(!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
|
||||
// #Pangea
|
||||
// (!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
|
||||
(!AppConfig.hideUnknownEvents ||
|
||||
isEventTypeKnown ||
|
||||
importantStateEvents.contains(type)) &&
|
||||
// Pangea#
|
||||
// remove state events that we don't want to render
|
||||
(isState || !AppConfig.hideAllStateEvents) &&
|
||||
// #Pangea
|
||||
|
|
@ -82,6 +88,7 @@ extension IsStateExtension on Event {
|
|||
EventTypes.RoomMember,
|
||||
EventTypes.RoomTombstone,
|
||||
EventTypes.CallInvite,
|
||||
PangeaEventTypes.activityPlan,
|
||||
};
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,20 +90,21 @@ void showMemberActionsPopupMenu({
|
|||
),
|
||||
const PopupMenuDivider(),
|
||||
// #Pangea
|
||||
PopupMenuItem(
|
||||
value: _MemberActions.chat,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.forum_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(
|
||||
dmRoomId == null
|
||||
? L10n.of(context).startConversation
|
||||
: L10n.of(context).sendAMessage,
|
||||
),
|
||||
],
|
||||
if (user.room.client.userID != user.id)
|
||||
PopupMenuItem(
|
||||
value: _MemberActions.chat,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.forum_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(
|
||||
dmRoomId == null
|
||||
? L10n.of(context).startConversation
|
||||
: L10n.of(context).sendAMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
if (onMention != null)
|
||||
PopupMenuItem(
|
||||
|
|
|
|||
|
|
@ -48,6 +48,24 @@ class _PresenceBuilderState extends State<PresenceBuilder> {
|
|||
}
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
@override
|
||||
void didUpdateWidget(PresenceBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.userId == widget.userId) return;
|
||||
|
||||
final client = widget.client ?? Matrix.of(context).client;
|
||||
final userId = widget.userId;
|
||||
if (userId != null) {
|
||||
client.fetchCurrentPresence(userId).then(_updatePresence);
|
||||
_sub?.cancel();
|
||||
_sub = client.onPresenceChanged.stream
|
||||
.where((presence) => presence.userid == userId)
|
||||
.listen(_updatePresence);
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue