parent
a6d962f9ba
commit
bf2f412de9
@ -1,26 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
def flutterSdkPath = {
|
|
||||||
def properties = new Properties()
|
|
||||||
file("local.properties").withInputStream { properties.load(it) }
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
|
||||||
return flutterSdkPath
|
|
||||||
}()
|
|
||||||
|
|
||||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
|
||||||
id "com.android.application" version "8.7.3" apply false
|
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
|
|
||||||
id "com.google.gms.google-services" version "4.3.8" apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
include ":app"
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
Hermes.AppDir
|
|
||||||
*.AppImage
|
|
||||||
*.AppImage.zsync
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
exec ./hermes
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Version=1.0
|
|
||||||
Name=Hermes
|
|
||||||
Comment=Matrix Client. Chat with your friends
|
|
||||||
Exec=AppRun
|
|
||||||
Icon=hermes
|
|
||||||
Terminal=false
|
|
||||||
Categories=Network;Chat;InstantMessaging;X-Matrix;
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
# Hermes AppImage
|
|
||||||
|
|
||||||
Hermes is provided as AppImage too. To Download, visit hermes.im.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
- Ensure you install `appimagetool`
|
|
||||||
|
|
||||||
```shell
|
|
||||||
flutter build linux
|
|
||||||
|
|
||||||
# copy binaries to appimage dir
|
|
||||||
cp -r build/linux/{x64,arm64}/release/bundle appimage/Hermes.AppDir
|
|
||||||
cd appimage
|
|
||||||
|
|
||||||
# prepare AppImage files
|
|
||||||
cp Hermes.desktop Hermes.AppDir/
|
|
||||||
mkdir -p Hermes.AppDir/usr/share/icons
|
|
||||||
cp ../assets/logo.svg Hermes.AppDir/hermes.svg
|
|
||||||
cp AppRun Hermes.AppDir
|
|
||||||
|
|
||||||
# build the AppImage
|
|
||||||
appimagetool Hermes.AppDir
|
|
||||||
```
|
|
||||||
@ -1,2 +1,2 @@
|
|||||||
# im.hermes.Hermes
|
# im.pantheon.Hermes
|
||||||
8b25b37b-f160-4350-b4f6-9a04554e8f9e
|
8b25b37b-f160-4350-b4f6-9a04554e8f9e
|
||||||
|
|||||||
@ -1,255 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:path/path.dart' as path_lib;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:record/record.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
import 'package:hermes/l10n/l10n.dart';
|
|
||||||
import 'package:hermes/config/app_config.dart';
|
|
||||||
import 'package:hermes/config/setting_keys.dart';
|
|
||||||
import 'package:hermes/utils/platform_infos.dart';
|
|
||||||
import 'package:hermes/widgets/matrix.dart';
|
|
||||||
import 'events/audio_player.dart';
|
|
||||||
|
|
||||||
class RecordingDialog extends StatefulWidget {
|
|
||||||
const RecordingDialog({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
RecordingDialogState createState() => RecordingDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class RecordingDialogState extends State<RecordingDialog> {
|
|
||||||
Timer? _recorderSubscription;
|
|
||||||
Duration _duration = Duration.zero;
|
|
||||||
|
|
||||||
bool error = false;
|
|
||||||
|
|
||||||
final _audioRecorder = AudioRecorder();
|
|
||||||
final List<double> amplitudeTimeline = [];
|
|
||||||
|
|
||||||
String? fileName;
|
|
||||||
|
|
||||||
Future<void> startRecording() async {
|
|
||||||
final store = Matrix.of(context).store;
|
|
||||||
try {
|
|
||||||
final codec = kIsWeb
|
|
||||||
// Web seems to create webm instead of ogg when using opus encoder
|
|
||||||
// which does not play on iOS right now. So we use wav for now:
|
|
||||||
? AudioEncoder.wav
|
|
||||||
// Everywhere else we use opus if supported by the platform:
|
|
||||||
: await _audioRecorder.isEncoderSupported(AudioEncoder.opus)
|
|
||||||
? AudioEncoder.opus
|
|
||||||
: AudioEncoder.aacLc;
|
|
||||||
fileName =
|
|
||||||
'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}';
|
|
||||||
String? path;
|
|
||||||
if (!kIsWeb) {
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
path = path_lib.join(tempDir.path, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await _audioRecorder.hasPermission();
|
|
||||||
if (result != true) {
|
|
||||||
setState(() => error = true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await WakelockPlus.enable();
|
|
||||||
|
|
||||||
await _audioRecorder.start(
|
|
||||||
RecordConfig(
|
|
||||||
bitRate: AppSettings.audioRecordingBitRate.getItem(store),
|
|
||||||
sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store),
|
|
||||||
numChannels: AppSettings.audioRecordingNumChannels.getItem(store),
|
|
||||||
autoGain: AppSettings.audioRecordingAutoGain.getItem(store),
|
|
||||||
echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store),
|
|
||||||
noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store),
|
|
||||||
encoder: codec,
|
|
||||||
),
|
|
||||||
path: path ?? '',
|
|
||||||
);
|
|
||||||
setState(() => _duration = Duration.zero);
|
|
||||||
_recorderSubscription?.cancel();
|
|
||||||
_recorderSubscription =
|
|
||||||
Timer.periodic(const Duration(milliseconds: 100), (_) async {
|
|
||||||
final amplitude = await _audioRecorder.getAmplitude();
|
|
||||||
var value = 100 + amplitude.current * 2;
|
|
||||||
value = value < 1 ? 1 : value;
|
|
||||||
amplitudeTimeline.add(value);
|
|
||||||
setState(() {
|
|
||||||
_duration += const Duration(milliseconds: 100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (_) {
|
|
||||||
setState(() => error = true);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
startRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
WakelockPlus.disable();
|
|
||||||
_recorderSubscription?.cancel();
|
|
||||||
_audioRecorder.stop();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _stopAndSend() async {
|
|
||||||
_recorderSubscription?.cancel();
|
|
||||||
final path = await _audioRecorder.stop();
|
|
||||||
|
|
||||||
if (path == null) throw ('Recording failed!');
|
|
||||||
const waveCount = AudioPlayerWidget.wavesCount;
|
|
||||||
final step = amplitudeTimeline.length < waveCount
|
|
||||||
? 1
|
|
||||||
: (amplitudeTimeline.length / waveCount).round();
|
|
||||||
final waveform = <int>[];
|
|
||||||
for (var i = 0; i < amplitudeTimeline.length; i += step) {
|
|
||||||
waveform.add((amplitudeTimeline[i] / 100 * 1024).round());
|
|
||||||
}
|
|
||||||
Navigator.of(context, rootNavigator: false).pop<RecordingResult>(
|
|
||||||
RecordingResult(
|
|
||||||
path: path,
|
|
||||||
duration: _duration.inMilliseconds,
|
|
||||||
waveform: waveform,
|
|
||||||
fileName: fileName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
const maxDecibalWidth = 64.0;
|
|
||||||
final time =
|
|
||||||
'${_duration.inMinutes.toString().padLeft(2, '0')}:${(_duration.inSeconds % 60).toString().padLeft(2, '0')}';
|
|
||||||
final content = error
|
|
||||||
? Text(L10n.of(context).oopsSomethingWentWrong)
|
|
||||||
: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: amplitudeTimeline.reversed
|
|
||||||
.take(26)
|
|
||||||
.toList()
|
|
||||||
.reversed
|
|
||||||
.map(
|
|
||||||
(amplitude) => Container(
|
|
||||||
margin: const EdgeInsets.only(left: 2),
|
|
||||||
width: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(AppConfig.borderRadius),
|
|
||||||
),
|
|
||||||
height: maxDecibalWidth * (amplitude / 100),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: 48,
|
|
||||||
child: Text(time),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (PlatformInfos.isCupertinoStyle) {
|
|
||||||
return CupertinoAlertDialog(
|
|
||||||
content: content,
|
|
||||||
actions: [
|
|
||||||
CupertinoDialogAction(
|
|
||||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
|
||||||
child: Text(
|
|
||||||
L10n.of(context).cancel,
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.textTheme.bodyMedium?.color?.withAlpha(150),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (error != true)
|
|
||||||
CupertinoDialogAction(
|
|
||||||
onPressed: _stopAndSend,
|
|
||||||
child: Text(L10n.of(context).send),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return AlertDialog(
|
|
||||||
content: content,
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
|
||||||
child: Text(
|
|
||||||
L10n.of(context).cancel,
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (error != true)
|
|
||||||
TextButton(
|
|
||||||
onPressed: _stopAndSend,
|
|
||||||
child: Text(L10n.of(context).send),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RecordingResult {
|
|
||||||
final String path;
|
|
||||||
final int duration;
|
|
||||||
final List<int> waveform;
|
|
||||||
final String? fileName;
|
|
||||||
|
|
||||||
const RecordingResult({
|
|
||||||
required this.path,
|
|
||||||
required this.duration,
|
|
||||||
required this.waveform,
|
|
||||||
required this.fileName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on AudioEncoder {
|
|
||||||
String get fileExtension {
|
|
||||||
switch (this) {
|
|
||||||
case AudioEncoder.aacLc:
|
|
||||||
case AudioEncoder.aacEld:
|
|
||||||
case AudioEncoder.aacHe:
|
|
||||||
return 'm4a';
|
|
||||||
case AudioEncoder.opus:
|
|
||||||
return 'ogg';
|
|
||||||
case AudioEncoder.wav:
|
|
||||||
return 'wav';
|
|
||||||
case AudioEncoder.amrNb:
|
|
||||||
case AudioEncoder.amrWb:
|
|
||||||
case AudioEncoder.flac:
|
|
||||||
case AudioEncoder.pcm16bits:
|
|
||||||
throw UnsupportedError('Not yet used');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
diff --git a/android/app/build.gradle b/android/app/build.gradle
|
|
||||||
index bb8e015cd..3ff4a7579 100644
|
|
||||||
--- a/android/app/build.gradle
|
|
||||||
+++ b/android/app/build.gradle
|
|
||||||
@@ -2,7 +2,7 @@ plugins {
|
|
||||||
id "com.android.application"
|
|
||||||
id "kotlin-android"
|
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
|
||||||
- //id "com.google.gms.google-services"
|
|
||||||
+ id "com.google.gms.google-services"
|
|
||||||
}
|
|
||||||
|
|
||||||
def localProperties = new Properties()
|
|
||||||
@@ -97,11 +97,12 @@ flutter {
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
- //implementation 'com.google.firebase:firebase-messaging:19.0.1' // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698
|
|
||||||
+ implementation 'com.google.firebase:firebase-messaging:19.0.1'
|
|
||||||
+ // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698
|
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations.all {
|
|
||||||
- exclude group: 'com.google.android.gms'
|
|
||||||
+ //exclude group: 'com.google.android.gms'
|
|
||||||
}
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
|
|
||||||
index d0e0fbc9..0a546da0 100644
|
|
||||||
--- a/android/app/proguard-rules.pro
|
|
||||||
+++ b/android/app/proguard-rules.pro
|
|
||||||
@@ -1 +1,42 @@
|
|
||||||
--keep class net.sqlcipher.** { *; }
|
|
||||||
\ No newline at end of file
|
|
||||||
+-optimizationpasses 5
|
|
||||||
+## Flutter wrapper
|
|
||||||
+-keep class net.sqlcipher.** { *; }
|
|
||||||
+-keep class io.flutter.app.** { *; }
|
|
||||||
+-keep class io.flutter.plugin.** { *; }
|
|
||||||
+-keep class io.flutter.util.** { *; }
|
|
||||||
+-keep class io.flutter.view.** { *; }
|
|
||||||
+-keep class io.flutter.** { *; }
|
|
||||||
+-keep class io.flutter.plugins.** { *; }
|
|
||||||
+-dontwarn io.flutter.embedding.**
|
|
||||||
+
|
|
||||||
+##---------------Begin: proguard configuration for Gson (Needed for flutter_local_notifications) ----------
|
|
||||||
+# Gson uses generic type information stored in a class file when working with fields. Proguard
|
|
||||||
+# removes such information by default, so configure it to keep all of it.
|
|
||||||
+-keepattributes Signature
|
|
||||||
+
|
|
||||||
+# For using GSON @Expose annotation
|
|
||||||
+-keepattributes *Annotation*
|
|
||||||
+
|
|
||||||
+# Gson specific classes
|
|
||||||
+-dontwarn sun.misc.**
|
|
||||||
+
|
|
||||||
+# Application classes that will be serialized/deserialized over Gson
|
|
||||||
+-keep class com.google.gson.examples.android.model.** { <fields>; }
|
|
||||||
+
|
|
||||||
+# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
|
||||||
+# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
|
||||||
+-keep class * extends com.google.gson.TypeAdapter
|
|
||||||
+-keep class * implements com.google.gson.TypeAdapterFactory
|
|
||||||
+-keep class * implements com.google.gson.JsonSerializer
|
|
||||||
+-keep class * implements com.google.gson.JsonDeserializer
|
|
||||||
+
|
|
||||||
+# Prevent R8 from leaving Data object members always null
|
|
||||||
+-keepclassmembers,allowobfuscation class * {
|
|
||||||
+ @com.google.gson.annotations.SerializedName <fields>;
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
|
||||||
+-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
|
||||||
+-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
|
||||||
+
|
|
||||||
+##---------------End: proguard configuration for Gson (Needed for flutter_local_notifications) ----------
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/android/app/src/main/kotlin/chat/pantheon/hermes/FcmPushService.kt b/android/app/src/main/kotlin/chat/pantheon/hermes/FcmPushService.kt
|
|
||||||
index d9930f55..510e9845 100644
|
|
||||||
--- a/android/app/src/main/kotlin/chat/pantheon/hermes/FcmPushService.kt
|
|
||||||
+++ b/android/app/src/main/kotlin/chat/pantheon/hermes/FcmPushService.kt
|
|
||||||
@@ -1,4 +1,4 @@
|
|
||||||
-/*package chat.pantheon.hermes
|
|
||||||
+package chat.pantheon.hermes
|
|
||||||
|
|
||||||
import com.famedly.fcm_shared_isolate.FcmSharedIsolateService
|
|
||||||
|
|
||||||
@@ -33,4 +33,3 @@ class FcmPushService : FcmSharedIsolateService() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-*/
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/android/settings.gradle b/android/settings.gradle
|
|
||||||
index b2fd960a..fdb01a4d 100644
|
|
||||||
--- a/android/settings.gradle
|
|
||||||
+++ b/android/settings.gradle
|
|
||||||
@@ -20,7 +20,7 @@ plugins {
|
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
|
||||||
id "com.android.application" version "8.7.3" apply false
|
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
|
|
||||||
- // id "com.google.gms.google-services" version "4.3.8" apply false
|
|
||||||
+ id "com.google.gms.google-services" version "4.3.8" apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
include ":app"
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart
|
|
||||||
index 1ba2659a..989f458e 100644
|
|
||||||
--- a/lib/utils/background_push.dart
|
|
||||||
+++ b/lib/utils/background_push.dart
|
|
||||||
@@ -39,7 +39,7 @@ import '../config/setting_keys.dart';
|
|
||||||
import '../widgets/matrix.dart';
|
|
||||||
import 'platform_infos.dart';
|
|
||||||
|
|
||||||
-//import 'package:fcm_shared_isolate/fcm_shared_isolate.dart';
|
|
||||||
+import 'package:fcm_shared_isolate/fcm_shared_isolate.dart';
|
|
||||||
|
|
||||||
class NoTokenException implements Exception {
|
|
||||||
String get cause => 'Cannot get firebase token';
|
|
||||||
@@ -64,7 +64,7 @@ class BackgroundPush {
|
|
||||||
|
|
||||||
final pendingTests = <String, Completer<void>>{};
|
|
||||||
|
|
||||||
- final dynamic firebase = null; //FcmSharedIsolate();
|
|
||||||
+ final dynamic firebase = FcmSharedIsolate();
|
|
||||||
|
|
||||||
DateTime? lastReceivedPush;
|
|
||||||
|
|
||||||
diff --git a/pubspec.yaml b/pubspec.yaml
|
|
||||||
index fb3e3ca4..039b2ccc 100644
|
|
||||||
--- a/pubspec.yaml
|
|
||||||
+++ b/pubspec.yaml
|
|
||||||
@@ -25,7 +25,7 @@ dependencies:
|
|
||||||
dynamic_color: ^1.7.0
|
|
||||||
emoji_picker_flutter: ^4.3.0
|
|
||||||
emojis: ^0.9.9
|
|
||||||
- #fcm_shared_isolate: ^0.2.0
|
|
||||||
+ fcm_shared_isolate: ^0.2.0
|
|
||||||
file_picker: ^8.1.2
|
|
||||||
file_selector: ^1.0.3
|
|
||||||
flutter:
|
|
||||||
Loading…
Reference in New Issue