Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f74057ce9b | ||
|
|
0245f792de | ||
|
|
9b37565887 | ||
|
|
5d115e0f9e | ||
|
|
0091b5f272 | ||
|
|
6dc1835377 | ||
|
|
101262897f | ||
|
|
d9e29f8651 | ||
|
|
04235e0972 | ||
|
|
c3027a82fb | ||
|
|
973a4cc4d7 | ||
|
|
5578309f95 | ||
|
|
6c732dc729 |
@@ -42,6 +42,6 @@
|
||||
## Проверка
|
||||
|
||||
- [ ] `flutter analyze`
|
||||
- [ ] Сборка `komet` flavor
|
||||
- [ ] Сборка `qlyra` flavor
|
||||
- [ ] Ручная проверка затронутых сценариев
|
||||
- [ ] Регресс по безопасности (если затронута)
|
||||
|
||||
@@ -21,4 +21,4 @@ runs:
|
||||
- name: Cache Rust build
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: native/komet_crypto/rust
|
||||
workspaces: native/qlyra_crypto/rust
|
||||
|
||||
@@ -69,12 +69,12 @@ jobs:
|
||||
echo "KEYSTORE_BASE64 secret not set — release will be debug-signed."
|
||||
exit 0
|
||||
fi
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/komet-release.jks
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/qlyra-release.jks
|
||||
{
|
||||
echo "storePassword=$KEYSTORE_PASSWORD"
|
||||
echo "keyPassword=$KEY_PASSWORD"
|
||||
echo "keyAlias=$KEY_ALIAS"
|
||||
echo "storeFile=komet-release.jks"
|
||||
echo "storeFile=qlyra-release.jks"
|
||||
} > android/key.properties
|
||||
echo "Release signing configured."
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
- name: Upload Universal APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-universal
|
||||
name: qlyra-android-fcm-universal
|
||||
path: build/app/outputs/flutter-apk/app-oneme-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
- name: Upload arm64-v8a APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-arm64-v8a
|
||||
name: qlyra-android-fcm-arm64-v8a
|
||||
path: build/app/outputs/flutter-apk/app-arm64-v8a-oneme-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
- name: Upload armeabi-v7a APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-armeabi-v7a
|
||||
name: qlyra-android-fcm-armeabi-v7a
|
||||
path: build/app/outputs/flutter-apk/app-armeabi-v7a-oneme-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
- name: Upload x86_64 APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-x86_64
|
||||
name: qlyra-android-fcm-x86_64
|
||||
path: build/app/outputs/flutter-apk/app-x86_64-oneme-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
- name: Upload App Bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-aab
|
||||
name: qlyra-android-fcm-aab
|
||||
path: build/app/outputs/bundle/onemeRelease/app-oneme-release.aab
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
- name: Upload debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-fcm-symbols
|
||||
name: qlyra-android-fcm-symbols
|
||||
path: build/symbols
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
|
||||
@@ -69,68 +69,68 @@ jobs:
|
||||
echo "KEYSTORE_BASE64 secret not set — release will be debug-signed."
|
||||
exit 0
|
||||
fi
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/komet-release.jks
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/qlyra-release.jks
|
||||
{
|
||||
echo "storePassword=$KEYSTORE_PASSWORD"
|
||||
echo "keyPassword=$KEY_PASSWORD"
|
||||
echo "keyAlias=$KEY_ALIAS"
|
||||
echo "storeFile=komet-release.jks"
|
||||
echo "storeFile=qlyra-release.jks"
|
||||
} > android/key.properties
|
||||
echo "Release signing configured."
|
||||
|
||||
- name: Build Universal APK
|
||||
run: flutter build apk --release --flavor komet --obfuscate --split-debug-info=build/symbols
|
||||
run: flutter build apk --release --flavor qlyra --obfuscate --split-debug-info=build/symbols
|
||||
|
||||
- name: Build Split APKs
|
||||
run: flutter build apk --release --split-per-abi --flavor komet --obfuscate --split-debug-info=build/symbols
|
||||
run: flutter build apk --release --split-per-abi --flavor qlyra --obfuscate --split-debug-info=build/symbols
|
||||
|
||||
- name: Upload Universal APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-universal
|
||||
path: build/app/outputs/flutter-apk/app-komet-release.apk
|
||||
name: qlyra-android-universal
|
||||
path: build/app/outputs/flutter-apk/app-qlyra-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload arm64-v8a APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-arm64-v8a
|
||||
path: build/app/outputs/flutter-apk/app-arm64-v8a-komet-release.apk
|
||||
name: qlyra-android-arm64-v8a
|
||||
path: build/app/outputs/flutter-apk/app-arm64-v8a-qlyra-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload armeabi-v7a APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-armeabi-v7a
|
||||
path: build/app/outputs/flutter-apk/app-armeabi-v7a-komet-release.apk
|
||||
name: qlyra-android-armeabi-v7a
|
||||
path: build/app/outputs/flutter-apk/app-armeabi-v7a-qlyra-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload x86_64 APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-x86_64
|
||||
path: build/app/outputs/flutter-apk/app-x86_64-komet-release.apk
|
||||
name: qlyra-android-x86_64
|
||||
path: build/app/outputs/flutter-apk/app-x86_64-qlyra-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
- name: Build App Bundle
|
||||
run: flutter build appbundle --release --flavor komet --obfuscate --split-debug-info=build/symbols
|
||||
run: flutter build appbundle --release --flavor qlyra --obfuscate --split-debug-info=build/symbols
|
||||
|
||||
- name: Upload App Bundle artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-aab
|
||||
path: build/app/outputs/bundle/kometRelease/app-komet-release.aab
|
||||
name: qlyra-android-aab
|
||||
path: build/app/outputs/bundle/qlyraRelease/app-qlyra-release.aab
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-android-symbols
|
||||
name: qlyra-android-symbols
|
||||
path: build/symbols
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
|
||||
@@ -54,14 +54,14 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist build/ios/Payload
|
||||
cp -R build/ios/iphoneos/Runner.app build/ios/Payload/
|
||||
(cd build/ios && zip -qr ../../dist/Komet-ios-unsigned.ipa Payload)
|
||||
(cd build/ios && zip -qr ../../dist/Qlyra-ios-unsigned.ipa Payload)
|
||||
rm -rf build/ios/Payload
|
||||
|
||||
- name: Upload unsigned iOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-ios-unsigned
|
||||
path: dist/Komet-ios-unsigned.ipa
|
||||
name: qlyra-ios-unsigned
|
||||
path: dist/Qlyra-ios-unsigned.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
@@ -79,9 +79,9 @@ jobs:
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>get-task-allow</key><true/>
|
||||
<key>application-identifier</key><string>ru.komet.app</string>
|
||||
<key>application-identifier</key><string>ru.qlyra.app</string>
|
||||
<key>keychain-access-groups</key>
|
||||
<array><string>ru.komet.app</string></array>
|
||||
<array><string>ru.qlyra.app</string></array>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
@@ -104,12 +104,12 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist build/ios/Payload
|
||||
cp -R build/ios/iphoneos/Runner.app build/ios/Payload/
|
||||
(cd build/ios && zip -qr ../../dist/Komet-ios-pseudosigned.ipa Payload)
|
||||
(cd build/ios && zip -qr ../../dist/Qlyra-ios-pseudosigned.ipa Payload)
|
||||
|
||||
- name: Upload iOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-ios-pseudosigned
|
||||
path: dist/Komet-ios-pseudosigned.ipa
|
||||
name: qlyra-ios-pseudosigned
|
||||
path: dist/Qlyra-ios-pseudosigned.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
@@ -56,12 +56,12 @@ jobs:
|
||||
- name: Package Linux
|
||||
run: |
|
||||
mkdir -p dist
|
||||
tar -C build/linux/x64/release/bundle -czf dist/Komet-linux-x64.tar.gz .
|
||||
tar -C build/linux/x64/release/bundle -czf dist/Qlyra-linux-x64.tar.gz .
|
||||
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-linux-x64
|
||||
path: dist/Komet-linux-x64.tar.gz
|
||||
name: qlyra-linux-x64
|
||||
path: dist/Qlyra-linux-x64.tar.gz
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Upload macOS DMG artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-macos
|
||||
name: qlyra-macos
|
||||
path: dist/*.dmg
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
@@ -52,12 +52,12 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath dist/Komet-windows-x64.zip
|
||||
Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath dist/Qlyra-windows-x64.zip
|
||||
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komet-windows-x64
|
||||
path: dist/Komet-windows-x64.zip
|
||||
name: qlyra-windows-x64
|
||||
path: dist/Qlyra-windows-x64.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
@@ -60,17 +60,17 @@ jobs:
|
||||
echo "KEYSTORE_BASE64 secret not set — release will be debug-signed."
|
||||
exit 0
|
||||
fi
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/komet-release.jks
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/qlyra-release.jks
|
||||
{
|
||||
echo "storePassword=$KEYSTORE_PASSWORD"
|
||||
echo "keyPassword=$KEY_PASSWORD"
|
||||
echo "keyAlias=$KEY_ALIAS"
|
||||
echo "storeFile=komet-release.jks"
|
||||
echo "storeFile=qlyra-release.jks"
|
||||
} > android/key.properties
|
||||
echo "Release signing configured."
|
||||
|
||||
- name: Build Android APK
|
||||
run: flutter build apk --release --flavor komet
|
||||
run: flutter build apk --release --flavor qlyra
|
||||
|
||||
build-ios:
|
||||
runs-on: macos-latest
|
||||
|
||||
@@ -59,17 +59,17 @@ jobs:
|
||||
echo "KEYSTORE_BASE64 secret not set — release will be debug-signed."
|
||||
exit 0
|
||||
fi
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/komet-release.jks
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/qlyra-release.jks
|
||||
{
|
||||
echo "storePassword=$KEYSTORE_PASSWORD"
|
||||
echo "keyPassword=$KEY_PASSWORD"
|
||||
echo "keyAlias=$KEY_ALIAS"
|
||||
echo "storeFile=komet-release.jks"
|
||||
echo "storeFile=qlyra-release.jks"
|
||||
} > android/key.properties
|
||||
echo "Release signing configured."
|
||||
|
||||
- name: Build Android APK
|
||||
run: flutter build apk --release --flavor komet
|
||||
run: flutter build apk --release --flavor qlyra
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: [komet, oneme]
|
||||
flavor: [qlyra, oneme]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -70,12 +70,12 @@ jobs:
|
||||
echo "KEYSTORE_BASE64 secret not set — release will be debug-signed."
|
||||
exit 0
|
||||
fi
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/komet-release.jks
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/qlyra-release.jks
|
||||
{
|
||||
echo "storePassword=$KEYSTORE_PASSWORD"
|
||||
echo "keyPassword=$KEY_PASSWORD"
|
||||
echo "keyAlias=$KEY_ALIAS"
|
||||
echo "storeFile=komet-release.jks"
|
||||
echo "storeFile=qlyra-release.jks"
|
||||
} > android/key.properties
|
||||
echo "Release signing configured."
|
||||
|
||||
@@ -89,12 +89,12 @@ jobs:
|
||||
run: |
|
||||
F=${{ matrix.flavor }}
|
||||
mkdir -p dist
|
||||
cp build/app/outputs/flutter-apk/app-$F-release.apk dist/Komet-android-$F-universal.apk
|
||||
cp build/app/outputs/flutter-apk/app-arm64-v8a-$F-release.apk dist/Komet-android-$F-arm64-v8a.apk
|
||||
cp build/app/outputs/flutter-apk/app-armeabi-v7a-$F-release.apk dist/Komet-android-$F-armeabi-v7a.apk
|
||||
cp build/app/outputs/flutter-apk/app-x86_64-$F-release.apk dist/Komet-android-$F-x86_64.apk
|
||||
cp build/app/outputs/bundle/${F}Release/app-$F-release.aab dist/Komet-android-$F.aab
|
||||
tar -czf dist/Komet-android-$F-symbols.tar.gz -C build/symbols .
|
||||
cp build/app/outputs/flutter-apk/app-$F-release.apk dist/Qlyra-android-$F-universal.apk
|
||||
cp build/app/outputs/flutter-apk/app-arm64-v8a-$F-release.apk dist/Qlyra-android-$F-arm64-v8a.apk
|
||||
cp build/app/outputs/flutter-apk/app-armeabi-v7a-$F-release.apk dist/Qlyra-android-$F-armeabi-v7a.apk
|
||||
cp build/app/outputs/flutter-apk/app-x86_64-$F-release.apk dist/Qlyra-android-$F-x86_64.apk
|
||||
cp build/app/outputs/bundle/${F}Release/app-$F-release.aab dist/Qlyra-android-$F.aab
|
||||
tar -czf dist/Qlyra-android-$F-symbols.tar.gz -C build/symbols .
|
||||
|
||||
- name: Upload Android artifacts (${{ matrix.flavor }})
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath dist/Komet-windows-x64.zip
|
||||
Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath dist/Qlyra-windows-x64.zip
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
- name: Package Linux
|
||||
run: |
|
||||
mkdir -p dist
|
||||
tar -C build/linux/x64/release/bundle -czf dist/Komet-linux-x64.tar.gz .
|
||||
tar -C build/linux/x64/release/bundle -czf dist/Qlyra-linux-x64.tar.gz .
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist
|
||||
APP=$(find build/macos/Build/Products/Release -maxdepth 1 -name "*.app" | head -1)
|
||||
ditto -c -k --keepParent "$APP" dist/Komet-macos.zip
|
||||
ditto -c -k --keepParent "$APP" dist/Qlyra-macos.zip
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist build/ios/Payload
|
||||
cp -R build/ios/iphoneos/Runner.app build/ios/Payload/
|
||||
(cd build/ios && zip -qr ../../dist/Komet-ios-unsigned.ipa Payload)
|
||||
(cd build/ios && zip -qr ../../dist/Qlyra-ios-unsigned.ipa Payload)
|
||||
rm -rf build/ios/Payload
|
||||
- name: Install ldid
|
||||
run: brew install ldid
|
||||
@@ -246,9 +246,9 @@ jobs:
|
||||
<dict>
|
||||
<key>platform-application</key><true/>
|
||||
<key>get-task-allow</key><true/>
|
||||
<key>application-identifier</key><string>ru.komet.app</string>
|
||||
<key>application-identifier</key><string>ru.qlyra.app</string>
|
||||
<key>keychain-access-groups</key>
|
||||
<array><string>ru.komet.app</string></array>
|
||||
<array><string>ru.qlyra.app</string></array>
|
||||
<key>com.apple.private.security.no-container</key><true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -271,7 +271,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist build/ios/Payload
|
||||
cp -R build/ios/iphoneos/Runner.app build/ios/Payload/
|
||||
(cd build/ios && zip -qr ../../dist/Komet-ios-pseudosigned.ipa Payload)
|
||||
(cd build/ios && zip -qr ../../dist/Qlyra-ios-pseudosigned.ipa Payload)
|
||||
- name: Upload iOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -297,7 +297,7 @@ jobs:
|
||||
BUILD="${{ github.event.inputs.build_number }}"
|
||||
BUILD="${BUILD:-$GITHUB_RUN_NUMBER}"
|
||||
echo "tag=v${BRANCH_VERSION}-dev.${BUILD}" >> "$GITHUB_OUTPUT"
|
||||
echo "name=Komet ${BRANCH_VERSION}-dev (build ${BUILD})" >> "$GITHUB_OUTPUT"
|
||||
echo "name=Qlyra ${BRANCH_VERSION}-dev (build ${BUILD})" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: List artifacts
|
||||
run: ls -lhR dist
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: [komet, oneme]
|
||||
flavor: [qlyra, oneme]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -65,12 +65,12 @@ jobs:
|
||||
echo "KEYSTORE_BASE64 secret not set — release will be debug-signed."
|
||||
exit 0
|
||||
fi
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/komet-release.jks
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/qlyra-release.jks
|
||||
{
|
||||
echo "storePassword=$KEYSTORE_PASSWORD"
|
||||
echo "keyPassword=$KEY_PASSWORD"
|
||||
echo "keyAlias=$KEY_ALIAS"
|
||||
echo "storeFile=komet-release.jks"
|
||||
echo "storeFile=qlyra-release.jks"
|
||||
} > android/key.properties
|
||||
echo "Release signing configured."
|
||||
|
||||
@@ -84,12 +84,12 @@ jobs:
|
||||
run: |
|
||||
F=${{ matrix.flavor }}
|
||||
mkdir -p dist
|
||||
cp build/app/outputs/flutter-apk/app-$F-release.apk dist/Komet-android-$F-universal.apk
|
||||
cp build/app/outputs/flutter-apk/app-arm64-v8a-$F-release.apk dist/Komet-android-$F-arm64-v8a.apk
|
||||
cp build/app/outputs/flutter-apk/app-armeabi-v7a-$F-release.apk dist/Komet-android-$F-armeabi-v7a.apk
|
||||
cp build/app/outputs/flutter-apk/app-x86_64-$F-release.apk dist/Komet-android-$F-x86_64.apk
|
||||
cp build/app/outputs/bundle/${F}Release/app-$F-release.aab dist/Komet-android-$F.aab
|
||||
tar -czf dist/Komet-android-$F-symbols.tar.gz -C build/symbols .
|
||||
cp build/app/outputs/flutter-apk/app-$F-release.apk dist/Qlyra-android-$F-universal.apk
|
||||
cp build/app/outputs/flutter-apk/app-arm64-v8a-$F-release.apk dist/Qlyra-android-$F-arm64-v8a.apk
|
||||
cp build/app/outputs/flutter-apk/app-armeabi-v7a-$F-release.apk dist/Qlyra-android-$F-armeabi-v7a.apk
|
||||
cp build/app/outputs/flutter-apk/app-x86_64-$F-release.apk dist/Qlyra-android-$F-x86_64.apk
|
||||
cp build/app/outputs/bundle/${F}Release/app-$F-release.aab dist/Qlyra-android-$F.aab
|
||||
tar -czf dist/Qlyra-android-$F-symbols.tar.gz -C build/symbols .
|
||||
|
||||
- name: Upload Android artifacts (${{ matrix.flavor }})
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath dist/Komet-windows-x64.zip
|
||||
Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath dist/Qlyra-windows-x64.zip
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- name: Package Linux
|
||||
run: |
|
||||
mkdir -p dist
|
||||
tar -C build/linux/x64/release/bundle -czf dist/Komet-linux-x64.tar.gz .
|
||||
tar -C build/linux/x64/release/bundle -czf dist/Qlyra-linux-x64.tar.gz .
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist
|
||||
APP=$(find build/macos/Build/Products/Release -maxdepth 1 -name "*.app" | head -1)
|
||||
ditto -c -k --keepParent "$APP" dist/Komet-macos.zip
|
||||
ditto -c -k --keepParent "$APP" dist/Qlyra-macos.zip
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -233,9 +233,9 @@ jobs:
|
||||
<dict>
|
||||
<key>platform-application</key><true/>
|
||||
<key>get-task-allow</key><true/>
|
||||
<key>application-identifier</key><string>ru.komet.app</string>
|
||||
<key>application-identifier</key><string>ru.qlyra.app</string>
|
||||
<key>keychain-access-groups</key>
|
||||
<array><string>ru.komet.app</string></array>
|
||||
<array><string>ru.qlyra.app</string></array>
|
||||
<key>com.apple.private.security.no-container</key><true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist build/ios/Payload
|
||||
cp -R build/ios/iphoneos/Runner.app build/ios/Payload/
|
||||
(cd build/ios && zip -qr ../../dist/Komet-ios-pseudosigned.ipa Payload)
|
||||
(cd build/ios && zip -qr ../../dist/Qlyra-ios-pseudosigned.ipa Payload)
|
||||
- name: Upload iOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
run: |
|
||||
PUBSPEC_VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f1)
|
||||
echo "tag=v${PUBSPEC_VERSION}+${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||
echo "name=Komet ${PUBSPEC_VERSION} (сборка ${GITHUB_RUN_NUMBER})" >> "$GITHUB_OUTPUT"
|
||||
echo "name=Qlyra ${PUBSPEC_VERSION} (сборка ${GITHUB_RUN_NUMBER})" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: List artifacts
|
||||
run: ls -lhR dist
|
||||
@@ -305,31 +305,31 @@ jobs:
|
||||
|
||||
## Какой файл качать
|
||||
|
||||
### Android — версия Komet (без push-уведомлений)
|
||||
- **`Komet-android-komet-arm64-v8a.apk`** — рекомендуется для большинства современных телефонов (64-битный ARM).
|
||||
- `Komet-android-komet-universal.apk` — универсальный APK, работает везде, но весит больше.
|
||||
- `Komet-android-komet-armeabi-v7a.apk` — для старых 32-битных устройств.
|
||||
- `Komet-android-komet-x86_64.apk` — для эмуляторов и редких x86-устройств.
|
||||
- `Komet-android-komet.aab` — App Bundle для публикации в Google Play (не для ручной установки).
|
||||
### Android — версия Qlyra (без push-уведомлений)
|
||||
- **`Qlyra-android-qlyra-arm64-v8a.apk`** — рекомендуется для большинства современных телефонов (64-битный ARM).
|
||||
- `Qlyra-android-qlyra-universal.apk` — универсальный APK, работает везде, но весит больше.
|
||||
- `Qlyra-android-qlyra-armeabi-v7a.apk` — для старых 32-битных устройств.
|
||||
- `Qlyra-android-qlyra-x86_64.apk` — для эмуляторов и редких x86-устройств.
|
||||
- `Qlyra-android-qlyra.aab` — App Bundle для публикации в Google Play (не для ручной установки).
|
||||
|
||||
### Android — версия OneMe (с push-уведомлениями через Firebase)
|
||||
- **`Komet-android-oneme-arm64-v8a.apk`** — рекомендуется для большинства современных телефонов.
|
||||
- `Komet-android-oneme-universal.apk` — универсальный APK.
|
||||
- `Komet-android-oneme-armeabi-v7a.apk` — для старых 32-битных устройств.
|
||||
- `Komet-android-oneme-x86_64.apk` — для эмуляторов и x86-устройств.
|
||||
- `Komet-android-oneme.aab` — App Bundle для Google Play.
|
||||
- **`Qlyra-android-oneme-arm64-v8a.apk`** — рекомендуется для большинства современных телефонов.
|
||||
- `Qlyra-android-oneme-universal.apk` — универсальный APK.
|
||||
- `Qlyra-android-oneme-armeabi-v7a.apk` — для старых 32-битных устройств.
|
||||
- `Qlyra-android-oneme-x86_64.apk` — для эмуляторов и x86-устройств.
|
||||
- `Qlyra-android-oneme.aab` — App Bundle для Google Play.
|
||||
|
||||
### Windows
|
||||
- **`Komet-windows-x64.zip`** — портативная сборка для Windows 10/11 (x64). Распаковать и запустить `Komet.exe`.
|
||||
- **`Qlyra-windows-x64.zip`** — портативная сборка для Windows 10/11 (x64). Распаковать и запустить `Qlyra.exe`.
|
||||
|
||||
### Linux
|
||||
- **`Komet-linux-x64.tar.gz`** — сборка для 64-битного Linux. Требуются `libgtk-3`, `libsecret-1`, `libjsoncpp`. Распаковать и запустить бинарник.
|
||||
- **`Qlyra-linux-x64.tar.gz`** — сборка для 64-битного Linux. Требуются `libgtk-3`, `libsecret-1`, `libjsoncpp`. Распаковать и запустить бинарник.
|
||||
|
||||
### macOS
|
||||
- **`Komet-macos.zip`** — `.app` для macOS. Распаковать и перетащить в `/Applications`. Приложение не нотаризовано — при первом запуске разрешите через «Системные настройки → Защита и безопасность».
|
||||
- **`Qlyra-macos.zip`** — `.app` для macOS. Распаковать и перетащить в `/Applications`. Приложение не нотаризовано — при первом запуске разрешите через «Системные настройки → Защита и безопасность».
|
||||
|
||||
### iOS
|
||||
- **`Komet-ios-pseudosigned.ipa`** — IPA с псевдо-подписью (ldid).
|
||||
- **`Qlyra-ios-pseudosigned.ipa`** — IPA с псевдо-подписью (ldid).
|
||||
⚠️ Не устанавливается через App Store. Подходит для:
|
||||
- jailbreak (Filza, rootless),
|
||||
- TrollStore,
|
||||
|
||||
@@ -136,7 +136,7 @@ agents.md
|
||||
.env
|
||||
.env.*
|
||||
# Локальные дампы трафика и скрипты анализа (содержат секреты)
|
||||
komet.txt
|
||||
qlyra.txt
|
||||
original_app.txt
|
||||
fingerprint.py
|
||||
PCAPdroid_*.txt
|
||||
@@ -149,8 +149,5 @@ maxtun/
|
||||
turnprobe/
|
||||
test/live_server_probe_test.dart
|
||||
|
||||
# local kolibri development: point the plugin at third_party/kolibri and the
|
||||
# Rust core at third_party/kolibri/kolibri-net instead of the published ones
|
||||
pubspec_overrides.yaml
|
||||
.cargo/
|
||||
third_party/kolibri/
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"cmake.sourceDirectory": "/run/media/invisedivine/Drive/! My projects/Komet/linux/runner",
|
||||
"kiroAgent.configureMCP": "Disabled"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
Не оставляй комментарии в код
|
||||
Старайся писать чистый код
|
||||
Лучше качество чем количество
|
||||
Когда при исправления какой то ошибки/добавление новой возникает ситуация 50/50 где можно выбрать починить сейчас но костылём, или чинить долго, упорно, может даже вообще не починить и переписать пол приложения - выбирай долго и упорно.
|
||||
ВМЕСТО СНЕКБАРОВ ИСПОЛЬЗУЙ НАШИ КАСТОМНЫЕ УВЕДОМЛЕНИЕ showCustomNotification(context, 'текст')
|
||||
Never leave real data in test files, including existing message contents or real IDs captured from requests. Use synthetic fixtures instead.
|
||||
Не пытайся собирать APK/AAB (flutter build apk, flutter build appbundle, gradle assemble) — сборку запускает пользователь, тебе достаточно flutter analyze
|
||||
Если делаешь кнопку, где иконка переключается перечёркнутая/неперечёркнутая (вспышка, микрофон, звук, уведомления) — она должна анимироваться lottie-иконкой, а не подменяться мгновенно: добавь спеку в SLASH_SPECS в tool/make_morph_icons.py (fill=1.0, если кнопка рисует Icon(..., fill: 1)), прогони python3 tool/make_morph_icons.py и выводи через LottieSlashIcon. Ассеты в assets/lottie/ руками не правь — только через генератор.
|
||||
@@ -4,20 +4,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project
|
||||
|
||||
Komet is a cross-platform Flutter messaging client (Android, iOS, macOS, Windows, Linux, Web) that communicates via a custom packet-based protocol with MessagePack serialization and Zstd compression.
|
||||
Qlyra is a cross-platform Flutter messaging client (Android, iOS, macOS, Windows, Linux, Web) that communicates via a custom packet-based protocol with MessagePack serialization and Zstd compression.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
flutter pub get # install dependencies
|
||||
flutter analyze # lint / static analysis
|
||||
flutter run # run on connected device (default: komet flavor)
|
||||
flutter run # run on connected device (default: qlyra flavor)
|
||||
flutter run --flavor oneme -t lib/main.dart # run oneme flavor (FCM)
|
||||
|
||||
# Android builds (release builds use obfuscation; keep symbols to de-obfuscate crashes)
|
||||
flutter build apk --release --flavor komet --obfuscate --split-debug-info=build/symbols
|
||||
flutter build apk --release --split-per-abi --flavor komet --obfuscate --split-debug-info=build/symbols
|
||||
flutter build appbundle --release --flavor komet --obfuscate --split-debug-info=build/symbols
|
||||
flutter build apk --release --flavor qlyra --obfuscate --split-debug-info=build/symbols
|
||||
flutter build apk --release --split-per-abi --flavor qlyra --obfuscate --split-debug-info=build/symbols
|
||||
flutter build appbundle --release --flavor qlyra --obfuscate --split-debug-info=build/symbols
|
||||
|
||||
# Other platforms
|
||||
flutter build ios --release --no-codesign
|
||||
@@ -35,10 +35,10 @@ Android builds require **Java 17**. Gradle memory is configured to `-Xmx4096m`.
|
||||
|
||||
| Flavor | App ID | Notes |
|
||||
|--------|--------|-------|
|
||||
| `komet` | `ru.komet.app` | Default, no FCM |
|
||||
| `qlyra` | `ru.qlyra.app` | Default, no FCM |
|
||||
| `oneme` | `ru.oneme.app` | FCM push notifications via Firebase |
|
||||
|
||||
Flavor-specific Android resources live in `android/app/src/komet/` and `android/app/src/oneme/`.
|
||||
Flavor-specific Android resources live in `android/app/src/qlyra/` and `android/app/src/oneme/`.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -97,5 +97,5 @@ Four GitHub Actions workflows in `.github/workflows/`:
|
||||
|
||||
- `flutter-dev.yml` — PR lint + Android build for dev branch
|
||||
- `flutter-main.yml` — PR lint + all-platform builds for main branch
|
||||
- `build-android.yml` — production APKs + AAB (`komet` flavor), triggered on push to main
|
||||
- `build-android.yml` — production APKs + AAB (`qlyra` flavor), triggered on push to main
|
||||
- `build-android-fcm.yml` — production APKs + AAB (`oneme` flavor with FCM), triggered on push to main
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
> [!IMPORTANT]
|
||||
> Потерялись? Возможно вы ищите старую версию Komet. Она расположена [здесь](https://github.com/KometTeam/KometOld).
|
||||
---
|
||||
<img src="assets/qlyra.png" width="100" alt="Qlyra Logo">
|
||||
|
||||
# Komet
|
||||
# Qlyra
|
||||
|
||||
Кроссплатформенный клиент для мессенджера MAX.
|
||||
|
||||
## Android
|
||||
|
||||
Основной вариант приложения использует идентификатор `ru.qlyra.app` и flavor `qlyra`.
|
||||
|
||||
## Сборка из исходного кода
|
||||
|
||||
```console
|
||||
$ flutter build [windows|linux] [--release]
|
||||
$ flutter build apk --release --flavor qlyra
|
||||
```
|
||||
|
||||
@@ -11,6 +11,6 @@ GeneratedPluginRegistrant.java
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
app/key.properties
|
||||
app/komet-release.jks
|
||||
app/qlyra-release.jks
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
||||
@@ -17,7 +17,7 @@ if (hasReleaseSigning) {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ru.komet.app"
|
||||
namespace = "ru.qlyra.app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@@ -32,7 +32,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ru.komet.app"
|
||||
applicationId = "ru.qlyra.app"
|
||||
minSdk = maxOf(flutter.minSdkVersion, 23)
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
@@ -54,10 +54,10 @@ android {
|
||||
flavorDimensions += "distribution"
|
||||
|
||||
productFlavors {
|
||||
create("komet") {
|
||||
create("qlyra") {
|
||||
dimension = "distribution"
|
||||
isDefault = true
|
||||
applicationId = "ru.komet.app"
|
||||
applicationId = "ru.qlyra.app"
|
||||
}
|
||||
create("oneme") {
|
||||
dimension = "distribution"
|
||||
|
||||
@@ -33,15 +33,15 @@
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
|
||||
<application
|
||||
android:label="Komet"
|
||||
android:label="Qlyra"
|
||||
android:name="${applicationName}"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name="ru.komet.app.MainActivity"
|
||||
android:name="ru.qlyra.app.MainActivity"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Komet"
|
||||
android:label="Qlyra"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
@@ -69,30 +69,38 @@
|
||||
<data android:scheme="http" android:host="max.ru"/>
|
||||
<data android:scheme="http" android:host="www.max.ru"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="https" android:host="komet.pw"/>
|
||||
<data android:scheme="https" android:host="www.komet.pw"/>
|
||||
<data android:path="/export-logs"/>
|
||||
<data android:path="/export-logs/"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="komet"/>
|
||||
<data android:scheme="qlyra"/>
|
||||
<data android:scheme="max"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/share_target_label">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="text/*"/>
|
||||
<data android:mimeType="image/*"/>
|
||||
<data android:mimeType="video/*"/>
|
||||
<data android:mimeType="audio/*"/>
|
||||
<data android:mimeType="application/*"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/share_target_label">
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="image/*"/>
|
||||
<data android:mimeType="video/*"/>
|
||||
<data android:mimeType="audio/*"/>
|
||||
<data android:mimeType="application/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity-alias
|
||||
android:name="ru.komet.app.MinimalIcon"
|
||||
android:name="ru.qlyra.app.MinimalIcon"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_minimal"
|
||||
android:label="Komet"
|
||||
android:targetActivity="ru.komet.app.MainActivity">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Qlyra"
|
||||
android:targetActivity="ru.qlyra.app.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@@ -113,10 +121,10 @@
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.nfc.cardemulation.host_apdu_service"
|
||||
android:resource="@xml/komet_nfc_apdu"/>
|
||||
android:resource="@xml/qlyra_nfc_apdu"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".KometFcmService"
|
||||
android:name=".QlyraFcmService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
@@ -158,7 +166,7 @@
|
||||
android:value="2" />
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="komet_messages" />
|
||||
android:value="qlyra_messages" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(komet_rlottie LANGUAGES C CXX ASM)
|
||||
project(qlyra_rlottie LANGUAGES C CXX ASM)
|
||||
|
||||
add_subdirectory(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../../../../third_party/rlottie_build"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
@@ -8,7 +8,9 @@ import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.ServiceCompat
|
||||
|
||||
object CallState {
|
||||
@Volatile
|
||||
@@ -18,15 +20,18 @@ object CallState {
|
||||
class CallForegroundService : Service() {
|
||||
|
||||
companion object {
|
||||
const val ACTION_START = "ru.komet.app.CALL_ONGOING_START"
|
||||
const val ACTION_STOP = "ru.komet.app.CALL_ONGOING_STOP"
|
||||
const val ACTION_SCREEN_SHARE = "ru.komet.app.CALL_SCREEN_SHARE"
|
||||
const val ACTION_START = "ru.qlyra.app.CALL_ONGOING_START"
|
||||
const val ACTION_STOP = "ru.qlyra.app.CALL_ONGOING_STOP"
|
||||
const val ACTION_SCREEN_SHARE = "ru.qlyra.app.CALL_SCREEN_SHARE"
|
||||
const val EXTRA_SCREEN_SHARE = "screenShare"
|
||||
const val ONGOING_ID = 424243
|
||||
|
||||
@Volatile
|
||||
var screenShare = false
|
||||
|
||||
@Volatile
|
||||
private var running: CallForegroundService? = null
|
||||
|
||||
fun setScreenShare(ctx: Context, enabled: Boolean, caller: String) {
|
||||
screenShare = enabled
|
||||
val intent = Intent(ctx, CallForegroundService::class.java).apply {
|
||||
@@ -41,7 +46,7 @@ class CallForegroundService : Service() {
|
||||
ctx.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("KometFcm", "screen share FGS update failed: ${e.message}")
|
||||
Log.w("QlyraFcm", "screen share FGS update failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,13 +63,19 @@ class CallForegroundService : Service() {
|
||||
ctx.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("KometFcm", "ongoing FGS start failed: ${e.message}")
|
||||
Log.w("QlyraFcm", "ongoing FGS start failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(ctx: Context) {
|
||||
CallState.inCall = false
|
||||
screenShare = false
|
||||
val service = running
|
||||
if (service != null) {
|
||||
service.shutdown()
|
||||
return
|
||||
}
|
||||
NotificationManagerCompat.from(ctx).cancel(ONGOING_ID)
|
||||
try {
|
||||
ctx.startService(
|
||||
Intent(ctx, CallForegroundService::class.java).apply {
|
||||
@@ -78,18 +89,25 @@ class CallForegroundService : Service() {
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
running = this
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (running === this) running = null
|
||||
CallState.inCall = false
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun shutdown() {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
ACTION_STOP -> shutdown()
|
||||
ACTION_SCREEN_SHARE -> {
|
||||
screenShare = intent.getBooleanExtra(EXTRA_SCREEN_SHARE, false)
|
||||
val caller = intent.getStringExtra(CallConst.EXTRA_CALLER) ?: "Звонок"
|
||||
@@ -142,7 +160,7 @@ class CallForegroundService : Service() {
|
||||
startForeground(ONGOING_ID, notif)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("KometFcm", "startForeground failed: ${e.message}")
|
||||
Log.w("QlyraFcm", "startForeground failed: ${e.message}")
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
@@ -46,21 +46,21 @@ object CallEvents {
|
||||
}
|
||||
|
||||
object CallConst {
|
||||
const val CHANNEL_ID = "komet_calls"
|
||||
const val CHANNEL_ID = "qlyra_calls"
|
||||
const val CHANNEL_NAME = "Звонки"
|
||||
const val NOTIF_ID = 424242
|
||||
const val ACCENT = 0xFF7C6BF0.toInt()
|
||||
|
||||
const val EXTRA_CALL = "komet_call"
|
||||
const val EXTRA_ACTION = "komet_call_action"
|
||||
const val EXTRA_DECLINE_PAYLOAD = "komet_decline_payload"
|
||||
const val EXTRA_NOTIF_ID = "komet_notif_id"
|
||||
const val EXTRA_CALLER = "komet_caller"
|
||||
const val EXTRA_CALL = "qlyra_call"
|
||||
const val EXTRA_ACTION = "qlyra_call_action"
|
||||
const val EXTRA_DECLINE_PAYLOAD = "qlyra_decline_payload"
|
||||
const val EXTRA_NOTIF_ID = "qlyra_notif_id"
|
||||
const val EXTRA_CALLER = "qlyra_caller"
|
||||
|
||||
const val ACTION_RING = "ring"
|
||||
const val ACTION_ANSWER = "answer"
|
||||
const val ACTION_DECLINE = "ru.komet.app.CALL_DECLINE"
|
||||
const val ACTION_HANGUP = "ru.komet.app.CALL_HANGUP"
|
||||
const val ACTION_DECLINE = "ru.qlyra.app.CALL_DECLINE"
|
||||
const val ACTION_HANGUP = "ru.qlyra.app.CALL_HANGUP"
|
||||
|
||||
const val FLN_RECEIVER =
|
||||
"com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver"
|
||||
@@ -72,7 +72,7 @@ object CallNotifier {
|
||||
|
||||
fun showIncoming(ctx: Context, data: Map<String, String>) {
|
||||
if (AppState.resumed) {
|
||||
Log.d("KometFcm", "call push suppressed (app foreground)")
|
||||
Log.d("QlyraFcm", "call push suppressed (app foreground)")
|
||||
return
|
||||
}
|
||||
val name = data["userName"] ?: data["title"] ?: "Неизвестный"
|
||||
@@ -82,7 +82,7 @@ object CallNotifier {
|
||||
val account = data["c"] ?: ""
|
||||
val callJson = JSONObject(data as Map<*, *>).toString()
|
||||
|
||||
Log.d("KometFcm", "showIncoming caller=$callerId conv=$conversationId keys=${data.keys}")
|
||||
Log.d("QlyraFcm", "showIncoming caller=$callerId conv=$conversationId keys=${data.keys}")
|
||||
|
||||
ensureChannel(ctx)
|
||||
|
||||
@@ -117,7 +117,7 @@ object CallNotifier {
|
||||
}
|
||||
|
||||
fun finishCall(ctx: Context, data: Map<String, String>) {
|
||||
Log.d("KometFcm", "finishCall keys=${data.keys}")
|
||||
Log.d("QlyraFcm", "finishCall keys=${data.keys}")
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(ctx).cancel(CallConst.NOTIF_ID)
|
||||
CallEvents.emit("ended")
|
||||
@@ -208,7 +208,7 @@ object CallRinger {
|
||||
activeConversationId = conversationId
|
||||
|
||||
val power = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
wakeLock = power.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "komet:call").apply {
|
||||
wakeLock = power.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "qlyra:call").apply {
|
||||
setReferenceCounted(false)
|
||||
acquire(60_000L)
|
||||
}
|
||||
@@ -271,7 +271,7 @@ object CallRinger {
|
||||
rt.play()
|
||||
ringtone = rt
|
||||
} catch (e: Exception) {
|
||||
Log.w("KometFcm", "ringtone failed: ${e.message}")
|
||||
Log.w("QlyraFcm", "ringtone failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ object CallRinger {
|
||||
}
|
||||
vibrator = vib
|
||||
} catch (e: Exception) {
|
||||
Log.w("KometFcm", "vibrate failed: ${e.message}")
|
||||
Log.w("QlyraFcm", "vibrate failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ class CallActionReceiver : BroadcastReceiver() {
|
||||
try {
|
||||
ctx.sendBroadcast(fln)
|
||||
} catch (e: Exception) {
|
||||
Log.w("KometFcm", "decline broadcast failed: ${e.message}")
|
||||
Log.w("QlyraFcm", "decline broadcast failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.content.Intent
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
|
||||
object ChatNotifications {
|
||||
const val EXTRA_CHAT = "komet_chat"
|
||||
const val EXTRA_CHAT = "qlyra_chat"
|
||||
|
||||
@Volatile
|
||||
var activeChatId: Long = 0L
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
@@ -20,7 +20,7 @@ import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object FkmChannel {
|
||||
private const val NAME = "ru.komet.app/fkm"
|
||||
private const val NAME = "ru.qlyra.app/fkm"
|
||||
const val NOTIF_PERMS_REQUEST = 7713
|
||||
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
@@ -117,7 +117,7 @@ object FkmChannel {
|
||||
val data = call.argument<Map<String, String>>("data") ?: return false
|
||||
worker.execute {
|
||||
try {
|
||||
KometNotifier(ctx).handle(data)
|
||||
QlyraNotifier(ctx).handle(data)
|
||||
FkmState.countDelivered(ctx)
|
||||
FkmService.refresh(ctx)
|
||||
} catch (e: Exception) {
|
||||
@@ -132,12 +132,12 @@ object FkmChannel {
|
||||
ctx: Context,
|
||||
call: MethodCall,
|
||||
tag: String,
|
||||
action: (KometNotifier, Map<String, String>) -> Unit,
|
||||
action: (QlyraNotifier, Map<String, String>) -> Unit,
|
||||
): Boolean {
|
||||
val data = call.argument<Map<String, String>>("data") ?: return false
|
||||
worker.execute {
|
||||
try {
|
||||
action(KometNotifier(ctx), data)
|
||||
action(QlyraNotifier(ctx), data)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Fkm", "$tag failed: ${e.message}")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -15,7 +15,7 @@ import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
object FkmState {
|
||||
private const val PREFS = "komet_fkm"
|
||||
private const val PREFS = "qlyra_fkm"
|
||||
private const val KEY_ENABLED = "enabled"
|
||||
private const val KEY_DELIVERED = "delivered"
|
||||
|
||||
@@ -60,7 +60,7 @@ object FkmState {
|
||||
}
|
||||
|
||||
object FkmNotification {
|
||||
const val CHANNEL_ID = "komet_fkm"
|
||||
const val CHANNEL_ID = "qlyra_fkm"
|
||||
const val NOTIFICATION_ID = 424244
|
||||
|
||||
fun build(ctx: Context): Notification {
|
||||
@@ -228,7 +228,7 @@ class FkmService : Service() {
|
||||
|
||||
class FkmDisableReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
const val ACTION_DISABLE = "ru.komet.app.FKM_DISABLE"
|
||||
const val ACTION_DISABLE = "ru.qlyra.app.FKM_DISABLE"
|
||||
}
|
||||
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.Manifest
|
||||
import android.app.KeyguardManager
|
||||
@@ -52,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val channelName = "ru.komet.app/vpn_bypass"
|
||||
private val channelName = "ru.qlyra.app/vpn_bypass"
|
||||
private val iconPackage = MainActivity::class.java.name.substringBeforeLast('.')
|
||||
private val iconComponents = mapOf(
|
||||
"MainActivity" to "$iconPackage.MainActivity",
|
||||
@@ -77,11 +77,19 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
private var pendingCall: Map<String, Any?>? = null
|
||||
private var pendingChat: Long = 0L
|
||||
private var pendingShare: Map<String, Any?>? = null
|
||||
private var pendingShareTask: java.util.concurrent.Future<Map<String, Any?>?>? = null
|
||||
private val shareExecutor = java.util.concurrent.Executors.newSingleThreadExecutor()
|
||||
private val shareHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private companion object {
|
||||
@Volatile
|
||||
var keepAwake = false
|
||||
|
||||
const val LOG_TAG = "VpnBypass"
|
||||
const val SHARE_TAG = "ShareIntake"
|
||||
const val NFC_TAG = "NfcExchange"
|
||||
const val KEEP_ENGINE_ID = "komet_keep_engine"
|
||||
const val KEEP_ENGINE_ID = "qlyra_keep_engine"
|
||||
const val NFC_PHASE_MIN_MS = 350L
|
||||
const val NFC_PHASE_JITTER_MS = 400
|
||||
const val BLE_PERMS_REQUEST = 7711
|
||||
@@ -118,7 +126,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/nfc",
|
||||
"ru.qlyra.app/nfc",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"status" -> result.success(nfcStatus())
|
||||
@@ -142,7 +150,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
EventChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/nfc_events",
|
||||
"ru.qlyra.app/nfc_events",
|
||||
).setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
nfcEvents = events
|
||||
@@ -167,7 +175,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/app_icon",
|
||||
"ru.qlyra.app/app_icon",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"setAppIcon" -> {
|
||||
@@ -189,7 +197,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/upload_service",
|
||||
"ru.qlyra.app/upload_service",
|
||||
).setMethodCallHandler { call, result ->
|
||||
val ctx = this
|
||||
fun uploadIntent(call: MethodCall, action: String) =
|
||||
@@ -240,7 +248,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/video_note",
|
||||
"ru.qlyra.app/video_note",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"permission" -> requestNotePermissions(result)
|
||||
@@ -279,7 +287,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/video",
|
||||
"ru.qlyra.app/video",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"cropSquare" -> {
|
||||
@@ -324,7 +332,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/calls",
|
||||
"ru.qlyra.app/calls",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"consumeInitialCall" -> {
|
||||
@@ -354,6 +362,10 @@ class MainActivity : FlutterActivity() {
|
||||
)
|
||||
result.success(null)
|
||||
}
|
||||
"dropOngoing" -> {
|
||||
CallForegroundService.stop(applicationContext)
|
||||
result.success(null)
|
||||
}
|
||||
"notifyEnded" -> {
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(this).cancel(CallConst.NOTIF_ID)
|
||||
@@ -381,7 +393,7 @@ class MainActivity : FlutterActivity() {
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("KometFcm", "open FSI settings failed: ${e.message}")
|
||||
Log.w("QlyraFcm", "open FSI settings failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
result.success(null)
|
||||
@@ -390,9 +402,22 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.qlyra.app/screen",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"setKeepAwake" -> {
|
||||
setKeepAwake(call.argument<Boolean>("enabled") == true)
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
EventChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/calls_events",
|
||||
"ru.qlyra.app/calls_events",
|
||||
).setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
CallEvents.sink = events
|
||||
@@ -405,7 +430,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/notifications",
|
||||
"ru.qlyra.app/notifications",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"consumeInitialChat" -> {
|
||||
@@ -428,7 +453,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
EventChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/notification_events",
|
||||
"ru.qlyra.app/notification_events",
|
||||
).setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
ChatNotifications.sink = events
|
||||
@@ -439,14 +464,64 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
})
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.qlyra.app/share",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"consumeInitialShare" -> {
|
||||
stashShare(intent, emit = false)
|
||||
val ready = pendingShare
|
||||
val task = pendingShareTask
|
||||
if (ready != null) {
|
||||
pendingShare = null
|
||||
result.success(ready)
|
||||
} else if (task != null) {
|
||||
pendingShareTask = null
|
||||
shareExecutor.execute {
|
||||
val payload = try {
|
||||
task.get()
|
||||
} catch (e: Exception) {
|
||||
Log.w(SHARE_TAG, "materialize failed: $e")
|
||||
null
|
||||
}
|
||||
shareHandler.post { result.success(payload) }
|
||||
}
|
||||
} else {
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
"clearCache" -> {
|
||||
ShareIntake.clearCache(applicationContext)
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
EventChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.qlyra.app/share_events",
|
||||
).setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
ShareIntake.sink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
ShareIntake.sink = null
|
||||
}
|
||||
})
|
||||
|
||||
FkmChannel.attach(flutterEngine, this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (intent?.hasExtra(CallConst.EXTRA_CALL) == true) applyCallWindowFlags()
|
||||
super.onCreate(savedInstanceState)
|
||||
applyKeepAwake()
|
||||
intent?.let { if (it.hasExtra(CallConst.EXTRA_CALL)) stashCall(it, emit = false) }
|
||||
stashChatOpen(intent, emit = false)
|
||||
stashShare(intent, emit = false)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
@@ -457,6 +532,7 @@ class MainActivity : FlutterActivity() {
|
||||
stashCall(intent, emit = true)
|
||||
}
|
||||
stashChatOpen(intent, emit = true)
|
||||
stashShare(intent, emit = true)
|
||||
}
|
||||
|
||||
private fun stashChatOpen(source: Intent?, emit: Boolean) {
|
||||
@@ -471,9 +547,40 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun stashShare(source: Intent?, emit: Boolean) {
|
||||
if (!ShareIntake.isShare(source)) return
|
||||
val intent = source ?: return
|
||||
val snapshot = ShareIntake.snapshot(intent) ?: return
|
||||
intent.action = Intent.ACTION_MAIN
|
||||
intent.removeExtra(Intent.EXTRA_STREAM)
|
||||
intent.removeExtra(Intent.EXTRA_TEXT)
|
||||
val task = shareExecutor.submit<Map<String, Any?>?> {
|
||||
ShareIntake.materialize(applicationContext, snapshot)
|
||||
}
|
||||
if (!emit) {
|
||||
pendingShareTask = task
|
||||
return
|
||||
}
|
||||
shareExecutor.execute {
|
||||
val payload = try {
|
||||
task.get()
|
||||
} catch (e: Exception) {
|
||||
Log.w(SHARE_TAG, "materialize failed: $e")
|
||||
null
|
||||
} ?: return@execute
|
||||
shareHandler.post {
|
||||
val sink = ShareIntake.sink
|
||||
if (sink != null) sink.success(payload) else pendingShare = payload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stashCall(intent: Intent, emit: Boolean) {
|
||||
val json = intent.getStringExtra(CallConst.EXTRA_CALL) ?: return
|
||||
val action = intent.getStringExtra(CallConst.EXTRA_ACTION) ?: CallConst.ACTION_RING
|
||||
intent.removeExtra(CallConst.EXTRA_CALL)
|
||||
intent.removeExtra(CallConst.EXTRA_ACTION)
|
||||
intent.removeExtra(CallConst.EXTRA_CALLER)
|
||||
if (action == CallConst.ACTION_ANSWER) CallRinger.stop()
|
||||
val map = mapOf<String, Any?>("data" to json, "action" to action)
|
||||
val sink = CallEvents.sink
|
||||
@@ -492,8 +599,7 @@ class MainActivity : FlutterActivity() {
|
||||
@Suppress("DEPRECATION")
|
||||
window.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -510,12 +616,24 @@ class MainActivity : FlutterActivity() {
|
||||
@Suppress("DEPRECATION")
|
||||
window.clearFlags(
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setKeepAwake(enabled: Boolean) {
|
||||
keepAwake = enabled
|
||||
runOnUiThread { applyKeepAwake() }
|
||||
}
|
||||
|
||||
private fun applyKeepAwake() {
|
||||
if (keepAwake) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
// Центр-кроп видео в квадрат size×size (без искажений) через media3
|
||||
// Transformer: LAYOUT_SCALE_TO_FIT_WITH_CROP заполняет квадрат и обрезает
|
||||
// лишнее по бокам.
|
||||
@@ -845,8 +963,9 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
shareExecutor.shutdown()
|
||||
if (keepEngineAlive() && isFinishing) {
|
||||
Log.d("KometFcm", "task removed, caching engine (call=${CallState.inCall} fkm=${FkmState.enabled})")
|
||||
Log.d("QlyraFcm", "task removed, caching engine (call=${CallState.inCall} fkm=${FkmState.enabled})")
|
||||
flutterEngine?.let { FlutterEngineCache.getInstance().put(KEEP_ENGINE_ID, it) }
|
||||
}
|
||||
super.onDestroy()
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
object NfcExchange {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.nfc.cardemulation.HostApduService
|
||||
import android.os.Bundle
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
@@ -26,29 +26,29 @@ import com.google.firebase.messaging.RemoteMessage
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class KometFcmService : FirebaseMessagingService() {
|
||||
class QlyraFcmService : FirebaseMessagingService() {
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
val data = message.data
|
||||
Log.d("KometFcm", "onMessageReceived type=${data["type"]} keys=${data.keys}")
|
||||
Log.d("QlyraFcm", "onMessageReceived type=${data["type"]} keys=${data.keys}")
|
||||
if (data.isEmpty()) return
|
||||
val type = data["type"]
|
||||
FkmState.restore(applicationContext)
|
||||
if (FkmState.enabled && type != "InboundCall" && type != "CallFinished") {
|
||||
Log.d("KometFcm", "message push dropped: FKM handles messages")
|
||||
Log.d("QlyraFcm", "message push dropped: FKM handles messages")
|
||||
return
|
||||
}
|
||||
KometNotifier(applicationContext).handle(data)
|
||||
QlyraNotifier(applicationContext).handle(data)
|
||||
}
|
||||
}
|
||||
|
||||
class KometNotifier(private val ctx: Context) {
|
||||
class QlyraNotifier(private val ctx: Context) {
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "komet_messages"
|
||||
private const val CHANNEL_ID = "qlyra_messages"
|
||||
private const val CHANNEL_NAME = "Сообщения"
|
||||
private const val GROUP_KEY = "komet_messages_group"
|
||||
private const val GROUP_KEY = "qlyra_messages_group"
|
||||
private const val SUMMARY_ID = 424200
|
||||
private const val PREFS = "komet_push"
|
||||
private const val PREFS = "qlyra_push"
|
||||
private const val FLN_RECEIVER =
|
||||
"com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver"
|
||||
private const val FLN_ACTION_TAPPED =
|
||||
@@ -336,7 +336,7 @@ class KometNotifier(private val ctx: Context) {
|
||||
.setShowWhen(true)
|
||||
.setNumber(entries.size)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||
.setContentTitle("Komet")
|
||||
.setContentTitle("Qlyra")
|
||||
.setContentText(boldLine(newest.first, newest.second))
|
||||
.setStyle(inbox)
|
||||
.build()
|
||||
@@ -399,7 +399,7 @@ class KometNotifier(private val ctx: Context) {
|
||||
.build()
|
||||
ShortcutManagerCompat.pushDynamicShortcut(ctx, shortcut)
|
||||
} catch (e: Exception) {
|
||||
Log.w("KometFcm", "shortcut push failed: $e")
|
||||
Log.w("QlyraFcm", "shortcut push failed: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
object ShareIntake {
|
||||
|
||||
private const val TAG = "ShareIntake"
|
||||
private const val CACHE_DIR = "shared_in"
|
||||
private const val MAX_FILES = 30
|
||||
|
||||
private val seq = AtomicLong(0L)
|
||||
|
||||
@Volatile
|
||||
var sink: EventChannel.EventSink? = null
|
||||
|
||||
fun isShare(intent: Intent?): Boolean {
|
||||
val action = intent?.action ?: return false
|
||||
return action == Intent.ACTION_SEND || action == Intent.ACTION_SEND_MULTIPLE
|
||||
}
|
||||
|
||||
class Snapshot(
|
||||
val uris: List<Uri>,
|
||||
val text: String?,
|
||||
val subject: String?,
|
||||
val intentType: String?,
|
||||
)
|
||||
|
||||
fun snapshot(intent: Intent): Snapshot? {
|
||||
val uris = collectUris(intent).take(MAX_FILES)
|
||||
val text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
||||
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||
if (uris.isEmpty() && text.isNullOrBlank()) return null
|
||||
return Snapshot(uris, text, subject, intent.type)
|
||||
}
|
||||
|
||||
fun materialize(context: Context, snapshot: Snapshot): Map<String, Any?>? {
|
||||
val files = ArrayList<Map<String, Any?>>()
|
||||
for (uri in snapshot.uris) {
|
||||
val copied = copyToCache(context, uri, snapshot.intentType)
|
||||
if (copied != null) files.add(copied)
|
||||
}
|
||||
|
||||
if (files.isEmpty() && snapshot.text.isNullOrBlank()) return null
|
||||
|
||||
return mapOf(
|
||||
"files" to files,
|
||||
"text" to snapshot.text,
|
||||
"subject" to snapshot.subject,
|
||||
)
|
||||
}
|
||||
|
||||
fun clearCache(context: Context) {
|
||||
try {
|
||||
val dir = File(context.cacheDir, CACHE_DIR)
|
||||
if (!dir.isDirectory) return
|
||||
dir.listFiles()?.forEach { it.delete() }
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "cache cleanup failed: $e")
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectUris(intent: Intent): List<Uri> {
|
||||
if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
|
||||
val list = if (android.os.Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
|
||||
}
|
||||
return list?.filterNotNull() ?: emptyList()
|
||||
}
|
||||
val single = if (android.os.Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
||||
}
|
||||
return if (single != null) listOf(single) else emptyList()
|
||||
}
|
||||
|
||||
private fun copyToCache(context: Context, uri: Uri, intentType: String?): Map<String, Any?>? {
|
||||
val resolver = context.contentResolver
|
||||
val mime = resolveMime(resolver, uri, intentType)
|
||||
val displayName = queryDisplayName(resolver, uri) ?: fallbackName(uri, mime)
|
||||
|
||||
return try {
|
||||
val dir = File(context.cacheDir, CACHE_DIR).apply { mkdirs() }
|
||||
val target = File(dir, "${System.currentTimeMillis()}_${seq.incrementAndGet()}_${sanitize(displayName)}")
|
||||
resolver.openInputStream(uri).use { input ->
|
||||
if (input == null) return null
|
||||
target.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
if (target.length() <= 0L) {
|
||||
target.delete()
|
||||
return null
|
||||
}
|
||||
mapOf(
|
||||
"path" to target.absolutePath,
|
||||
"name" to displayName,
|
||||
"mime" to mime,
|
||||
"size" to target.length(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "cannot read $uri: $e")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveMime(resolver: ContentResolver, uri: Uri, intentType: String?): String {
|
||||
val fromResolver = resolver.getType(uri)
|
||||
if (!fromResolver.isNullOrBlank() && fromResolver != "*/*") return fromResolver
|
||||
val ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
|
||||
if (!ext.isNullOrBlank()) {
|
||||
val guessed = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.lowercase())
|
||||
if (!guessed.isNullOrBlank()) return guessed
|
||||
}
|
||||
if (!intentType.isNullOrBlank() && intentType != "*/*") return intentType
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
private fun queryDisplayName(resolver: ContentResolver, uri: Uri): String? {
|
||||
if (uri.scheme == ContentResolver.SCHEME_FILE) return uri.lastPathSegment
|
||||
return try {
|
||||
resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
|
||||
val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (index >= 0 && cursor.moveToFirst()) cursor.getString(index) else null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "display name for $uri: $e")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun fallbackName(uri: Uri, mime: String): String {
|
||||
val last = uri.lastPathSegment?.substringAfterLast('/')
|
||||
if (!last.isNullOrBlank() && last.contains('.')) return last
|
||||
val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "bin"
|
||||
return "shared_${System.currentTimeMillis()}.$ext"
|
||||
}
|
||||
|
||||
private fun sanitize(name: String): String {
|
||||
val cleaned = name.replace(Regex("[^A-Za-z0-9._-]"), "_")
|
||||
return if (cleaned.length <= 64) cleaned else cleaned.takeLast(64)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.app.*
|
||||
import android.content.Intent
|
||||
@@ -11,11 +11,11 @@ import androidx.core.app.NotificationCompat
|
||||
class UploadForegroundService : Service() {
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "komet_upload"
|
||||
const val CHANNEL_ID = "qlyra_upload"
|
||||
const val NOTIFICATION_ID = 9001
|
||||
const val ACTION_START = "ru.komet.app.UPLOAD_START"
|
||||
const val ACTION_UPDATE = "ru.komet.app.UPLOAD_UPDATE"
|
||||
const val ACTION_STOP = "ru.komet.app.UPLOAD_STOP"
|
||||
const val ACTION_START = "ru.qlyra.app.UPLOAD_START"
|
||||
const val ACTION_UPDATE = "ru.qlyra.app.UPLOAD_UPDATE"
|
||||
const val ACTION_STOP = "ru.qlyra.app.UPLOAD_STOP"
|
||||
const val EXTRA_TITLE = "title"
|
||||
const val EXTRA_BODY = "body"
|
||||
const val EXTRA_PROGRESS = "progress"
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.komet.app
|
||||
package ru.qlyra.app
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 49 KiB |
@@ -2,11 +2,12 @@
|
||||
<string name="upload_channel_name">Отправка медиа</string>
|
||||
<string name="fkm_channel_name">Сервис уведомлений</string>
|
||||
<string name="fkm_channel_description">Держит фоновое соединение с сервером</string>
|
||||
<string name="fkm_title">Komet · сервис уведомлений</string>
|
||||
<string name="fkm_title">Qlyra · сервис уведомлений</string>
|
||||
<string name="fkm_status_active">Соединение активно</string>
|
||||
<string name="fkm_status_inactive">Соединение не активно</string>
|
||||
<string name="fkm_status_line">%1$s · принято %2$d</string>
|
||||
<string name="fkm_explain">Это уведомление держит фоновое соединение с сервером, чтобы сообщения приходили без гугловых пушей. Убрать его можно, выключив FKM — кнопкой ниже или в Настройки → Уведомления → FKM.</string>
|
||||
<string name="fkm_disable">Выключить</string>
|
||||
<string name="notif_deleted_prefix">Удалено:</string>
|
||||
<string name="share_target_label">Отправить в чат</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<resources>
|
||||
<string name="nfc_service_description">Komet contact exchange</string>
|
||||
<string name="nfc_aid_group_description">Komet contact exchange</string>
|
||||
<string name="nfc_service_description">Qlyra contact exchange</string>
|
||||
<string name="nfc_aid_group_description">Qlyra contact exchange</string>
|
||||
<string name="upload_channel_name">Sending media</string>
|
||||
<string name="fkm_channel_name">Notification service</string>
|
||||
<string name="fkm_channel_description">Keeps the background connection to the server alive</string>
|
||||
<string name="fkm_title">Komet · notification service</string>
|
||||
<string name="fkm_title">Qlyra · notification service</string>
|
||||
<string name="fkm_status_active">Connection active</string>
|
||||
<string name="fkm_status_inactive">Connection inactive</string>
|
||||
<string name="fkm_status_line">%1$s · %2$d delivered</string>
|
||||
<string name="fkm_explain">This notification is what keeps a background connection to the server, so messages arrive without Google push. To get rid of it, turn FKM off — with the button below, or in Settings → Notifications → FKM.</string>
|
||||
<string name="fkm_disable">Turn off</string>
|
||||
<string name="notif_deleted_prefix">Deleted:</string>
|
||||
<string name="share_target_label">Send to a chat</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:659634599081:android:00000000000000000000ab",
|
||||
"android_client_info": {
|
||||
"package_name": "ru.komet.app"
|
||||
"package_name": "ru.qlyra.app"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -15,7 +15,7 @@
|
||||
<rect width="1440" height="2280" fill="#F8F4FE"/>
|
||||
|
||||
<g id="header">
|
||||
<text x="64" y="84" class="title" fill="#1B1B21">Komet — Design System</text>
|
||||
<text x="64" y="84" class="title" fill="#1B1B21">Qlyra — Design System</text>
|
||||
<text x="64" y="112" class="small" fill="#46464F">Seed #C1C4FF · Material 3 Expressive · Inter</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -7,7 +7,7 @@ ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
require 'json'
|
||||
|
||||
# Firebase is Android-only: FCM is used solely by the `oneme` flavor, and on
|
||||
# iOS there is a single `ru.komet.app` bundle that never initialises push.
|
||||
# iOS there is a single `ru.qlyra.app` bundle that never initialises push.
|
||||
# Keep the Firebase SDK out of the iOS build entirely so iOS is not forced up
|
||||
# to the iOS 15 deployment target that firebase_core's podspec requires.
|
||||
FIREBASE_IOS_PLUGINS = %w[firebase_core firebase_messaging].freeze
|
||||
@@ -79,7 +79,7 @@ end
|
||||
# xcframeworks into the pod target's own build dir, so for Runner that variable
|
||||
# expands to a file nothing ever produces and Xcode fails on the missing input.
|
||||
OPUS_XCFRAMEWORK = 'ogg_opus_player/darwin/Frameworks/libopus.xcframework'.freeze
|
||||
OPUS_LIB_VAR = 'KOMET_OPUS_LIB'.freeze
|
||||
OPUS_LIB_VAR = 'QLYRA_OPUS_LIB'.freeze
|
||||
OPUS_FORCE_LOAD = %(-force_load "$(#{OPUS_LIB_VAR})").freeze
|
||||
OPUS_XCFRAMEWORK_DIR =
|
||||
File.expand_path(File.join('.symlinks', 'plugins', OPUS_XCFRAMEWORK), __dir__).freeze
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
4B1A4BBCD56B3CE42AD0A480 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 789A1AEF2F4FF7DEED202476 /* Pods_Runner.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
AA11BB22CC33DD44EE550101 /* KometVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE550001 /* KometVideo.swift */; };
|
||||
AA11BB22CC33DD44EE550102 /* KometVideoNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE550002 /* KometVideoNote.swift */; };
|
||||
AA11BB22CC33DD44EE550103 /* KometNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE550003 /* KometNotifications.swift */; };
|
||||
AA11BB22CC33DD44EE550101 /* QlyraVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE550001 /* QlyraVideo.swift */; };
|
||||
AA11BB22CC33DD44EE550102 /* QlyraVideoNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE550002 /* QlyraVideoNote.swift */; };
|
||||
AA11BB22CC33DD44EE550103 /* QlyraNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE550003 /* QlyraNotifications.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
@@ -59,9 +59,9 @@
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
AA11BB22CC33DD44EE550001 /* KometVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KometVideo.swift; sourceTree = "<group>"; };
|
||||
AA11BB22CC33DD44EE550002 /* KometVideoNote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KometVideoNote.swift; sourceTree = "<group>"; };
|
||||
AA11BB22CC33DD44EE550003 /* KometNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KometNotifications.swift; sourceTree = "<group>"; };
|
||||
AA11BB22CC33DD44EE550001 /* QlyraVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QlyraVideo.swift; sourceTree = "<group>"; };
|
||||
AA11BB22CC33DD44EE550002 /* QlyraVideoNote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QlyraVideoNote.swift; sourceTree = "<group>"; };
|
||||
AA11BB22CC33DD44EE550003 /* QlyraNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QlyraNotifications.swift; sourceTree = "<group>"; };
|
||||
AA11BB22CC33DD44EE550004 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
782E66FE4E292DCFD0D1B7D2 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
789A1AEF2F4FF7DEED202476 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -164,9 +164,9 @@
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
AA11BB22CC33DD44EE550001 /* KometVideo.swift */,
|
||||
AA11BB22CC33DD44EE550002 /* KometVideoNote.swift */,
|
||||
AA11BB22CC33DD44EE550003 /* KometNotifications.swift */,
|
||||
AA11BB22CC33DD44EE550001 /* QlyraVideo.swift */,
|
||||
AA11BB22CC33DD44EE550002 /* QlyraVideoNote.swift */,
|
||||
AA11BB22CC33DD44EE550003 /* QlyraNotifications.swift */,
|
||||
AA11BB22CC33DD44EE550004 /* Runner.entitlements */,
|
||||
);
|
||||
path = Runner;
|
||||
@@ -407,9 +407,9 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
AA11BB22CC33DD44EE550101 /* KometVideo.swift in Sources */,
|
||||
AA11BB22CC33DD44EE550102 /* KometVideoNote.swift in Sources */,
|
||||
AA11BB22CC33DD44EE550103 /* KometNotifications.swift in Sources */,
|
||||
AA11BB22CC33DD44EE550101 /* QlyraVideo.swift in Sources */,
|
||||
AA11BB22CC33DD44EE550102 /* QlyraVideoNote.swift in Sources */,
|
||||
AA11BB22CC33DD44EE550103 /* QlyraNotifications.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -510,7 +510,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.komet.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.qlyra.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -527,7 +527,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.komet.app.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.qlyra.app.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -545,7 +545,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.komet.app.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.qlyra.app.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -561,7 +561,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.komet.app.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.qlyra.app.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -693,7 +693,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.komet.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.qlyra.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -716,7 +716,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.komet.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.qlyra.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
final class QlyraStreamHandler: NSObject, FlutterStreamHandler {
|
||||
private let onSink: (FlutterEventSink?) -> Void
|
||||
|
||||
init(onSink: @escaping (FlutterEventSink?) -> Void) {
|
||||
@@ -26,15 +26,15 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
private var channels: [FlutterMethodChannel] = []
|
||||
private var eventChannels: [FlutterEventChannel] = []
|
||||
private var streamHandlers: [KometStreamHandler] = []
|
||||
private var videoNote: KometVideoNote?
|
||||
private var streamHandlers: [QlyraStreamHandler] = []
|
||||
private var videoNote: QlyraVideoNote?
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
KometNotifications.shared.start()
|
||||
QlyraNotifications.shared.start()
|
||||
|
||||
if let controller = window?.rootViewController as? FlutterViewController {
|
||||
let messenger = controller.binaryMessenger
|
||||
@@ -42,6 +42,7 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
registerVideo(messenger)
|
||||
registerVideoNote(messenger)
|
||||
registerNotifications(messenger)
|
||||
registerScreen(messenger)
|
||||
}
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
@@ -56,7 +57,7 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
|
||||
private func events(_ name: String, _ messenger: FlutterBinaryMessenger,
|
||||
_ onSink: @escaping (FlutterEventSink?) -> Void) {
|
||||
let handler = KometStreamHandler(onSink: onSink)
|
||||
let handler = QlyraStreamHandler(onSink: onSink)
|
||||
let channel = FlutterEventChannel(name: name, binaryMessenger: messenger)
|
||||
channel.setStreamHandler(handler)
|
||||
streamHandlers.append(handler)
|
||||
@@ -64,7 +65,7 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
}
|
||||
|
||||
private func registerAppIcon(_ messenger: FlutterBinaryMessenger) {
|
||||
method("ru.komet.app/app_icon", messenger) { call, result in
|
||||
method("ru.qlyra.app/app_icon", messenger) { call, result in
|
||||
switch call.method {
|
||||
case "getAppIcon":
|
||||
result(UIApplication.shared.alternateIconName)
|
||||
@@ -99,23 +100,23 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
}
|
||||
|
||||
private func registerVideo(_ messenger: FlutterBinaryMessenger) {
|
||||
method("ru.komet.app/video", messenger) { call, result in
|
||||
KometVideo.shared.handle(call, result: result)
|
||||
method("ru.qlyra.app/video", messenger) { call, result in
|
||||
QlyraVideo.shared.handle(call, result: result)
|
||||
}
|
||||
}
|
||||
|
||||
private func registerVideoNote(_ messenger: FlutterBinaryMessenger) {
|
||||
guard let textures = registrar(forPlugin: "KometVideoNote")?.textures() else { return }
|
||||
guard let textures = registrar(forPlugin: "QlyraVideoNote")?.textures() else { return }
|
||||
|
||||
method("ru.komet.app/video_note", messenger) { [weak self] call, result in
|
||||
method("ru.qlyra.app/video_note", messenger) { [weak self] call, result in
|
||||
guard let self = self else { return }
|
||||
switch call.method {
|
||||
case "permission":
|
||||
KometVideoNote.requestPermission(result)
|
||||
QlyraVideoNote.requestPermission(result)
|
||||
case "init":
|
||||
let args = call.arguments as? [String: Any] ?? [:]
|
||||
self.videoNote?.dispose()
|
||||
let recorder = KometVideoNote(registry: textures)
|
||||
let recorder = QlyraVideoNote(registry: textures)
|
||||
self.videoNote = recorder
|
||||
recorder.initialize(
|
||||
front: (args["front"] as? NSNumber)?.boolValue ?? true,
|
||||
@@ -142,7 +143,7 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
}
|
||||
|
||||
private func withRecorder(_ result: @escaping FlutterResult,
|
||||
_ body: (KometVideoNote) -> Void) {
|
||||
_ body: (QlyraVideoNote) -> Void) {
|
||||
guard let recorder = videoNote else {
|
||||
result(FlutterError(code: "NOT_READY", message: "recorder not initialized", details: nil))
|
||||
return
|
||||
@@ -150,12 +151,27 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
body(recorder)
|
||||
}
|
||||
|
||||
private func registerNotifications(_ messenger: FlutterBinaryMessenger) {
|
||||
method("ru.komet.app/notifications", messenger) { call, result in
|
||||
KometNotifications.shared.handle(call, result: result)
|
||||
private func registerScreen(_ messenger: FlutterBinaryMessenger) {
|
||||
method("ru.qlyra.app/screen", messenger) { call, result in
|
||||
switch call.method {
|
||||
case "setKeepAwake":
|
||||
let enabled = ((call.arguments as? [String: Any])?["enabled"] as? NSNumber)?.boolValue ?? false
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.isIdleTimerDisabled = enabled
|
||||
result(nil)
|
||||
}
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
events("ru.komet.app/notification_events", messenger) { sink in
|
||||
KometNotifications.shared.attach(sink)
|
||||
}
|
||||
|
||||
private func registerNotifications(_ messenger: FlutterBinaryMessenger) {
|
||||
method("ru.qlyra.app/notifications", messenger) { call, result in
|
||||
QlyraNotifications.shared.handle(call, result: result)
|
||||
}
|
||||
events("ru.qlyra.app/notification_events", messenger) { sink in
|
||||
QlyraNotifications.shared.attach(sink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 797 KiB |
|
Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 32 KiB |
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Komet</string>
|
||||
<string>Qlyra</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Komet</string>
|
||||
<string>Qlyra</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict>
|
||||
<key>CFBundleAlternateIcons</key>
|
||||
@@ -76,10 +76,10 @@
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>ru.komet.app</string>
|
||||
<string>ru.qlyra.app</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>komet</string>
|
||||
<string>qlyra</string>
|
||||
<string>max</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
@@ -2,10 +2,10 @@ import Flutter
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
final class KometNotifications: NSObject {
|
||||
static let shared = KometNotifications()
|
||||
final class QlyraNotifications: NSObject {
|
||||
static let shared = QlyraNotifications()
|
||||
|
||||
private static let chatKeys = ["komet_chat", "chatId", "chat_id"]
|
||||
private static let chatKeys = ["qlyra_chat", "chatId", "chat_id"]
|
||||
|
||||
private var sink: FlutterEventSink?
|
||||
private var pendingChatId: Int64 = 0
|
||||
@@ -76,7 +76,7 @@ final class KometNotifications: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
extension KometNotifications: UNUserNotificationCenterDelegate {
|
||||
extension QlyraNotifications: UNUserNotificationCenterDelegate {
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
@@ -54,10 +54,10 @@ private struct VideoExportSpec {
|
||||
}
|
||||
}
|
||||
|
||||
final class KometVideo {
|
||||
static let shared = KometVideo()
|
||||
final class QlyraVideo {
|
||||
static let shared = QlyraVideo()
|
||||
|
||||
private let queue = DispatchQueue(label: "ru.komet.app.video", qos: .userInitiated)
|
||||
private let queue = DispatchQueue(label: "ru.qlyra.app.video", qos: .userInitiated)
|
||||
private var session: AVAssetExportSession?
|
||||
private var cancelled = false
|
||||
|
||||
@@ -3,7 +3,7 @@ import CoreImage
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
final class KometVideoNoteTexture: NSObject, FlutterTexture {
|
||||
final class QlyraVideoNoteTexture: NSObject, FlutterTexture {
|
||||
private let lock = NSLock()
|
||||
private var latest: CVPixelBuffer?
|
||||
|
||||
@@ -21,15 +21,15 @@ final class KometVideoNoteTexture: NSObject, FlutterTexture {
|
||||
}
|
||||
}
|
||||
|
||||
final class KometVideoNote: NSObject {
|
||||
final class QlyraVideoNote: NSObject {
|
||||
private let registry: FlutterTextureRegistry
|
||||
private let queue = DispatchQueue(label: "ru.komet.app.videonote", qos: .userInitiated)
|
||||
private let queue = DispatchQueue(label: "ru.qlyra.app.videonote", qos: .userInitiated)
|
||||
private let ciContext = CIContext(options: [.useSoftwareRenderer: false])
|
||||
|
||||
private let session = AVCaptureSession()
|
||||
private let videoOutput = AVCaptureVideoDataOutput()
|
||||
private let audioOutput = AVCaptureAudioDataOutput()
|
||||
private let texture = KometVideoNoteTexture()
|
||||
private let texture = QlyraVideoNoteTexture()
|
||||
|
||||
private var textureId: Int64 = 0
|
||||
private var deviceInput: AVCaptureDeviceInput?
|
||||
@@ -207,12 +207,12 @@ final class KometVideoNote: NSObject {
|
||||
guard let device = AVCaptureDevice.default(
|
||||
.builtInWideAngleCamera, for: .video, position: position)
|
||||
?? AVCaptureDevice.default(for: .video) else {
|
||||
throw NSError(domain: "KometVideoNote", code: 1,
|
||||
throw NSError(domain: "QlyraVideoNote", code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "no camera found"])
|
||||
}
|
||||
let input = try AVCaptureDeviceInput(device: device)
|
||||
guard session.canAddInput(input) else {
|
||||
throw NSError(domain: "KometVideoNote", code: 2,
|
||||
throw NSError(domain: "QlyraVideoNote", code: 2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "camera input rejected"])
|
||||
}
|
||||
session.addInput(input)
|
||||
@@ -270,7 +270,7 @@ final class KometVideoNote: NSObject {
|
||||
private func prepareWriter() throws {
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
let url = directory.appendingPathComponent(
|
||||
"komet_note_\(Int(Date().timeIntervalSince1970 * 1000)).mp4")
|
||||
"qlyra_note_\(Int(Date().timeIntervalSince1970 * 1000)).mp4")
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
|
||||
let writer = try AVAssetWriter(outputURL: url, fileType: .mp4)
|
||||
@@ -286,7 +286,7 @@ final class KometVideoNote: NSObject {
|
||||
let video = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
|
||||
video.expectsMediaDataInRealTime = true
|
||||
guard writer.canAdd(video) else {
|
||||
throw NSError(domain: "KometVideoNote", code: 3,
|
||||
throw NSError(domain: "QlyraVideoNote", code: 3,
|
||||
userInfo: [NSLocalizedDescriptionKey: "video input rejected"])
|
||||
}
|
||||
writer.add(video)
|
||||
@@ -310,7 +310,7 @@ final class KometVideoNote: NSObject {
|
||||
])
|
||||
|
||||
guard writer.startWriting() else {
|
||||
throw NSError(domain: "KometVideoNote", code: 4,
|
||||
throw NSError(domain: "QlyraVideoNote", code: 4,
|
||||
userInfo: [NSLocalizedDescriptionKey: "writer refused to start"])
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ final class KometVideoNote: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
extension KometVideoNote: AVCaptureVideoDataOutputSampleBufferDelegate,
|
||||
extension QlyraVideoNote: AVCaptureVideoDataOutputSampleBufferDelegate,
|
||||
AVCaptureAudioDataOutputSampleBufferDelegate {
|
||||
func captureOutput(
|
||||
_ output: AVCaptureOutput,
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)ru.komet.app</string>
|
||||
<string>$(AppIdentifierPrefix)ru.qlyra.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -4,13 +4,13 @@ import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter_timezone/flutter_timezone.dart';
|
||||
import 'package:kolibri/kolibri.dart';
|
||||
import 'package:kusoft/kusoft.dart';
|
||||
import 'package:timezone/data/latest_all.dart' as tz;
|
||||
|
||||
import '../core/cache/self_presence.dart';
|
||||
import '../core/config/config.dart';
|
||||
import '../core/config/countries.dart';
|
||||
import '../core/config/komet_settings.dart';
|
||||
import '../core/config/qlyra_settings.dart';
|
||||
import '../core/config/proxy_config.dart';
|
||||
import '../core/protocol/opcode_map.dart';
|
||||
import '../core/protocol/packet.dart';
|
||||
@@ -27,11 +27,11 @@ enum SessionState { disconnected, connecting, connected, online }
|
||||
|
||||
/// Клиент API.
|
||||
///
|
||||
/// Тонкий адаптер над Rust-ядром [KolibriSession] (пакет kolibri): подключение,
|
||||
/// Тонкий адаптер над Rust-ядром [KusoftSession] (пакет kusoft): подключение,
|
||||
/// хэндшейк, пинг и реконнект живут в ядре, здесь — оркестрация жизненного цикла
|
||||
/// и сохранение прежнего интерфейса для модулей (Packet/пуши/стримы).
|
||||
class Api {
|
||||
KolibriSession? _session;
|
||||
KusoftSession? _session;
|
||||
|
||||
/// Роутер пушей (ответы на запросы ядро матчит само, диспетчер держим только
|
||||
/// ради registerHandler/pushStream).
|
||||
@@ -60,7 +60,7 @@ class Api {
|
||||
String? get callsOsVersion => _callsOsVersion;
|
||||
|
||||
/// Сырой доступ к сессии ядра — для медиа-загрузок (data-plane).
|
||||
KolibriSession? get session => _session;
|
||||
KusoftSession? get session => _session;
|
||||
|
||||
String? spoofScope;
|
||||
|
||||
@@ -284,7 +284,7 @@ class Api {
|
||||
}
|
||||
// Лог запроса/ответа ведётся из wire-лога ядра (_onWireLog) по настоящему
|
||||
// проводному seq, поэтому здесь ничего не пишем.
|
||||
final KolibriResponse resp = await session
|
||||
final KusoftResponse resp = await session
|
||||
.requestMapFull(opcode, Map<String, dynamic>.from(payload))
|
||||
.timeout(
|
||||
ServerConfig.requestTimeout,
|
||||
@@ -368,7 +368,7 @@ class Api {
|
||||
|
||||
/// Строит устройство-поля и создаёт сессию ядра. Заодно заполняет
|
||||
/// [_userAgent] и [_deviceId] для геттеров.
|
||||
Future<(KolibriSession, Stream<WireLogEvent>)> _buildSessionOptions(
|
||||
Future<(KusoftSession, Stream<WireLogEvent>)> _buildSessionOptions(
|
||||
({String host, int port, bool trustMincifryCa}) endpoint,
|
||||
) async {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
@@ -513,7 +513,7 @@ class Api {
|
||||
deviceLocale: deviceLocale,
|
||||
clientSessionId: clientSessionId,
|
||||
pingIntervalSecs: ServerConfig.pingInterval.inSeconds,
|
||||
pingInteractive: !KometSettings.ghostMode.value,
|
||||
pingInteractive: !QlyraSettings.ghostMode.value,
|
||||
autoReconnect: false,
|
||||
insecureTls: insecureTls,
|
||||
proxy: proxy,
|
||||
@@ -665,7 +665,7 @@ class Api {
|
||||
try {
|
||||
await session
|
||||
.requestMapFull(Opcode.ping, {
|
||||
'interactive': !KometSettings.ghostMode.value,
|
||||
'interactive': !QlyraSettings.ghostMode.value,
|
||||
})
|
||||
.timeout(const Duration(seconds: 6));
|
||||
} catch (_) {
|
||||
@@ -720,7 +720,7 @@ class Api {
|
||||
/// синхронизация interactive-флага пинга и присутствия.
|
||||
void _startLiveness() {
|
||||
_livenessTimer?.cancel();
|
||||
_lastInteractive = !KometSettings.ghostMode.value;
|
||||
_lastInteractive = !QlyraSettings.ghostMode.value;
|
||||
_livenessTimer = Timer.periodic(_livenessInterval, (_) => _tickLiveness());
|
||||
}
|
||||
|
||||
@@ -729,11 +729,11 @@ class Api {
|
||||
if (session == null || _sessionState != SessionState.online) return;
|
||||
final st = session.state();
|
||||
if (st != 'online' && st != 'connected') {
|
||||
logger.w('kolibri сессия "$st" — реконнект');
|
||||
logger.w('kusoft сессия "$st" — реконнект');
|
||||
_onDisconnected();
|
||||
return;
|
||||
}
|
||||
final interactive = !KometSettings.ghostMode.value;
|
||||
final interactive = !QlyraSettings.ghostMode.value;
|
||||
if (interactive != _lastInteractive) {
|
||||
_lastInteractive = interactive;
|
||||
try {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import '../api.dart';
|
||||
import '../../core/config/debug_test.dart';
|
||||
import '../../core/config/komet_settings.dart';
|
||||
import '../../core/config/qlyra_settings.dart';
|
||||
import '../../core/protocol/chat_cache_fingerprint.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
@@ -488,7 +488,7 @@ class AccountModule {
|
||||
final data = _requireMapPayload(packet, 'checkPassword');
|
||||
|
||||
if (data['error'] != null) {
|
||||
throw Exception('checkPassword: неверный пароль');
|
||||
throw const WrongPasswordException();
|
||||
}
|
||||
|
||||
final tokenAttrs = data['tokenAttrs'];
|
||||
@@ -534,7 +534,7 @@ class AccountModule {
|
||||
}) {
|
||||
final payload = <dynamic, dynamic>{
|
||||
'token': token,
|
||||
'interactive': interactive ?? !KometSettings.ghostMode.value,
|
||||
'interactive': interactive ?? !QlyraSettings.ghostMode.value,
|
||||
'exp': {
|
||||
'chatsCountGroups': Uint8List.fromList([0x0b, 0x32]),
|
||||
},
|
||||
@@ -645,7 +645,9 @@ class AccountModule {
|
||||
Future<ProfileData> _resurrectProfile(int accountId) async {
|
||||
if (DebugTest.berserk) {
|
||||
await AppDatabase.deleteAccount(accountId);
|
||||
logger.w('login: [BERSERK] профиль удалён из БД, форсирую регенерацию (id=$accountId)');
|
||||
logger.w(
|
||||
'login: [BERSERK] профиль удалён из БД, форсирую регенерацию (id=$accountId)',
|
||||
);
|
||||
} else {
|
||||
final cached = await AppDatabase.loadProfile(accountId);
|
||||
if (cached != null) return cached;
|
||||
@@ -656,11 +658,15 @@ class AccountModule {
|
||||
try {
|
||||
final fetched = await ContactsModule.fetchSelfProfile(_api, accountId);
|
||||
if (fetched != null) {
|
||||
logger.i('login: профиль восстановлен через CONTACT_INFO (id=$accountId)');
|
||||
logger.i(
|
||||
'login: профиль восстановлен через CONTACT_INFO (id=$accountId)',
|
||||
);
|
||||
return fetched;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.w('login: восстановление профиля через CONTACT_INFO не удалось: $e');
|
||||
logger.w(
|
||||
'login: восстановление профиля через CONTACT_INFO не удалось: $e',
|
||||
);
|
||||
}
|
||||
|
||||
logger.w('login: профиль недоступен, использую заглушку (id=$accountId)');
|
||||
|
||||
@@ -290,6 +290,12 @@ class WrongDeviceTokenException implements Exception {
|
||||
String toString() => 'WrongDeviceTokenException';
|
||||
}
|
||||
|
||||
class WrongPasswordException implements Exception {
|
||||
const WrongPasswordException();
|
||||
@override
|
||||
String toString() => 'WrongPasswordException';
|
||||
}
|
||||
|
||||
class RequestCodeResult {
|
||||
final String token;
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ class TwoFactorModule extends AccountApiBase {
|
||||
checkPacketError(packet, 'check2faPassword');
|
||||
final data = packet.payload;
|
||||
if (data is Map && data['error'] != null) {
|
||||
throw Exception('Неверный пароль');
|
||||
throw const WrongPasswordException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class AnimojiModule {
|
||||
'😍',
|
||||
];
|
||||
|
||||
static const String _recentsKey = 'komet_recent_animoji';
|
||||
static const String _recentsKey = 'qlyra_recent_animoji';
|
||||
static const int _maxRecents = 24;
|
||||
|
||||
final Map<int, Animoji> _byId = {};
|
||||
|
||||
@@ -266,13 +266,14 @@ int? _otherParticipantId(dynamic participants, int currentUserId) {
|
||||
String? _nameFromContact(Map<dynamic, dynamic> contact) {
|
||||
final names = contact['names'];
|
||||
if (names is! List || names.isEmpty) return null;
|
||||
final nameRaw = names.firstWhere(
|
||||
(n) => n is Map && n['type'] == 'ONEME',
|
||||
orElse: () => names.firstWhere((n) => n is Map, orElse: () => null),
|
||||
);
|
||||
if (nameRaw is! Map) return null;
|
||||
final name = nameRaw;
|
||||
return name['name'] as String?;
|
||||
Map<dynamic, dynamic>? fallback;
|
||||
for (final entry in names) {
|
||||
if (entry is! Map) continue;
|
||||
final name = entry.cast<dynamic, dynamic>();
|
||||
fallback ??= name;
|
||||
if (name['type'] == 'ONEME') return name['name'] as String?;
|
||||
}
|
||||
return fallback?['name'] as String?;
|
||||
}
|
||||
|
||||
List<ChatSearchHit> parseSearchResult(dynamic payload) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../core/config/komet_settings.dart';
|
||||
import '../../core/config/qlyra_settings.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
import '../../core/cache/info_cache.dart';
|
||||
@@ -446,7 +446,7 @@ class ChatsModule {
|
||||
|
||||
/// Sentinel в `lastMsgText` когда последнее сообщение в чате удалено,
|
||||
/// а кеша истории нет — UI должен отрисовать курсивную плашку.
|
||||
static const String lastMsgPlaceholder = '__komet_lastmsg_placeholder__';
|
||||
static const String lastMsgPlaceholder = '__qlyra_lastmsg_placeholder__';
|
||||
|
||||
ChatsModule._();
|
||||
|
||||
@@ -472,7 +472,7 @@ class ChatsModule {
|
||||
if ((row['unread_count'] as int? ?? 0) == 0) return;
|
||||
|
||||
final msgIdNum = int.tryParse(messageId);
|
||||
if (msgIdNum != null && !KometSettings.antiRead.value) {
|
||||
if (msgIdNum != null && !QlyraSettings.antiRead.value) {
|
||||
try {
|
||||
await api.sendRequest(Opcode.chatMark, {
|
||||
'type': 'READ_MESSAGE',
|
||||
@@ -501,7 +501,7 @@ class ChatsModule {
|
||||
required int remaining,
|
||||
}) async {
|
||||
final msgIdNum = int.tryParse(messageId);
|
||||
if (msgIdNum != null && !KometSettings.antiRead.value) {
|
||||
if (msgIdNum != null && !QlyraSettings.antiRead.value) {
|
||||
try {
|
||||
await api.sendRequest(Opcode.chatMark, {
|
||||
'type': 'READ_MESSAGE',
|
||||
@@ -737,7 +737,7 @@ class ChatsModule {
|
||||
}
|
||||
if (chatId == null) return;
|
||||
|
||||
final keepDeleted = KometSettings.viewDeleted.value;
|
||||
final keepDeleted = QlyraSettings.viewDeleted.value;
|
||||
final ids = payload['messageIds'];
|
||||
if (ids is List) {
|
||||
for (final raw in ids) {
|
||||
@@ -803,7 +803,7 @@ class ChatsModule {
|
||||
}
|
||||
|
||||
if (status == 'REMOVED' && msgIdStr != null) {
|
||||
final keepDeleted = KometSettings.viewDeleted.value;
|
||||
final keepDeleted = QlyraSettings.viewDeleted.value;
|
||||
if (keepDeleted) {
|
||||
await AppDatabase.markMessageDeleted(accountId, chatId, msgIdStr);
|
||||
} else {
|
||||
@@ -847,7 +847,7 @@ class ChatsModule {
|
||||
mergedPayload[entry.key.toString()] = entry.value;
|
||||
}
|
||||
final newRow = Map<String, dynamic>.from(existing);
|
||||
if (KometSettings.viewRedacted.value) {
|
||||
if (QlyraSettings.viewRedacted.value) {
|
||||
final oldText = existing['text']?.toString();
|
||||
if ((oldText ?? '') != (msgText ?? '') &&
|
||||
oldText != null &&
|
||||
|
||||
@@ -11,11 +11,7 @@ class CommentsInfo {
|
||||
final String postId;
|
||||
final int? totalCount;
|
||||
final int? updatedAt;
|
||||
const CommentsInfo({
|
||||
required this.postId,
|
||||
this.totalCount,
|
||||
this.updatedAt,
|
||||
});
|
||||
const CommentsInfo({required this.postId, this.totalCount, this.updatedAt});
|
||||
|
||||
factory CommentsInfo.fromPayload(String postId, Map payload) {
|
||||
final raw = payload['totalCount'];
|
||||
@@ -98,7 +94,8 @@ class CommentsModule {
|
||||
|
||||
final link = msg['link'];
|
||||
final postId =
|
||||
(payload['postId'] ?? (link is Map ? link['postId'] : null) ??
|
||||
(payload['postId'] ??
|
||||
(link is Map ? link['postId'] : null) ??
|
||||
msg['postId'])
|
||||
?.toString();
|
||||
if (postId == null || postId.isEmpty) return;
|
||||
|
||||
@@ -21,6 +21,7 @@ class CachedContact {
|
||||
final String? baseRawUrl;
|
||||
final int updateTime;
|
||||
final Set<String> options;
|
||||
final int accountStatus;
|
||||
|
||||
const CachedContact({
|
||||
required this.id,
|
||||
@@ -33,12 +34,14 @@ class CachedContact {
|
||||
this.baseRawUrl,
|
||||
required this.updateTime,
|
||||
this.options = const {},
|
||||
this.accountStatus = 0,
|
||||
});
|
||||
|
||||
bool get isOfficial => options.contains('OFFICIAL');
|
||||
bool get isBot => options.contains('BOT');
|
||||
bool get isServiceAccount => options.contains('SERVICE_ACCOUNT');
|
||||
bool get isVerified => isOfficial;
|
||||
bool get isDeleted => accountStatus != 0;
|
||||
|
||||
factory CachedContact.fromDbRow(Map<String, dynamic> row) => CachedContact(
|
||||
id: row['id'] as int,
|
||||
@@ -51,6 +54,7 @@ class CachedContact {
|
||||
baseRawUrl: row['base_raw_url'] as String?,
|
||||
updateTime: row['update_time'] as int,
|
||||
options: _decodeOptions(row['options']),
|
||||
accountStatus: (row['account_status'] as int?) ?? 0,
|
||||
);
|
||||
|
||||
static Set<String> _decodeOptions(dynamic raw) {
|
||||
@@ -183,6 +187,7 @@ class ContactsModule {
|
||||
'base_raw_url': null,
|
||||
'update_time': 0,
|
||||
'options': null,
|
||||
'account_status': 0,
|
||||
};
|
||||
|
||||
if (row == null) return null;
|
||||
@@ -397,7 +402,14 @@ class ContactsModule {
|
||||
int accountId,
|
||||
) async {
|
||||
final contacts = data['contacts'];
|
||||
if (contacts is! List || contacts.isEmpty) return;
|
||||
if (contacts is! List) {
|
||||
logger.i('Контакты: сервер не прислал список (акк $accountId)');
|
||||
return;
|
||||
}
|
||||
if (contacts.isEmpty) {
|
||||
logger.i('Контакты: сервер прислал пустой список (акк $accountId)');
|
||||
return;
|
||||
}
|
||||
|
||||
final rows = <Map<String, dynamic>>[];
|
||||
for (final raw in contacts.whereType<Map>()) {
|
||||
@@ -492,8 +504,14 @@ class ContactsModule {
|
||||
return photos;
|
||||
}
|
||||
|
||||
static Future<List<CachedContact>> getContacts(int accountId) async {
|
||||
final rows = await AppDatabase.loadContacts(accountId);
|
||||
static Future<List<CachedContact>> getContacts(
|
||||
int accountId, {
|
||||
bool includeDeleted = false,
|
||||
}) async {
|
||||
final rows = await AppDatabase.loadContacts(
|
||||
accountId,
|
||||
includeDeleted: includeDeleted,
|
||||
);
|
||||
return rows.map(CachedContact.fromDbRow).toList();
|
||||
}
|
||||
|
||||
@@ -559,7 +577,7 @@ class ContactsModule {
|
||||
firstName: '$first ${i + 1}',
|
||||
lastName: last,
|
||||
phone: 79000000000 + i,
|
||||
baseUrl: 'https://i.pravatar.cc/150?u=komet_debug_$i',
|
||||
baseUrl: 'https://i.pravatar.cc/150?u=qlyra_debug_$i',
|
||||
updateTime: 1,
|
||||
options: i % 6 == 0 ? const {'OFFICIAL'} : const {},
|
||||
),
|
||||
@@ -571,7 +589,7 @@ class ContactsModule {
|
||||
/// Прогревает in-memory ContactCache из локальных контактов.
|
||||
/// Нужно вызывать на cold start: иначе кэш пуст до следующего логина.
|
||||
static Future<void> primeCacheFromDb(int accountId) async {
|
||||
final contacts = await getContacts(accountId);
|
||||
final contacts = await getContacts(accountId, includeDeleted: true);
|
||||
for (final c in contacts) {
|
||||
ContactCache.putPhone(c.id, c.phone);
|
||||
final fullName = (c.lastName != null && c.lastName!.isNotEmpty)
|
||||
@@ -632,6 +650,7 @@ class ContactsModule {
|
||||
'base_raw_url': contact['baseRawUrl'] as String?,
|
||||
'update_time': (contact['updateTime'] as int?) ?? 0,
|
||||
'options': optionsStr,
|
||||
'account_status': (contact['accountStatus'] as int?) ?? 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:convert' show jsonDecode, utf8;
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:kolibri/kolibri.dart' as kb;
|
||||
import 'package:kusoft/kusoft.dart' as kb;
|
||||
|
||||
import '../api.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
@@ -47,7 +47,7 @@ class UploadError extends UploadEvent {
|
||||
}
|
||||
|
||||
/// Оркестратор медиа-загрузок: control-plane (URL, отправка сообщения) идёт
|
||||
/// обычными опкодами, data-plane (заливка на CDN) — через Rust-ядро kolibri,
|
||||
/// обычными опкодами, data-plane (заливка на CDN) — через Rust-ядро kusoft,
|
||||
/// которое стримит файл с диска (не держит его целиком в памяти).
|
||||
class FileUploader {
|
||||
final Api api;
|
||||
@@ -101,39 +101,35 @@ class FileUploader {
|
||||
var status = 0;
|
||||
String? error;
|
||||
final done = Completer<void>();
|
||||
sub =
|
||||
session
|
||||
.uploadFilePath(
|
||||
url: info.url,
|
||||
path: file.path,
|
||||
filename: filename,
|
||||
connection: 'close',
|
||||
)
|
||||
.listen(
|
||||
(e) {
|
||||
switch (e) {
|
||||
case kb.UploadEvent_Progress(:final sent, :final total):
|
||||
ctrl.add(
|
||||
UploadProgress(
|
||||
sent: sent.toInt(),
|
||||
total: total.toInt(),
|
||||
),
|
||||
);
|
||||
case kb.UploadEvent_Done(status: final s):
|
||||
status = s;
|
||||
case kb.UploadEvent_Error(:final message):
|
||||
error = message;
|
||||
}
|
||||
},
|
||||
onError: (Object err) {
|
||||
error = err.toString();
|
||||
if (!done.isCompleted) done.complete();
|
||||
},
|
||||
onDone: () {
|
||||
if (!done.isCompleted) done.complete();
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
sub = session
|
||||
.uploadFilePath(
|
||||
url: info.url,
|
||||
path: file.path,
|
||||
filename: filename,
|
||||
connection: 'close',
|
||||
)
|
||||
.listen(
|
||||
(e) {
|
||||
switch (e) {
|
||||
case kb.UploadEvent_Progress(:final sent, :final total):
|
||||
ctrl.add(
|
||||
UploadProgress(sent: sent.toInt(), total: total.toInt()),
|
||||
);
|
||||
case kb.UploadEvent_Done(status: final s):
|
||||
status = s;
|
||||
case kb.UploadEvent_Error(:final message):
|
||||
error = message;
|
||||
}
|
||||
},
|
||||
onError: (Object err) {
|
||||
error = err.toString();
|
||||
if (!done.isCompleted) done.complete();
|
||||
},
|
||||
onDone: () {
|
||||
if (!done.isCompleted) done.complete();
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
await done.future;
|
||||
if (cancelled) return;
|
||||
if (error != null) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../api.dart';
|
||||
import '../../core/config/komet_settings.dart';
|
||||
import '../../core/config/qlyra_settings.dart';
|
||||
import '../../core/contacts/device_contacts_service.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
@@ -737,7 +737,7 @@ class MessagesModule {
|
||||
}
|
||||
}
|
||||
|
||||
final toSave = KometSettings.viewRedacted.value && results.isNotEmpty
|
||||
final toSave = QlyraSettings.viewRedacted.value && results.isNotEmpty
|
||||
? await _mergeEditHistory(accountId, chatId, results)
|
||||
: results;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
|
||||
import '../../core/cache/info_cache.dart';
|
||||
import '../../core/cache/self_presence.dart';
|
||||
import '../../core/config/komet_settings.dart';
|
||||
import '../../core/config/qlyra_settings.dart';
|
||||
import '../../core/storage/token_storage.dart';
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../api.dart';
|
||||
@@ -40,7 +40,7 @@ class SelfCheckService {
|
||||
Future<void> _check() async {
|
||||
final api = _api;
|
||||
if (api == null || api.state != SessionState.online) return;
|
||||
if (!KometSettings.selfOnlineCheck.value) return;
|
||||
if (!QlyraSettings.selfOnlineCheck.value) return;
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) return;
|
||||
final presence = await PresenceFetch.get(accountId, forceRefresh: true);
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import '../../core/media/gallery_source.dart';
|
||||
import '../../core/media/share_thumbnail.dart';
|
||||
import '../../core/media/video_transcoder.dart';
|
||||
import '../../core/storage/app_database.dart';
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../../models/attachment.dart';
|
||||
import '../../models/chat_preview_media.dart';
|
||||
import '../../models/shared_payload.dart';
|
||||
import '../../main.dart';
|
||||
import 'chats.dart';
|
||||
import 'messages.dart';
|
||||
import 'upload_service.dart';
|
||||
|
||||
class PreparedShareFile {
|
||||
final SharedFile source;
|
||||
final String? thumbDataUri;
|
||||
final int? width;
|
||||
final int? height;
|
||||
final int? durationMs;
|
||||
|
||||
const PreparedShareFile({
|
||||
required this.source,
|
||||
this.thumbDataUri,
|
||||
this.width,
|
||||
this.height,
|
||||
this.durationMs,
|
||||
});
|
||||
|
||||
SharedFileKind get kind => source.kind;
|
||||
File get file => source.file;
|
||||
}
|
||||
|
||||
class PreparedShare {
|
||||
final List<PreparedShareFile> files;
|
||||
final String? text;
|
||||
|
||||
const PreparedShare({required this.files, this.text});
|
||||
|
||||
List<PreparedShareFile> get photos =>
|
||||
files.where((f) => f.kind == SharedFileKind.photo).toList();
|
||||
|
||||
List<PreparedShareFile> get videos =>
|
||||
files.where((f) => f.kind == SharedFileKind.video).toList();
|
||||
|
||||
List<PreparedShareFile> get documents =>
|
||||
files.where((f) => f.kind == SharedFileKind.file).toList();
|
||||
|
||||
bool get isTextOnly => files.isEmpty;
|
||||
|
||||
static Future<PreparedShare> prepare(SharedPayload payload) async {
|
||||
final prepared = <PreparedShareFile>[];
|
||||
for (final source in payload.files) {
|
||||
prepared.add(await _prepareOne(source));
|
||||
}
|
||||
return PreparedShare(files: prepared, text: payload.text);
|
||||
}
|
||||
|
||||
static Future<PreparedShareFile> _prepareOne(SharedFile source) async {
|
||||
final thumb = await sharedThumbnailDataUri(source);
|
||||
switch (source.kind) {
|
||||
case SharedFileKind.photo:
|
||||
final dim = await imageFileDimensions(source.file);
|
||||
return PreparedShareFile(
|
||||
source: source,
|
||||
thumbDataUri: thumb,
|
||||
width: dim?.$1,
|
||||
height: dim?.$2,
|
||||
);
|
||||
case SharedFileKind.video:
|
||||
VideoInfo? info;
|
||||
try {
|
||||
info = await VideoTranscoder.probe(source.path);
|
||||
} catch (e) {
|
||||
logger.w('Поделиться: probe ${source.path}: $e');
|
||||
}
|
||||
return PreparedShareFile(
|
||||
source: source,
|
||||
thumbDataUri: thumb,
|
||||
width: (info?.width ?? 0) > 0 ? info!.width : null,
|
||||
height: (info?.height ?? 0) > 0 ? info!.height : null,
|
||||
durationMs: (info?.durationMs ?? 0) > 0 ? info!.durationMs : null,
|
||||
);
|
||||
case SharedFileKind.file:
|
||||
return PreparedShareFile(source: source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShareSendResult {
|
||||
final int chatCount;
|
||||
final int messageCount;
|
||||
|
||||
const ShareSendResult({required this.chatCount, required this.messageCount});
|
||||
}
|
||||
|
||||
class ShareSender {
|
||||
ShareSender._();
|
||||
|
||||
static final Map<
|
||||
String,
|
||||
({int accountId, int chatId, String text, String? preview, int time})
|
||||
>
|
||||
_tracked = {};
|
||||
|
||||
static StreamSubscription<UploadJobEvent>? _sub;
|
||||
|
||||
static void _track(
|
||||
String tempId, {
|
||||
required int accountId,
|
||||
required int chatId,
|
||||
required String text,
|
||||
required String? preview,
|
||||
required int time,
|
||||
}) {
|
||||
_sub ??= UploadService.instance.events.listen(_onUploadEvent);
|
||||
_tracked[tempId] = (
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
text: text,
|
||||
preview: preview,
|
||||
time: time,
|
||||
);
|
||||
}
|
||||
|
||||
static void _onUploadEvent(UploadJobEvent event) {
|
||||
final entry = _tracked.remove(event.tempId);
|
||||
if (entry == null) return;
|
||||
final status = event is UploadJobDone ? 'sent' : 'error';
|
||||
final messageId = event is UploadJobDone
|
||||
? (event.message?.id ?? event.tempId)
|
||||
: event.tempId;
|
||||
unawaited(
|
||||
chats
|
||||
.applyOutgoing(
|
||||
entry.accountId,
|
||||
entry.chatId,
|
||||
messageId: messageId,
|
||||
time: entry.time,
|
||||
text: entry.text,
|
||||
status: status,
|
||||
preview: entry.preview,
|
||||
)
|
||||
.catchError((Object e) {
|
||||
logger.w('Поделиться: не обновить превью чата ${entry.chatId}: $e');
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<ShareSendResult> send({
|
||||
required int accountId,
|
||||
required List<int> chatIds,
|
||||
required PreparedShare share,
|
||||
required String caption,
|
||||
}) async {
|
||||
var messages = 0;
|
||||
for (final chatId in chatIds) {
|
||||
messages += await _sendToChat(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
share: share,
|
||||
caption: caption,
|
||||
);
|
||||
}
|
||||
logger.i(
|
||||
'Поделиться: отправлено $messages сообщений в ${chatIds.length} чатов',
|
||||
);
|
||||
return ShareSendResult(chatCount: chatIds.length, messageCount: messages);
|
||||
}
|
||||
|
||||
static Future<int> _sendToChat({
|
||||
required int accountId,
|
||||
required int chatId,
|
||||
required PreparedShare share,
|
||||
required String caption,
|
||||
}) async {
|
||||
if (share.isTextOnly) {
|
||||
final text = caption.trim().isNotEmpty
|
||||
? caption
|
||||
: (share.text ?? '').trim();
|
||||
if (text.isEmpty) return 0;
|
||||
await _sendText(accountId: accountId, chatId: chatId, text: text);
|
||||
return 1;
|
||||
}
|
||||
|
||||
final photos = share.photos;
|
||||
final videos = share.videos;
|
||||
final documents = share.documents;
|
||||
|
||||
var used = false;
|
||||
String take() {
|
||||
if (used || caption.isEmpty) return '';
|
||||
used = true;
|
||||
return caption;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
if (photos.isNotEmpty) {
|
||||
await _sendPhotos(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
photos: photos,
|
||||
caption: take(),
|
||||
);
|
||||
count++;
|
||||
}
|
||||
for (final video in videos) {
|
||||
await _sendVideo(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
video: video,
|
||||
caption: take(),
|
||||
);
|
||||
count++;
|
||||
}
|
||||
for (final document in documents) {
|
||||
await _sendDocument(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
document: document,
|
||||
caption: take(),
|
||||
);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static Future<void> _sendText({
|
||||
required int accountId,
|
||||
required int chatId,
|
||||
required String text,
|
||||
}) async {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final tempId = UploadService.instance.newTempId();
|
||||
final placeholder = CachedMessage(
|
||||
id: tempId,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
senderId: accountId,
|
||||
text: text,
|
||||
time: now,
|
||||
status: 'sending',
|
||||
);
|
||||
await _persist(placeholder);
|
||||
await _bumpChat(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
preview: null,
|
||||
status: 'sending',
|
||||
);
|
||||
|
||||
String realId = tempId;
|
||||
var status = 'sent';
|
||||
try {
|
||||
final sent = await messagesModule.sendMessage(accountId, chatId, text);
|
||||
if (sent.isNotEmpty) realId = sent;
|
||||
} catch (e) {
|
||||
logger.w('Поделиться: текст в $chatId не ушёл: $e');
|
||||
status = 'error';
|
||||
}
|
||||
final settled = CachedMessage(
|
||||
id: realId,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
senderId: accountId,
|
||||
text: text,
|
||||
time: now,
|
||||
status: status,
|
||||
);
|
||||
await _persist(settled, removeId: realId == tempId ? null : tempId);
|
||||
await _bumpChat(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
messageId: realId,
|
||||
time: now,
|
||||
text: text,
|
||||
preview: null,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _sendPhotos({
|
||||
required int accountId,
|
||||
required int chatId,
|
||||
required List<PreparedShareFile> photos,
|
||||
required String caption,
|
||||
}) async {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final tempId = UploadService.instance.newTempId();
|
||||
|
||||
final jobs = <({File file, GalleryItem? item})>[];
|
||||
final attachments = <PhotoAttachment>[];
|
||||
for (final photo in photos) {
|
||||
jobs.add((file: photo.file, item: null));
|
||||
attachments.add(
|
||||
PhotoAttachment(
|
||||
localPath: photo.file.path,
|
||||
previewData: photo.thumbDataUri,
|
||||
width: photo.width,
|
||||
height: photo.height,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (jobs.isEmpty) return;
|
||||
|
||||
final label = photos.length > 1 ? 'Изображения' : 'Изображение';
|
||||
final preview = _preview(
|
||||
kind: ChatPreviewKind.photo,
|
||||
files: photos,
|
||||
label: caption.isEmpty ? label : null,
|
||||
);
|
||||
final placeholder = CachedMessage(
|
||||
id: tempId,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
senderId: accountId,
|
||||
text: caption.isEmpty ? null : caption,
|
||||
time: now,
|
||||
status: 'sending',
|
||||
attachments: attachments,
|
||||
);
|
||||
|
||||
await _persist(placeholder);
|
||||
final text = caption.isEmpty ? label : caption;
|
||||
await _bumpChat(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
preview: preview,
|
||||
status: 'sending',
|
||||
);
|
||||
_track(
|
||||
tempId,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
text: text,
|
||||
preview: preview,
|
||||
time: now,
|
||||
);
|
||||
|
||||
unawaited(
|
||||
UploadService.instance.sendPhotos(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
tempId: tempId,
|
||||
jobs: jobs,
|
||||
caption: caption,
|
||||
placeholder: placeholder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _sendVideo({
|
||||
required int accountId,
|
||||
required int chatId,
|
||||
required PreparedShareFile video,
|
||||
required String caption,
|
||||
}) async {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final tempId = UploadService.instance.newTempId();
|
||||
|
||||
final preview = _preview(
|
||||
kind: ChatPreviewKind.video,
|
||||
files: [video],
|
||||
label: caption.isEmpty ? 'Видео' : null,
|
||||
);
|
||||
final placeholder = CachedMessage(
|
||||
id: tempId,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
senderId: accountId,
|
||||
text: caption.isEmpty ? null : caption,
|
||||
time: now,
|
||||
status: 'sending',
|
||||
attachments: [
|
||||
VideoAttachment(
|
||||
localPath: video.file.path,
|
||||
previewData: video.thumbDataUri,
|
||||
width: video.width,
|
||||
height: video.height,
|
||||
duration: video.durationMs,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await _persist(placeholder);
|
||||
final text = caption.isEmpty ? 'Видео' : caption;
|
||||
await _bumpChat(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
preview: preview,
|
||||
status: 'sending',
|
||||
);
|
||||
_track(
|
||||
tempId,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
text: text,
|
||||
preview: preview,
|
||||
time: now,
|
||||
);
|
||||
|
||||
unawaited(
|
||||
UploadService.instance.sendVideo(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
tempId: tempId,
|
||||
file: video.file,
|
||||
caption: caption,
|
||||
placeholder: placeholder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _sendDocument({
|
||||
required int accountId,
|
||||
required int chatId,
|
||||
required PreparedShareFile document,
|
||||
required String caption,
|
||||
}) async {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final tempId = UploadService.instance.newTempId();
|
||||
final name = document.source.name;
|
||||
final size = document.source.size;
|
||||
|
||||
final preview = ChatPreviewMedia(
|
||||
kind: ChatPreviewKind.file,
|
||||
label: caption.isEmpty ? 'Файл' : null,
|
||||
detail: caption.isEmpty ? name : null,
|
||||
).encode();
|
||||
final placeholder = CachedMessage(
|
||||
id: tempId,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
senderId: accountId,
|
||||
text: caption.isEmpty ? null : caption,
|
||||
time: now,
|
||||
status: 'sending',
|
||||
attachments: [FileAttachment(name: name, size: size)],
|
||||
);
|
||||
|
||||
await _persist(placeholder);
|
||||
final text = caption.isEmpty ? 'Файл: $name' : caption;
|
||||
await _bumpChat(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
preview: preview,
|
||||
status: 'sending',
|
||||
);
|
||||
_track(
|
||||
tempId,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
text: text,
|
||||
preview: preview,
|
||||
time: now,
|
||||
);
|
||||
|
||||
unawaited(
|
||||
UploadService.instance.sendFile(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
tempId: tempId,
|
||||
source: document.file,
|
||||
filename: name,
|
||||
size: size,
|
||||
placeholder: placeholder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String? _preview({
|
||||
required ChatPreviewKind kind,
|
||||
required List<PreparedShareFile> files,
|
||||
String? label,
|
||||
}) {
|
||||
final thumbs = <ChatPreviewThumb>[];
|
||||
for (final file in files) {
|
||||
final data = file.thumbDataUri;
|
||||
if (data == null) continue;
|
||||
thumbs.add(
|
||||
ChatPreviewThumb(
|
||||
source: data,
|
||||
video: file.kind == SharedFileKind.video,
|
||||
),
|
||||
);
|
||||
if (thumbs.length >= 3) break;
|
||||
}
|
||||
if (thumbs.isEmpty && label == null) return null;
|
||||
return ChatPreviewMedia(kind: kind, thumbs: thumbs, label: label).encode();
|
||||
}
|
||||
|
||||
static Future<void> _persist(
|
||||
CachedMessage message, {
|
||||
String? removeId,
|
||||
}) async {
|
||||
try {
|
||||
if (removeId != null && removeId != message.id) {
|
||||
await AppDatabase.deleteMessage(
|
||||
message.accountId,
|
||||
message.chatId,
|
||||
removeId,
|
||||
);
|
||||
}
|
||||
await AppDatabase.saveMessages([message.toDbRow()]);
|
||||
} catch (e) {
|
||||
logger.w('Поделиться: не сохранить плейсхолдер: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _bumpChat({
|
||||
required int accountId,
|
||||
required int chatId,
|
||||
required String messageId,
|
||||
required int time,
|
||||
required String text,
|
||||
required String? preview,
|
||||
required String status,
|
||||
}) async {
|
||||
try {
|
||||
await chats.applyOutgoing(
|
||||
accountId,
|
||||
chatId,
|
||||
messageId: messageId,
|
||||
time: time,
|
||||
text: text,
|
||||
status: status,
|
||||
preview: preview,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.w('Поделиться: не обновить строку чата $chatId: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,9 +45,7 @@ class StoriesModule {
|
||||
|
||||
int _nowMs() => DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
int _normMs(int t) => t <= 0
|
||||
? 0
|
||||
: (t < 1000000000000 ? t * 1000 : t);
|
||||
int _normMs(int t) => t <= 0 ? 0 : (t < 1000000000000 ? t * 1000 : t);
|
||||
|
||||
// ── Кэш (SQLite) ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -196,9 +194,7 @@ class StoriesModule {
|
||||
final chunk = missing.sublist(i, end);
|
||||
try {
|
||||
final packet = await _api.sendRequest(Opcode.storiesGetByOwner, {
|
||||
'owners': [
|
||||
for (final id in chunk) StoryOwner(ownerId: id).toMap(),
|
||||
],
|
||||
'owners': [for (final id in chunk) StoryOwner(ownerId: id).toMap()],
|
||||
}, silent: true);
|
||||
if (packet.isError) continue;
|
||||
if (_applyOwnerPayload(packet.payload, chunk)) changed = true;
|
||||
@@ -230,7 +226,8 @@ class StoriesModule {
|
||||
}
|
||||
}
|
||||
for (final id in requested) {
|
||||
if (!seen.contains(id) && _peerPreviews.remove(id) != null) changed = true;
|
||||
if (!seen.contains(id) && _peerPreviews.remove(id) != null)
|
||||
changed = true;
|
||||
}
|
||||
final rawPeers = data['peerStories'];
|
||||
if (rawPeers is List) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../main.dart' show KometApp;
|
||||
import '../../main.dart' show QlyraApp;
|
||||
|
||||
enum UploadKind { photo, video, videoNote, voice, file }
|
||||
|
||||
@@ -53,7 +53,7 @@ class _NotificationJob {
|
||||
|
||||
class UploadNotificationService {
|
||||
static const MethodChannel _channel = MethodChannel(
|
||||
'ru.komet.app/upload_service',
|
||||
'ru.qlyra.app/upload_service',
|
||||
);
|
||||
static const int _minIntervalMs = 350;
|
||||
static const Duration _startDelay = Duration(milliseconds: 700);
|
||||
@@ -189,7 +189,7 @@ class UploadNotificationService {
|
||||
}
|
||||
|
||||
static AppLocalizations _localizations() {
|
||||
final context = KometApp.navigatorKey.currentContext;
|
||||
final context = QlyraApp.navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
final scoped = Localizations.of<AppLocalizations>(
|
||||
context,
|
||||
|
||||